Python Forum
Help with: Audiobook Library/Copier Project
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help with: Audiobook Library/Copier Project
#1
Music 
Hello pythons!

Truth: Its my first little coding project. Nothing fancy, just for private use. I went through hours and hours of trial and error with tutorials to get to this point. Please be forgiving and indulge me.

My code is an offline audiobook library/copier.
It allows you to view the audiobooks of a specified folder with nice big covers. It has a preview function that lets you listen to a 20-second preview of each audiobook to check out the reader. It also has a copy function that lets you move audiobooks (i.e.: from your storage folder to your listening folder).

This is what it does:
The extract_cover_art_from_media_file function extracts cover art from an audiobook file and saves it as an image. The on_tile_click function copies an audiobook from storage to the specified folder when you click on the copy button.

The preview_audio_file function plays a 20-second preview of an audiobook. It finds all audio files in a specified subfolder and plays the largest one. I chose this so that I can skip the first parts or intros and get a good impression of the reader. The stop_audio_playback function stops audio playback.

The search_folder function searches through all files in a specified folder and its subfolders. For each file, it checks if it’s an image or an audio file and processes it accordingly. It also searches the metadata for cover images (m4b, m4a). Some of the subfolders in the main folder are just named CD1, CD2, CD3 etc. with one or more audiofiles in them. They are not individual audiobooks because they are just differents Cds making up one large audiobook and are therefore treated seperately.

If you press escape or hit the big red X, the program will close immediately.

Why am I here, asking for help?

1. Multiple embedded audio files handeling

Often one folder contains an audiobook split into multiple audio files. When each of these files has the cover embedded in the metadata, the code interprets each of these audio files as a separate book and creates a tile for each audio file in the GUI. However, since they are all located in the same folder, they should be treated as one single book. I don't know how to get the code to make the distinction.

2. Inclusion of folders with no image data to the GUI
I'd love to have it possible that the program can also work with audiobooks where there is no cover image inside the folder and no cover in the metadata. I tried to make it create a tile with the folders name as a title but to no avail. It always broke the rest of the code.

This is the full code:
(if you want to test it just replace FOLDER_PATH = "C:/Users/xxx/Desktop/xxx" with a path to a folder containing audiobooks )

import os
import shutil
import vlc
import mutagen
from tkinter import *
from PIL import Image, ImageTk
from tkinter import filedialog

# Define constants
FOLDER_PATH = "C:/Users/xxx/Desktop/xxx"

# Create a VLC media player instance
player = vlc.MediaPlayer()

def extract_cover_art_from_media_file(media_file, folder):
    """Extract cover art from a media file."""
    media = mutagen.File(media_file)
    if media.tags is not None and "covr" in media.tags:
        cover_art_data = media.tags["covr"][0]
        image_path = os.path.join(folder, "cover.jpg")
        with open(image_path, "wb") as f:
            f.write(cover_art_data)
        return image_path

def on_tile_click(button, subfolder_path):
    """Handle tile click event."""
    dest_folder = dest_entry.get()
    if dest_folder:
        shutil.copytree(subfolder_path, os.path.join(dest_folder, os.path.basename(subfolder_path)))
        button.configure(bg="green")

def create_button_command(button, subfolder_path):
    """Create a command for a button."""
    return lambda: on_tile_click(button, subfolder_path)

def preview_audio_file(subfolder_path):
    """Preview an audio file."""
    audio_files = []
    for item in sorted(os.listdir(subfolder_path)):
        item_path = os.path.join(subfolder_path, item)
        if item.startswith("CD"):
            for audio_item in os.listdir(item_path):
                if audio_item.endswith((".mp3", ".wav", ".flac", ".m4a", ".opus", ".m4b", ".aax", ".ogg", ".wma", ".aac")):
                    audio_file = os.path.join(item_path, audio_item)
                    audio_files.append(audio_file)
            if audio_files:  # If we found any audio files in the "CD" subfolder, break the loop
                break
        elif item.endswith((".mp3", ".wav", ".flac", ".m4a", ".opus", ".m4b", ".aax", ".ogg", ".wma", ".aac")):
            audio_files.append(item_path)

    if audio_files:
        # Find the largest audio file
        largest_audio_file = max(audio_files, key=os.path.getsize)
        player.set_mrl(largest_audio_file)
        player.play()
        # Get the total length of the audio file in seconds
        length = player.get_length() / 1000  # The get_length() function returns length in milliseconds
        # Calculate the position 60 seconds into the audio file as a percentage of the total length
        position = 20 / length if length > 20 else 0.69  # Ensure position does not exceed 1.0
        # Set the position
        player.set_position(position)

def stop_audio_playback():
    """Stop audio playback."""
    player.stop()

def create_preview_button_command(subfolder_path):
    """Create a command for a preview button."""
    return lambda: preview_audio_file(subfolder_path)

