def sane_file(ctx: click.Context, param: click.Parameter, value: Path) -> File: if not param.name: logger.error("no param name set") raise click.Abort() file = File.from_path(value) ctx.params[param.name] = file return file
def worker(path: Path) -> File | None: try: file = File.from_path(path=path) return file.to_mp3(flat=flat, destination=destination) except MusicbotError as e: self.err(e) except Exception as e: # pylint: disable=broad-except logger.error(f"{path} : unable to convert to mp3 : {e}") return None
def find(file: File, acoustid_api_key: str) -> None: yt_path = f"{file.artist} - {file.title}.mp3" try: file_id = file.fingerprint(acoustid_api_key) print( f'Searching for artist {file.artist} and title {file.title} and duration {seconds_to_human(file.length)}' ) ydl_opts = { 'format': 'bestaudio/best', 'quiet': True, 'cachedir': False, 'no_warnings': True, 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'outtmpl': yt_path, } with youtube_dl.YoutubeDL(ydl_opts) as ydl: infos = ydl.extract_info(f"ytsearch1:'{file.artist} {file.title}'", download=True) url = None for entry in infos['entries']: url = entry['webpage_url'] break yt_ids = acoustid.match(acoustid_api_key, yt_path) yt_id = None for _, recording_id, _, _ in yt_ids: yt_id = recording_id break if file_id == yt_id: print(f'Found: fingerprint {file_id} | url {url}') else: print( f'Not exactly found: fingerprint file: {file_id} | yt: {yt_id} | url {url}' ) print(f'Based only on duration, maybe: {url}') except acoustid.WebServiceError as e: logger.error(e) except youtube_dl.utils.DownloadError as e: logger.error(e) finally: try: if yt_path: os.remove(yt_path) except OSError: logger.warning(f"File not found: {yt_path}")
def test_mp3_tags() -> None: m = File.from_path(path=fixtures.one_mp3) assert m.artist == "1995" assert m.title == "La Flemme" assert m.album == "La Source" assert m.genre == "Rap" assert m.track == 2 assert m._comment == "rap french" assert m.keywords == {'rap', 'french'} assert m.rating == 4.5 assert m.length == 258
def tags( file: File, link_options: LinkOptions, output: str, ) -> None: logger.info(file.handle.tags.keys()) music = file.to_music(link_options) if output == 'json': MusicbotObject.print_json(asdict(music)) return print(music)
def test_flac_tags() -> None: m = File.from_path(path=fixtures.one_flac) assert m.artist == "Buckethead" assert m.title == "Welcome To Bucketheadland" assert m.album == "Giant Robot" assert m.genre == "Avantgarde" assert m.track == 2 assert m._description == "rock cutoff" assert m.keywords == {'rock', 'cutoff'} assert m.rating == 5.0 assert m.length == 1
async def upsert_path( self, path: Path, link_options: LinkOptions = DEFAULT_LINK_OPTIONS) -> File | None: try: file = File.from_path(path=path) if 'no-title' in file.inconsistencies or 'no-artist' in file.inconsistencies or 'no-album' in file.inconsistencies: MusicbotObject.warn( f"{file} : missing mandatory fields title/album/artist : {file.inconsistencies}" ) return None if self.dry: return file input_music = file.to_music(link_options) params = dict( query=UPSERT_QUERY, **asdict(input_music), ) result = await self.client.query_required_single(**params) output_music = Music( title=result.name, artist=result.artist_name, album=result.album_name, genre=result.genre_name, size=result.size, length=result.length, keywords=set(result.all_keywords), track=result.track, rating=result.rating, links=set(result.links), ) music_diff = DeepDiff(asdict(input_music), asdict(output_music), ignore_order=True) if music_diff: MusicbotObject.err(f"{file} : file and music diff detected : ") MusicbotObject.print_json(music_diff, file=sys.stderr) return file except edgedb.errors.TransactionSerializationError as e: raise MusicbotError(f"{path} : transaction error : {e}") from e except edgedb.errors.NoDataError as e: raise MusicbotError( f"{path} : no data result for query : {e}") from e except OSError as e: logger.error(e) return None
def inconsistencies( file: File, fix: bool, checks: list[str], ) -> None: table = Table("Path", "Inconsistencies") try: if fix and not file.fix(checks=checks): MusicbotObject.err(f"{file} : unable to fix inconsistencies") if file.inconsistencies: table.add_row(str(file.path), ', '.join(file.inconsistencies)) except (OSError, MutagenError): table.add_row(str(file.path), "could not open file") MusicbotObject.console.print(table)
def set_tags( paths: list[Path], title: str | None = None, artist: str | None = None, album: str | None = None, genre: str | None = None, keywords: list[str] | None = None, rating: float | None = None, track: int | None = None, ) -> None: for path in paths: file = File.from_path(path=path) if not file.set_tags( title=title, artist=artist, album=album, genre=genre, keywords=keywords, rating=rating, track=track, ): MusicbotObject.err(f"{file} : unable to set tags")
def worker(path: Path) -> File | None: try: return File.from_path(path=path) except OSError as e: logger.error(e) return None
def sync( musicdb: MusicDb, music_filter: MusicFilter, delete: bool, destination: Path, yes: bool, flat: bool, ) -> None: logger.info(f'Destination: {destination}') future = musicdb.make_playlist(music_filter) playlist = async_run(future) if not playlist.musics: click.secho('no result for filter, nothing to sync') return folders = Folders(directories=[destination], extensions=set()) logger.info(f"Files : {len(folders.files)}") if not folders.files: logger.warning("no files found in destination") destinations = { str(path)[len(str(destination)) + 1:]: path for path in folders.paths } musics: list[File] = [] for music in playlist.musics: for link in music.links: try: if link.startswith('ssh://'): continue music_to_sync = File.from_path(Path(link)) musics.append(music_to_sync) except OSError as e: logger.error(e) logger.info(f"Destinations : {len(destinations)}") if flat: sources = {music.flat_filename: music.path for music in musics} else: sources = {music.filename: music.path for music in musics} logger.info(f"Sources : {len(sources)}") to_delete = set(destinations.keys()) - set(sources.keys()) if delete and (yes or click.confirm( f'Do you really want to delete {len(to_delete)} files and playlists ?' )): with MusicbotObject.progressbar(max_value=len(to_delete)) as pbar: for d in to_delete: try: path_to_delete = Path(destinations[d]) pbar.desc = f"Deleting musics and playlists: {path_to_delete.name}" if MusicbotObject.dry: logger.info(f"[DRY-RUN] Deleting {path_to_delete}") continue try: logger.info(f"Deleting {path_to_delete}") path_to_delete.unlink() except OSError as e: logger.error(e) finally: pbar.value += 1 pbar.update() to_copy = set(sources.keys()) - set(destinations.keys()) with MusicbotObject.progressbar(max_value=len(to_copy)) as pbar: logger.info(f"To copy: {len(to_copy)}") for c in sorted(to_copy): final_destination = destination / c try: path_to_copy = Path(sources[c]) pbar.desc = f'Copying {path_to_copy.name} to {destination}' if MusicbotObject.dry: logger.info( f"[DRY-RUN] Copying {path_to_copy.name} to {final_destination}" ) continue logger.info( f"Copying {path_to_copy.name} to {final_destination}") Path(final_destination).parent.mkdir(exist_ok=True) _ = shutil.copyfile(path_to_copy, final_destination) except KeyboardInterrupt: logger.debug(f"Cleanup {final_destination}") try: final_destination.unlink() except OSError: pass raise finally: pbar.value += 1 pbar.update() for d in folders.flush_empty_directories(): if any(e in d for e in folders.except_directories): logger.debug(f"Invalid path {d}") continue if not MusicbotObject.dry: shutil.rmtree(d) logger.info(f"[DRY-RUN] Removing empty dir {d}")
def fingerprint(file: File, acoustid_api_key: str) -> None: print(file.fingerprint(acoustid_api_key))
def flac2mp3(file: File, destination: Path) -> None: if not file.to_mp3(destination): MusicbotObject.err(f"{file} : unable to convert to MP3")
def delete_keywords(file: File, keywords: set[str]) -> None: if not file.delete_keywords(keywords): MusicbotObject.err(f"{file} : unable to delete keywords")
def add_keywords(file: File, keywords: set[str]) -> None: if not file.add_keywords(keywords): MusicbotObject.err(f"{file} : unable to add keywords")