#!/usr/bin/env python import argparse import os import shutil import tempfile from config import Config from log import DEBUG, INFO from model import Tracks, session from mutagen.flac import FLAC from pydub import AudioSegment, effects from tinytag import TinyTag def main(): "Main loop" INFO("Starting") # Parse command line p = argparse.ArgumentParser() p.add_argument('-u', '--update', action="store_true", dest="update", default=True, help="Update database") args = p.parse_args() # Run as required if args.update: INFO("Updating database") update_db() INFO("Finished") def add_path_to_db(path): "Add passed path to database along with metadata" track = Tracks.get_or_create(path) tag = TinyTag.get(path) audio = get_audio_segment(path) track.title = tag.title track.artist = tag.artist track.duration = int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000) track.start_gap = leading_silence(audio) track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 track.silence_at = round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 track.mtime = os.path.getmtime(path) session.commit() if Config.NORMALISE_ON_IMPORT: # Get current file gid, uid and permissions stats = os.stat(path) try: # Copy original file fd, temp_path = tempfile.mkstemp() shutil.copyfile(path, temp_path) except Exception as err: DEBUG(f"songdb.add_path_to_db({path}): err1: {str(err)}") return # Overwrite original file with normalised output normalised = effects.normalize(audio) try: normalised.export(path, format=os.path.splitext(path)[1][1:]) # Fix up permssions and ownership os.chown(path, stats.st_uid, stats.st_gid) os.chmod(path, stats.st_mode) # Copy tags src = FLAC(temp_path) dst = FLAC(path) for tag in src: dst[tag] = src[tag] dst.save() except Exception as err: DEBUG(f"songdb.add_path_to_db({path}): err2: {str(err)}") # Restore original file shutil.copyfile(path, temp_path) finally: if os.path.exists(temp_path): os.remove(temp_path) return track def get_audio_segment(path): try: if path.endswith('.mp3'): return AudioSegment.from_mp3(path) elif path.endswith('.flac'): return AudioSegment.from_file(path, "flac") except AttributeError: return None def leading_silence(audio_segment, silence_threshold=Config.DBFS_SILENCE, chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): """ Returns the millisecond/index that the leading silence ends. audio_segment - the segment to find silence in silence_threshold - the upper bound for how quiet is silent in dFBS chunk_size - chunk size for interating over the segment in ms https://github.com/jiaaro/pydub/blob/master/pydub/silence.py """ trim_ms = 0 # ms assert chunk_size > 0 # to avoid infinite loop while ( audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 silence_threshold and trim_ms < len(audio_segment)): trim_ms += chunk_size # if there is no end it should return the length of the segment return min(trim_ms, len(audio_segment)) def fade_point(audio_segment, fade_threshold=Config.DBFS_FADE, chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): """ Returns the millisecond/index of the point where the fade is down to fade_threshold and doesn't get louder again. audio_segment - the sdlg_search_database_uiegment to find silence in fade_threshold - the upper bound for how quiet is silent in dFBS chunk_size - chunk size for interating over the segment in ms """ assert chunk_size > 0 # to avoid infinite loop segment_length = audio_segment.duration_seconds * 1000 # ms trim_ms = segment_length - chunk_size while ( audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold and trim_ms > 0): # noqa W503 trim_ms -= chunk_size # if there is no trailing silence, return lenght of track (it's less # the chunk_size, but for chunk_size = 10ms, this may be ignored) return int(trim_ms) def rescan_database(): tracks = Tracks.get_all_tracks() total_tracks = len(tracks) track_count = 0 for track in tracks: track_count += 1 print(f"Track {track_count} of {total_tracks}") audio = get_audio_segment(track.path) track.start_gap = leading_silence(audio) track.fade_at = fade_point(audio) track.silence_at = trailing_silence(audio) session.commit() def trailing_silence(audio_segment, silence_threshold=-50.0, chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): return fade_point(audio_segment, silence_threshold, chunk_size) def update_db(): """ Repopulate database """ # Search for tracks in only one of directory and database db_paths = set(Tracks.get_all_paths()) os_paths_list = [] for root, dirs, files in os.walk(Config.ROOT): for f in files: path = os.path.join(root, f) ext = os.path.splitext(f)[1] if ext in [".flac", ".mp3"]: os_paths_list.append(path) os_paths = set(os_paths_list) for path in list(db_paths - os_paths): # TODO INFO(f"To remove from database: {path}") for path in list(os_paths - db_paths): # TODO INFO(f"Adding to dataabase: {path}") add_path_to_db(path) if __name__ == '__main__' and '__file__' in globals(): main()