diff --git a/00_exportchapters.py b/00_exportchapters.py index ae322a9..4f91215 100644 --- a/00_exportchapters.py +++ b/00_exportchapters.py @@ -1,4 +1,4 @@ -#Confirmed working as of 1/22/23 +'''Align tracks end to end, chop based on chapters list durations, export to final folder, archive originals''' import os import shutil @@ -18,19 +18,22 @@ def export_audio_file(start, end, name): author_name = folder.split(" - ")[0].strip() export_folder = os.path.join(destination_path, author_name, folder_name) author_folder = os.path.join(destination_path, author_name) + '''If an author folder doesn't already exist it makes one''' if not os.path.exists(author_folder): os.makedirs(author_folder) export_folder = os.path.join(author_folder, folder_name) + '''Skips the above if an author already exists and adds the book to it''' if not os.path.exists(export_folder): os.makedirs(export_folder) export_path = os.path.join(export_folder, f"{name}.mp3") export.export(export_path, format="mp3") + '''Defines metadata & artwork files''' metadata_file = os.path.join(folder_path, "cleaned_metadata.json") - chapters_file = os.path.join(folder_path, "overdrive_chapters_ms.txt") album_art = os.path.join(folder_path, "folder.jpg") if os.path.exists(album_art): + '''Copies metadata & artwork files alongside chapterized audio''' shutil.copy(album_art, export_folder) shutil.copy(metadata_file, export_folder) @@ -40,7 +43,7 @@ for folder in sorted(os.listdir(main_directory)): folder_path = os.path.join(main_directory, folder) if os.path.isdir(folder_path): try: - # Import MP3s in alphabetical order + '''Import MP3s in alphabetical order''' mp3_files = sorted(os.listdir(folder_path)) mp3_files = [os.path.join(folder_path, file) for file in mp3_files if file.endswith(".mp3")] @@ -49,10 +52,10 @@ for folder in sorted(os.listdir(main_directory)): print(mp3_file) combined += AudioSegment.from_file(mp3_file) - # Align end to end + '''Align end to end''' combined = combined.set_channels(1) - # Import labels + '''Import label names/durations''' labels_file = os.path.join(folder_path, "overdrive_chapters_ms_spans.txt") labels = [] with open(labels_file, "r") as f: @@ -60,25 +63,27 @@ for folder in sorted(os.listdir(main_directory)): start, end, name = line.strip().split("\t") labels.append((start, end, name)) - # Initialize counter for duplicate label names + '''Initialize counter for duplicate label names''' counter = {} - # Initialize counter for file export + '''Initialize counter for file export''' file_count = 0 for i, label in enumerate(labels): start, end, name = label + '''This ensures that the final end duration matches the length of the audiobook''' if end == "0" and i == len(labels) - 1: end = combined.duration_seconds * 1000 file_count += 1 name = f"{file_count:03}_{name.replace('/', '_')}" - # format file_count as 3 digit number + '''format file_count as 3 digit number''' export_audio_file(start, end, name) - + + '''Prints if there's an error''' except Exception as e: print(f"Error processing {folder}: {e}") - #Archive everything except chapters_list.py + '''Archive everything except chapters_list.py''' if os.path.isdir(folder_path) and folder != 'chapters_list.py': shutil.move(folder_path, archive + '/' + folder) \ No newline at end of file diff --git a/chapter_ms.py b/chapter_ms.py index 4619412..862034c 100644 --- a/chapter_ms.py +++ b/chapter_ms.py @@ -1,4 +1,4 @@ -#Confirmed working as of 1/22/23 +'''Converts label durations to milliseconds for pydub to read''' import os import re @@ -6,29 +6,30 @@ from datetime import datetime, timedelta from config import annex def duration_to_milliseconds(duration): + '''Conversion from Seconds to Milliseconds''' hours, minutes, seconds = duration.split(':') seconds = int(hours)*3600 + int(minutes)*60 + float(seconds) return int(seconds * 1000) -# path to directory containing the files +'''path to directory containing the files''' directory = annex -# loop through all files in directory tree +'''loop through all files in directory tree''' for dirpath, dirnames, filenames in os.walk(directory): for filename in filenames: if filename.startswith("overdrive_chapters") and filename.endswith(".txt"): - # open the file and read the contents + '''open the chapters file and read the contents''' with open(os.path.join(dirpath, filename), 'r') as f: lines = f.readlines() - # create a new file with the same name but with "milliseconds" added to the end + '''create a new file with the same name but with "ms" added to the end''' new_filename = filename[:-4] + "_ms.txt" with open(os.path.join(dirpath, new_filename), 'w') as new_file: - # loop through each line in the file + '''loop through each line in the file''' for line in lines: - # split the line into duration and label using regular expression + '''split the line into duration and label name using regular expression''' duration, label = re.split(r"\s+", line.strip(), 1) - # convert the duration to milliseconds + '''convert the duration to milliseconds''' milliseconds = duration_to_milliseconds(duration) - # write the converted duration and label to the new file + '''write the converted duration and label to the new file''' new_file.write(f'{milliseconds} {label}\n') diff --git a/config.py b/config.py index 21b7251..442f173 100644 --- a/config.py +++ b/config.py @@ -1,15 +1,17 @@ -# Define directories -# I like having multiple folders so as to visualize each step happening -# but you could easily adjust the setup to fit your preferences +''' +Define directories +I like having multiple folders so as to visualize each step happening +but you could easily adjust the setup to fit your preferences +''' -# Where the unpackaged odm mp3s will get moved +'''Where the unpackaged odm mp3s will get moved''' annex="/Users/jonas/Documents/SERVER/BOOKS/01_ANNEX" -# Where chapterized and labeled mp3s will be stored +'''Where chapterized and labeled mp3s will be stored''' circulation="/Users/jonas/Documents/SERVER/BOOKS/02_CIRCULATION" -# Where the final finished product will land +'''Where the final finished product will land''' bookshelf="/Users/jonas/Documents/SERVER/BOOKS/03_BOOKSHELF" -# Where the scraps go +'''Where the scraps go''' archive="/Users/jonas/Documents/SERVER/BOOKS/00_ARCHIVE" \ No newline at end of file diff --git a/durations.py b/durations.py index a822a07..cc36562 100644 --- a/durations.py +++ b/durations.py @@ -1,4 +1,4 @@ -#Confirmed working as of 1/22/23 +'''Takes the contents of the Milliseconds durations/labels file and creates start/end times''' import os from config import annex @@ -7,6 +7,7 @@ root_dir = annex for subdir, dirs, files in os.walk(root_dir): for file in files: + '''Locates "overdrive_chapters_ms.txt"''' file_path = os.path.join(subdir, file) if file == 'overdrive_chapters_ms.txt': with open(file_path, "r") as file: @@ -16,23 +17,24 @@ for subdir, dirs, files in os.walk(root_dir): for i in range(len(lines)): parts = lines[i].split(" ") if i == len(lines) - 2: - # If it's the second-to-last line, write the last line's number and 0 + '''If it's the second-to-last line, write the last line's number and 0''' file.write(parts[0]) file.write("\t" + lines[i + 1].split(" ")[0]) file.write("0") elif i != len(lines) - 1: - # Otherwise, write the number from the next line to the file + '''Otherwise, write the number from the next line to the file''' file.write(parts[0]) file.write("\t" + lines[i + 1].split(" ")[0]) else: - # If it's the last line, just write the number and 0 + '''If it's the last line, just write the number and 0''' if parts[0] != "": file.write(parts[0]) file.write("0") - # Write the rest of the line to the file + '''Write the rest of the line to the file''' file.write("\t" + " ".join(parts[1:])) if i != len(lines) - 1 or (i == len(lines) - 1 and lines[i] != ""): file.write("\n") if lines[-1] == "": + '''Ensures there's no empty line at the end of the txt''' file.seek(file.tell()-1, os.SEEK_SET) file.truncate() \ No newline at end of file diff --git a/extract_overdrive_chapters.py b/extract_overdrive_chapters.py index 97ca766..7856851 100644 --- a/extract_overdrive_chapters.py +++ b/extract_overdrive_chapters.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -# Credit to ex-nerd -# -# Recursively scans current or specified directory for all subdirectories -# containing mp3 files. If these mp3 files contain overdrive chapter markers -# (id3 tag), writes overdrive_chapters.txt to the same directory. -# -# Usage: -# -# extract_overdrive_chapters.py [optional directory path] -# -# Use with build_m4b from https://github.com/ex-nerd/audiotools -# -# Note: Due to overdrive low quality, there is no point in encoding aac files -# with better than: 64kbps stereo, HE, optimize for voice -# +''' +Credit to ex-nerd + +Recursively scans current or specified directory for all subdirectories +containing mp3 files. If these mp3 files contain overdrive chapter markers +(id3 tag), writes overdrive_chapters.txt to the same directory. + +Usage: + +extract_overdrive_chapters.py [optional directory path] + +Use with build_m4b from https://github.com/ex-nerd/audiotools + +Note: Due to overdrive low quality, there is no point in encoding aac files +with better than: 64kbps stereo, HE, optimize for voice +''' import os, sys, re import mutagen.id3 as id3 @@ -55,16 +56,16 @@ def load_mp3(total, dir, file): name = re.sub(r"^\*(.+)\*$", r"\1", name) name = re.sub( r"\s*\([^)]*\)$", "", name - ) # ignore any sub-chapter markers from Overdrive + ) '''ignore any sub-chapter markers from Overdrive''' name = re.sub( r"\s+\(?continued\)?$", "", name - ) # ignore any sub-chapter markers from Overdrive + ) '''ignore any sub-chapter markers from Overdrive''' name = re.sub( r"\s+-\s*$", "", name - ) # ignore any sub-chapter markers from Overdrive + ) '''ignore any sub-chapter markers from Overdrive''' name = re.sub( r"^Dis[kc]\s+\d+\W*$", "", name - ) # ignore any disk markers from Overdrive + ) '''ignore any disk markers from Overdrive''' name = name.strip() t_parts = list(length.split(":")) t_parts.reverse() @@ -81,7 +82,7 @@ def load_mp3(total, dir, file): def visit(dirname, filenames): print(dirname) os.chdir(dirname) - # Parse the files + '''Parse the files''' total = 0 all_chapters = OrderedDict() for file in sorted(filenames): diff --git a/final_metadata_add.py b/final_metadata_add.py index 1119997..abe8dbb 100644 --- a/final_metadata_add.py +++ b/final_metadata_add.py @@ -1,4 +1,4 @@ -#Confirmed working as of 1/23/23 +'''Adds metadata to the chapterized mp3s''' import os import shutil @@ -8,7 +8,7 @@ from mutagen.id3 import ID3, APIC, TIT2, TPE1, TCOM, TCON, TSOA, TRCK, TIT3, TAL from config import circulation,bookshelf def traverse_directory(directory): - # Iterate through all subfolders and files + '''Iterate through all subfolders and files''' for root, dirs, files in os.walk(directory): for dir in dirs: subfolder_path = os.path.join(root, dir) @@ -16,7 +16,7 @@ def traverse_directory(directory): add_metadata(subfolder_path) def add_metadata(directory): - # Get the metadata from the cleaned_metadata.json file in the current subfolder + '''Get the metadata from the cleaned_metadata.json file in the current subfolder''' with open(os.path.join(directory, "cleaned_metadata.json"), "r") as f: json_data = json.load(f) artist = json_data.get("Author", "") @@ -24,32 +24,30 @@ def add_metadata(directory): genre = json_data.get("Subjects", "") album_title = json_data.get("Title", "") - # Rest of the code - - # Replace all occurrences of -s with 's in the album title + '''Replace all occurrences of -s with "'s" in the album title''' album_title = re.sub("-s", "'s", album_title) - # Get a list of all mp3 files in the current directory + '''Get a list of all mp3 files in the current directory''' mp3_files = [f for f in os.listdir(directory) if f.endswith(".mp3")] - # Sort the list of mp3 files alphabetically + '''Sort the list of mp3 files alphabetically''' mp3_files.sort() - # Iterate through the list of mp3 files + '''Iterate through the list of mp3 files''' for i, mp3_file in enumerate(mp3_files, 1): mp3_file_path = os.path.join(directory, mp3_file) - # Extract the title of the mp3 file + '''Extract the title of the mp3 file''' file_name, file_ext = os.path.splitext(mp3_file) title = file_name.lstrip("0123456789_") - # Add the track number to the file using ID3 + '''Add the track number to the file using ID3''' audio = ID3(mp3_file_path) audio.add(TRCK(encoding=3, text=str(i))) audio.add(TIT2(encoding=3, text=title)) audio.save() - # Add the metadata to the file using ID3 + '''Add the metadata to the file using ID3''' audio = ID3(mp3_file_path) audio.add(TPE1(encoding=3, text=artist)) audio.add(TCOM(encoding=3, text=composer)) @@ -57,21 +55,21 @@ def add_metadata(directory): audio.add(TALB(encoding=3, text=album_title)) audio.save() - # Check if folder.jpg exists in the current directory + '''Check if folder.jpg exists in the current directory''' if os.path.exists(os.path.join(directory, "folder.jpg")): - # Add the album art to the file using ID3 + '''Add the album art to the file using ID3''' with open(os.path.join(directory, "folder.jpg"), "rb") as albumart: audio = ID3(mp3_file_path) audio.add(APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=albumart.read())) audio.save() -# Point the script at a directory +'''Point the script at a directory''' directory = circulation -# Traverse the directory and apply the metadata to the mp3 files traverse_directory(directory) for root, dirs, files in os.walk(directory): + '''Traverse the directory and apply the metadata to the mp3 files''' for dir in dirs: subfolder_path = os.path.join(root, dir) parent_folder = os.path.dirname(subfolder_path) diff --git a/metadatatoxml.py b/metadatatoxml.py index dba08dc..e0e1000 100644 --- a/metadatatoxml.py +++ b/metadatatoxml.py @@ -1,4 +1,5 @@ -#Confirmed working as of 1/22/23 + +'''Takes the .odm.metadata file and turns it into metadata.xml''' import os import shutil diff --git a/overdrive_full.sh b/overdrive_full.sh index 8aab91d..5e1e4a2 100644 --- a/overdrive_full.sh +++ b/overdrive_full.sh @@ -1,33 +1,31 @@ #!/bin/bash -#download odm's to chosen directory +#start by downloading odms to chosen directory -# configure variables +#configure variables python3.10 config.py -# unpack odm's -#bash overdrivedelete.sh +#unpack odms +bash overdrivedelete.sh -# extract chapters, specify folder holding the unpackaged overdrive mp3's -##source config.env -#python3 extract_overdrive_chapters.py $annex +#extract chapters, specify folder holding the unpackaged overdrive mp3s +source config.env +python3 extract_overdrive_chapters.py $annex -# clean metadata -#python3 metadatatoxml.py +#clean metadata +python3 metadatatoxml.py -##python3 xmlparse.py +python3 xmlparse.py -# turn chapters into ms -#python3 chapter_ms.py +#turn chapters into ms +python3 chapter_ms.py -# format durations -#python3 durations.py +#format durations +python3 durations.py -# export labeled audio and archive original folder +#export labeled audio and archive original folder # FIX: MAKE SURE IT CAN ADD TO EXISTING AUTHOR FOLDER python3 00_exportchapters.py -# tag the exported audio with proper metadata -python3 final_metadata_add.py - -#manually add to Plex +#tag the exported audio with proper metadata +python3 final_metadata_add.py \ No newline at end of file diff --git a/overdrivedelete.sh b/overdrivedelete.sh index 2f22a05..52d1832 100644 --- a/overdrivedelete.sh +++ b/overdrivedelete.sh @@ -1,4 +1,10 @@ -#This officially works for the full ODM extraction as of 1/21/23 at 9:54pm +#!/bin/bash + +#This is a Frankenstein version of chbrowns overdrive script +# +#It extracts the contents of the odm into a temp directory so as to point the metadata +#file to the export path of the contents of the odm and then deletes the leftover files + source config.env folder=$queue @@ -6,7 +12,7 @@ annex=$annex for file in $folder/*.odm do - # Make temp dir in loop so that it clears even if there's any errors + #Make temp dir in loop so that it clears even if theres any errors temp_dir=$(mktemp -d) cd "$temp_dir" @@ -20,20 +26,22 @@ do sleep 2 # wait for 2 seconds before retrying done - # Get the name of the downloaded folder + #Get the name of the downloaded folder downloaded_folder=$(find "$temp_dir" -type d -mindepth 1 -maxdepth 1 | head -n 1) echo "downloaded_folder: $downloaded_folder" - # Move the downloaded folder to the destination + #Move the downloaded folder to the destination mv "$downloaded_folder" "$annex" #Move the .odm.metadata to the downloaded folder metadata_file=$(find "$folder" -name "*.odm.metadata") mv "$metadata_file" "$annex/$(basename "$downloaded_folder")" + #Remove temp directory rm -rf "$temp_dir" done +#Remove .odm & .odm.license files rm $queue/*.odm rm $queue/*.odm.license \ No newline at end of file diff --git a/xmlparse.py b/xmlparse.py index 8146de1..5f64915 100644 --- a/xmlparse.py +++ b/xmlparse.py @@ -1,4 +1,4 @@ -#Confirmed as of 1/22/23 +'''Parses the metadata.xml file into only the required data''' import os import xml.etree.ElementTree as ET @@ -10,9 +10,11 @@ def extract_metadata(xml_file): root = tree.getroot() data = {} for child in root: + '''Finds occurrences of Title, Creators, Subjects and isolates''' if child.tag == "Title": data["Title"] = child.text elif child.tag == "Creators": + '''Identifies Authors and Narrators''' for creator in child: if creator.attrib["role"] == "Author": data["Author"] = creator.text @@ -26,7 +28,8 @@ def extract_metadata(xml_file): return data def process_folder(folder_path): - for dirpath, dirnames, filenames in os.walk(folder_path): + '''Walks to metadata.xml & exports the parsed version as cleaned_metadata.json''' + for dirpath, filenames in os.walk(folder_path): for filename in filenames: if filename == "metadata.xml": xml_file = os.path.join(dirpath, filename)