def search_folder(folder_path, frame, row_index, col_index):
    """Search a folder."""
    for item in os.listdir(folder_path):
        item_path = os.path.join(folder_path, item)
        image_file = None
        if os.path.isdir(item_path):
            if not item.startswith("CD"):
                row_index, col_index = search_folder(item_path, frame, row_index, col_index)
        else:
            if item.endswith((".png", ".jpg", ".jpeg", ".webp")):
                image_file = item
            elif item.endswith((".m4a", ".m4b")):
                media_file = os.path.join(folder_path, item)
                image_file = extract_cover_art_from_media_file(media_file, folder_path)

        if image_file:
            image_files = [file for file in os.listdir(folder_path) if file.endswith((".png", ".jpg", ".jpeg", ".webp"))]
            largest_image_file = max(image_files, key=lambda file: os.path.getsize(os.path.join(folder_path, file)))
            image_file = largest_image_file

            image_path = os.path.join(folder_path, image_file)
            image = Image.open(image_path)
            image = image.resize((460, 460))
            photo = ImageTk.PhotoImage(image)
            label = Label(frame, image=photo)
            label.image = photo

            preview_button = Button(frame, text="▶Play", bg="black", fg="white")
            preview_button.configure(command=create_preview_button_command(folder_path))
            preview_button.grid(row=row_index + 1, column=col_index)

            stop_button = Button(frame, text="⏹ Stop", bg="black", fg="white")
            stop_button.configure(command=stop_audio_playback)
            stop_button.grid(row=row_index + 2, column=col_index)

            copy_button = Button(frame, text="❤ Copy", bg="white", fg="black")
            copy_button.configure(command=create_button_command(copy_button, folder_path))
            copy_button.grid(row=row_index + 3, column=col_index)

            label.grid(row=row_index, column=col_index)
            col_index += 1
            if col_index == 4:
                col_index = 0
                row_index += 4

    return row_index, col_index

def create_gui():
    """Create the GUI."""
    global dest_entry
    window = Tk()
    window.title("AUDIOBOOK LIBRARY COPY TOOL")
    window.attributes('-fullscreen', True)
    window['bg'] = "black"
    
    # Close button
    close_button = Button(window, text="X", command=window.destroy, fg="white", bg="red", font=("Arial", 20))
    close_button.pack(anchor='ne')

    # Destination label and entry
    dest_label = Label(window, text="Copy Destination:")
    dest_entry = Entry(window)
    dest_label.pack()
    dest_entry.pack()

    def choose_dest_folder():
        """Choose a destination folder."""
        dest_folder = filedialog.askdirectory(initialdir="/", title="Select Destination Folder")
        dest_entry.delete(0, END)
        dest_entry.insert(0, dest_folder)

    choose_button = Button(window, text="Choose...", command=choose_dest_folder)
    choose_button.pack()

    canvas = Canvas(window)
    scrollbar = Scrollbar(window, command=canvas.yview, width=60)
    frame = Frame(canvas)

    canvas.configure(yscrollcommand=scrollbar.set)
    canvas.pack(side="left", fill="both", expand=True)
    scrollbar.pack(side="right", fill="y")

    canvas.create_window((0, 0), window=frame, anchor="nw")
    frame.bind("<Configure>", lambda event: canvas.configure(scrollregion=canvas.bbox("all")))

    def scroll(event):
        """Handle scroll event."""
        if event.state & 1:
            canvas.xview_scroll(-1 * int(event.delta / 120), "units")
        else:
            canvas.yview_scroll(-1 * int(event.delta / 120), "units")

    canvas.bind("<MouseWheel>", scroll)

    row_index, col_index = search_folder(FOLDER_PATH, frame, 0, 0)

    window.bind('<Escape>', lambda e: window.destroy())

    window.mainloop()

if __name__ == "__main__":
    create_gui()
I don't know, maybe its very easy. I just can't get my head around it anymore. I tried to get help with GPT4 but it didn't get it right either.

Sorry for the long thread. If anyone can and cares to give a newbie a hand, it would be much appreciated!

Thank you!
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Help needed in finding a library for this project PythonEnthusiast1729 7 832 Dec-27-2023, 11:27 AM
Last Post: PythonEnthusiast1729
  Problem with importing python-telegram library into the project gandonio 1 1,583 Nov-01-2022, 02:19 AM
Last Post: deanhystad
  making an audiobook using pygame Parshaw 0 1,137 Nov-30-2020, 08:35 AM
Last Post: Parshaw
  Join the Python Standard Library to my project sylas 1 2,210 May-16-2018, 05:59 AM
Last Post: buran
  Library Program Prison Project lewashby 9 6,286 Jul-13-2017, 05:25 AM
Last Post: Larz60+
  PyInstaller, how to create library folder instead of library.zip file ? harun2525 2 4,816 May-06-2017, 11:29 AM
Last Post: harun2525

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020