198 lines
6.0 KiB
Python
Executable File
198 lines
6.0 KiB
Python
Executable File
#!/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")
|
|
with Session() as session:
|
|
update_db(session)
|
|
|
|
INFO("Finished")
|
|
|
|
|
|
def add_path_to_db(session, path):
|
|
"Add passed path to database along with metadata"
|
|
|
|
track = Tracks.get_or_create(session, 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)
|
|
|
|
|
|
# Current unused (1 June 2021)
|
|
# def rescan_database(session):
|
|
#
|
|
# tracks = Tracks.get_all_tracks(session)
|
|
# 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(session):
|
|
"""
|
|
Repopulate database
|
|
"""
|
|
|
|
# Search for tracks in only one of directory and database
|
|
|
|
db_paths = set(Tracks.get_all_paths(session))
|
|
|
|
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(session, path)
|
|
|
|
|
|
if __name__ == '__main__' and '__file__' in globals():
|
|
main()
|