909 lines
37 KiB
Python
Executable File
909 lines
37 KiB
Python
Executable File
import mutagen
|
|
from eyed3.id3.frames import ImageFrame
|
|
from mutagen.flac import FLAC
|
|
from mutagen.mp3 import MP3
|
|
from mutagen.id3 import ID3
|
|
from mutagen.wave import WAVE
|
|
from mutagen.oggvorbis import OggVorbis
|
|
from mutagen.id3 import ID3, TIT2, TALB, TPE1, TPE2, COMM, TCOM, TCON, TDRC, TDRL, TRCK, TPUB, POPM, APIC
|
|
from mutagen.easyid3 import EasyID3
|
|
import os
|
|
from os import listdir
|
|
from os.path import isfile, join
|
|
import psutil
|
|
import datetime as dt
|
|
|
|
from icrawler.builtin import GoogleImageCrawler
|
|
|
|
from datetime import datetime
|
|
import spotify_search
|
|
|
|
import requests
|
|
|
|
import logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="{asctime} - {levelname} - [{funcName}:{lineno}] - {message}",
|
|
style="{",
|
|
datefmt="%Y-%m-%d %H:%M",
|
|
)
|
|
|
|
# TODO check if title is correct (not like "artist - name")
|
|
|
|
def switch_ID3_flag_tag(audio, ID3_tag, flac_tag):
|
|
try:
|
|
audio[flac_tag] = str(audio[ID3_tag][0].text[0])
|
|
logging.info("switched ID3 tag " + ID3_tag + " to flac tag " + flac_tag + " with value " + str(audio[flac_tag]) + ", type " + str(type(audio[flac_tag])))
|
|
audio.pop(ID3_tag, None)
|
|
except:
|
|
logging.info("could not switch ID3 tag " + ID3_tag + " to flac tag " + flac_tag)
|
|
|
|
def remove_flac_ID3_tags(audio, x: str):
|
|
if x.endswith(".flac"):
|
|
logging.info("switching ID3 tags to flac tags for file " + x)
|
|
|
|
switch_ID3_flag_tag(audio,"TPE2","BAND")
|
|
switch_ID3_flag_tag(audio, "TPE1", "ARTIST")
|
|
switch_ID3_flag_tag(audio, "TIT2", "TITLE")
|
|
switch_ID3_flag_tag(audio, "TALB", "ALBUM")
|
|
switch_ID3_flag_tag(audio, "COMM", "COMMENT")
|
|
switch_ID3_flag_tag(audio, "TCOM", "COMPOSER")
|
|
switch_ID3_flag_tag(audio, "TCON", "GENRE")
|
|
switch_ID3_flag_tag(audio, "TRCK", "TRACKNUMBER")
|
|
switch_ID3_flag_tag(audio, "TDRC", "DATE")
|
|
switch_ID3_flag_tag(audio, "TPUB", "PUBLISHER")
|
|
|
|
audio.pop("POPM", None)
|
|
audio.pop("APIC", None)
|
|
|
|
|
|
|
|
def make_folder(foldername):
|
|
try:
|
|
logging.info("Creating folder " + foldername)
|
|
if "/" in foldername:
|
|
folders = foldername.split('/')
|
|
pos = "."
|
|
for fold in folders:
|
|
pos = join(pos,fold)
|
|
if not os.path.exists(pos):
|
|
os.mkdir(pos)
|
|
else:
|
|
os.mkdir(foldername)
|
|
except Exception as err:
|
|
logging.error("could not create folder " + foldername)
|
|
logging.error(err)
|
|
|
|
|
|
def search_google_images_and_save(x: str, audio):
|
|
if x.endswith(".flac"):
|
|
remove_flac_ID3_tags(audio,x)
|
|
|
|
audio.save(x)
|
|
|
|
found_image = False
|
|
# Try album art search first
|
|
if (check_tag(audio, x, "TALB","album")):
|
|
if x.endswith(".flac"):
|
|
songpath = join(".",str(audio["artist"]),str(audio["album"]))
|
|
else:
|
|
artist_folder = str(audio.get("TPE2", audio.get("TPE1", "Unknown Artist")))
|
|
songpath = join(".", artist_folder, str(audio["TALB"]))
|
|
if "\x00" in songpath:
|
|
songpath = songpath.replace("\x00",", ")
|
|
make_folder(songpath)
|
|
os.replace(join(".",x),join(songpath,x))
|
|
if x.endswith(".flac"):
|
|
artist_val = str(audio.get("artist", "Unknown Artist"))
|
|
album_val = str(audio.get("album", "Unknown Album"))
|
|
else:
|
|
artist_val = str(audio.get("TPE2", audio.get("TPE1", "Unknown Artist")))
|
|
album_val = str(audio.get("TALB", "Unknown Album"))
|
|
# Skip Google search if artist or album is unknown
|
|
if "Unknown Artist" in artist_val or "Unknown Album" in album_val:
|
|
logging.info("Artist or album is unknown, skipping Google image search")
|
|
else:
|
|
google_keyword = artist_val + " " + album_val + " album"
|
|
logging.info("Moved file! Now searching for album art... keyword is " + google_keyword)
|
|
google_Crawler = GoogleImageCrawler(storage = {'root_dir': songpath})
|
|
try:
|
|
result = google_Crawler.crawl(keyword = google_keyword, max_num = 1)
|
|
found_image = True
|
|
except Exception as e:
|
|
logging.info(f"could not find Google result by album, searching by track and artist: {e}")
|
|
# Fallback: if no image found, try searching by song and artist
|
|
if not found_image or not any(f.split('.')[-1].lower() in ["jpg","png"] for f in listdir(songpath)):
|
|
song_keyword = artist_val + " " + str(audio.get("TIT2", ""))
|
|
logging.info("Fallback: searching for song art... keyword is " + song_keyword)
|
|
try:
|
|
google_Crawler.crawl(keyword = song_keyword, max_num = 1)
|
|
found_image = True
|
|
except Exception as e:
|
|
logging.info(f"could not find Google result by song: {e}")
|
|
# Rename cover art file if found
|
|
for f in listdir(songpath):
|
|
if (isfile(join(songpath,f)) and f.split(".")[-1].lower() in ["jpg","png"]):
|
|
os.replace(join(songpath,f),join(songpath,"Cover." + f.split(".")[-1].lower()))
|
|
logging.info("Done!")
|
|
else:
|
|
# search for song name and artist
|
|
songpath = join(".",str(audio.get("TPE2", "Unknown Artist")),str(audio.get("TIT2", "Unknown Title")))
|
|
make_folder(songpath)
|
|
os.replace(join(".",x),join(songpath,x))
|
|
artist_keyword = str(audio.get("TPE2", "Unknown Artist"))
|
|
title_keyword = str(audio.get("TIT2", "Unknown Title"))
|
|
# Skip Google search if artist or title is unknown
|
|
if "Unknown Artist" in artist_keyword or "Unknown Title" in title_keyword:
|
|
logging.info("Artist or title is unknown, skipping Google image search")
|
|
else:
|
|
song_keyword = artist_keyword + " " + title_keyword
|
|
logging.info("Moved file! Now searching for album art... keyword is " + song_keyword)
|
|
google_Crawler = GoogleImageCrawler(storage = {'root_dir': songpath})
|
|
try:
|
|
google_Crawler.crawl(keyword = song_keyword, max_num = 1)
|
|
found_image = True
|
|
except Exception as e:
|
|
logging.info(f"could not find Google result by track and artist: {e}")
|
|
for f in listdir(songpath):
|
|
if (isfile(join(songpath,f)) and f.split(".")[-1].lower() in ["jpg","png"]):
|
|
os.replace(join(songpath,f),join(songpath,"Cover." + f.split(".")[-1].lower()))
|
|
logging.info("Done!")
|
|
|
|
# TIT2 = title,
|
|
# TPE1 = artist,
|
|
# TPE2 = band,
|
|
# TALB = album,
|
|
# COMM = comment,
|
|
# TCOM = composer,
|
|
# TCON = genre,
|
|
# TRCK = number,
|
|
# TDRC = year,
|
|
# TPUB = publisher
|
|
|
|
def create_ID3_tag(audio, tagname: str, textvalue: str):
|
|
logging.info("creating ID3 tag " + tagname + " with value " + textvalue)
|
|
if tagname == "TALB":
|
|
audio[tagname] = TALB(encoding=3,text=textvalue)
|
|
elif tagname == "TIT2":
|
|
audio[tagname] = TIT2(encoding=3,text=textvalue)
|
|
elif tagname == "TPE1":
|
|
audio[tagname] = TPE1(encoding=3,text=textvalue)
|
|
elif tagname == "TPE2":
|
|
audio[tagname] = TPE2(encoding=3,text=textvalue)
|
|
elif tagname == "COMM":
|
|
audio[tagname] = COMM(encoding=3,text=textvalue)
|
|
elif tagname == "TCOM":
|
|
audio[tagname] = TCOM(encoding=3,text=textvalue)
|
|
elif tagname == "TCON":
|
|
audio[tagname] = TCON(encoding=3,text=textvalue)
|
|
elif tagname == "TRCK":
|
|
audio[tagname] = TRCK(encoding=3,text=textvalue)
|
|
elif tagname == "TDRC":
|
|
try:
|
|
audio[tagname] = TDRC(encoding=3,text=textvalue)
|
|
except:
|
|
pass
|
|
elif tagname == "TDRL":
|
|
try:
|
|
audio[tagname] = TDRL(encoding=3,text=textvalue)
|
|
except:
|
|
pass
|
|
elif tagname == "TPUB":
|
|
audio[tagname] = TPUB(encoding=3,text=textvalue)
|
|
|
|
def check_tag(audio, filename: str, ID3_tag: str, normal_tag) -> bool:
|
|
res = False
|
|
|
|
# check if the ID3 tag exists
|
|
if (ID3_tag in audio.keys() and len(str(audio[ID3_tag])) != 0):
|
|
logging.info(ID3_tag + " ID3 tag found! " + str(audio[ID3_tag]))
|
|
|
|
# apply it to the general album tag
|
|
if audio[ID3_tag] is not str and filename.endswith(".mp3"):
|
|
audio[normal_tag] = audio[ID3_tag]
|
|
else:
|
|
audio[normal_tag] = audio[ID3_tag]
|
|
logging.info("Set " + normal_tag + " to " + str(audio[normal_tag]))
|
|
res = True
|
|
|
|
# check if general tag exists
|
|
elif (normal_tag in audio.keys() and len(str(audio[normal_tag])) != 0):
|
|
logging.info(normal_tag + " normal tag found! " + str(audio[normal_tag]))
|
|
if audio[normal_tag] is not str:
|
|
audio[normal_tag] = audio[normal_tag][0]
|
|
logging.info("normal tag is not str, set it to " + str(audio[normal_tag][0]))
|
|
if (not filename.endswith(".flac")):
|
|
#apply it to the ID3 tag
|
|
if audio[normal_tag] is not str:
|
|
create_ID3_tag(audio, ID3_tag,audio[normal_tag][0])
|
|
else:
|
|
create_ID3_tag(audio, ID3_tag,audio[normal_tag])
|
|
else:
|
|
logging.debug(filename + " is a flac file, not creating ID3 tag")
|
|
res = True
|
|
|
|
return res
|
|
|
|
def check_title_songname(x: str, audio):
|
|
logging.info("checking title by name " + x)
|
|
extension = x.split(".")[-1].lower()
|
|
if (extension in ["mp3","flac","ogg","wav","m4a","mp4"]):
|
|
x = x.rsplit(".",1)[0] # remove the file extension
|
|
logging.info("file has extension " + extension + ". removing it from title. New title: " + x)
|
|
|
|
if (" - " in x):
|
|
items = x.split(" - ")
|
|
# If the format is 'artist - Topic - title', remove 'Topic'
|
|
if len(items) > 2 and items[1].strip().lower() == "topic":
|
|
logging.info("Detected ' - Topic - ' in name, removing 'Topic'.")
|
|
# Rebuild items without 'Topic'
|
|
items = [items[0]] + items[2:]
|
|
# Set artist and title tags robustly
|
|
if len(items) == 2:
|
|
artist, title = items[0].strip(), items[1].strip()
|
|
elif len(items) > 2:
|
|
artist, title = items[0].strip(), items[1].strip()
|
|
else:
|
|
artist, title = x.strip(), x.strip()
|
|
# Set both TPE1 (song artist) and TPE2 (album artist)
|
|
audio["TPE1"] = TPE1(encoding=3, text=artist)
|
|
audio["TPE2"] = TPE2(encoding=3, text=artist)
|
|
# Only set 'artist' as a string for FLAC, not for MP3
|
|
if hasattr(audio, 'mime') and audio.mime and 'flac' in audio.mime[0].lower():
|
|
audio["artist"] = artist
|
|
# Set title tags
|
|
audio["TIT2"] = TIT2(encoding=3, text=title)
|
|
if hasattr(audio, 'mime') and audio.mime and 'flac' in audio.mime[0].lower():
|
|
audio["title"] = title
|
|
logging.info(f"Set artist: {artist}, title: {title}")
|
|
else:
|
|
logging.info("no - found in title, setting full name as title: " + x)
|
|
if ("TIT2" not in audio.keys()):
|
|
song_title = x.strip().rstrip()
|
|
logging.info("TIT2 tag not found, creating it. Using song title: " + song_title)
|
|
audio["TIT2"] = TIT2(encoding=3,text=song_title)
|
|
audio["title"] = TIT2(encoding=3,text=song_title)
|
|
|
|
def check_for_multiple_artists(audio, filename: str, name: str):
|
|
logging.info("checking for multiple artists for name " + name)
|
|
artists = []
|
|
if (" x " in name):
|
|
artists = name.split(" x ")
|
|
elif (" X " in name):
|
|
artists = name.split(" X ")
|
|
elif ("," in name):
|
|
artists = name.split(",")
|
|
elif ("/" in name):
|
|
artists = name.split("/")
|
|
elif ("\x00" in name):
|
|
artists = name.split("\x00")
|
|
|
|
if (len(artists) > 0):
|
|
logging.info("multiple artists: " + str(artists))
|
|
if filename.endswith(".flac"):
|
|
audio["artist"] = TPE2(encoding=3,text=["\0".join(artists)])
|
|
else:
|
|
audio["TPE1"] = TPE2(encoding=3,text=["\0".join(artists)])
|
|
else:
|
|
logging.info("no multiple artists found in name " + name + ", setting artist to " + name)
|
|
if filename.endswith(".flac"):
|
|
audio["artist"] = TPE2(encoding=3,text=name)
|
|
else:
|
|
audio["TPE1"] = TPE2(encoding=3,text=name)
|
|
|
|
|
|
# checks for any artist from the song name. If it exists it sets the properties of the file
|
|
def check_artist_songname(x: str, audio):
|
|
items = x.split(" - ")
|
|
logging.info("Checking artist by name. items: " + str(items))
|
|
# Remove 'Topic' if present
|
|
if len(items) > 2 and items[1].strip().lower() == "topic":
|
|
logging.info("Detected ' - Topic - ' in name, removing 'Topic'.")
|
|
items = [items[0]] + items[2:]
|
|
artist = items[0].strip()
|
|
# Set both TPE1 (song artist) and TPE2 (album artist)
|
|
audio["TPE1"] = TPE1(encoding=3, text=artist)
|
|
audio["TPE2"] = TPE2(encoding=3, text=artist)
|
|
# Only set 'artist' as a string for FLAC, not for MP3
|
|
if hasattr(audio, 'mime') and audio.mime and 'flac' in audio.mime[0].lower():
|
|
audio["artist"] = artist
|
|
logging.info(f"Set artist tags TPE1 and TPE2 to {artist}")
|
|
|
|
def check_artist(audio, filename: str) -> bool:
|
|
res = False
|
|
|
|
# check if the ID3 artist tag exists
|
|
check_tag(audio, filename, "TPE1","artist")
|
|
check_tag(audio, filename, "TPE2","artist")
|
|
|
|
if ("TPE1" in audio.keys()):
|
|
if (len(str(audio["TPE1"])) != 0):
|
|
logging.info("TPE1 tag was found! " + str(audio["TPE1"]))
|
|
|
|
# apply it to general artist tag
|
|
audio["TPE2"] = TPE2(encoding=3,text=str(audio["TPE1"]))
|
|
audio["artist"] = audio["TPE1"]
|
|
logging.info("checking for multiple artists")
|
|
check_for_multiple_artists(audio, filename ,str(audio["TPE1"]))
|
|
res = True
|
|
|
|
|
|
# if no TPE1, check if the ID3 band tag exists
|
|
elif ("TPE2" in audio.keys()):
|
|
if (len(str(audio["TPE2"])) != 0):
|
|
logging.info("TPE2 tag was found! " + str(audio["TPE2"]))
|
|
|
|
# apply it to TPE1 and general artist tags
|
|
audio["TPE1"] = TPE1(encoding=3,text=str(audio["TPE2"]))
|
|
audio["artist"] = audio["TPE1"]
|
|
check_for_multiple_artists(audio,filename,str(audio["TPE2"]))
|
|
res = True
|
|
|
|
# check if artist audio tag exists
|
|
elif ("artist" in audio.keys()):
|
|
if (len(str(audio["artist"])) != 0):
|
|
logging.info("artist tag was found! " + str(audio["artist"]))
|
|
|
|
artist = ""
|
|
if (audio["artist"] is not str):
|
|
artist = audio["artist"][0]
|
|
else:
|
|
artist = audio["artist"]
|
|
|
|
logging.info("artist: " + artist)
|
|
# apply to both ID3 artist tags
|
|
audio["TPE1"] = TPE1(encoding=3,text=artist)
|
|
audio["TPE2"] = TPE2(encoding=3,text=artist)
|
|
|
|
if (audio["TPE2"][0].text is not str):
|
|
audio["TPE2"][0].text = str(audio["TPE2"][0].text[0])
|
|
|
|
if (audio["TPE1"] is not str):
|
|
audio["TPE1"] = audio["TPE1"][0]
|
|
|
|
logging.info("Set TPE1 and TPE2 to " + str(audio["TPE1"][0]) + " and " + str(audio["TPE2"][0]))
|
|
check_for_multiple_artists(audio,filename,artist)
|
|
res = True
|
|
|
|
return res
|
|
|
|
def set_genre_tag(genres, audio):
|
|
"""Apply genre tags to audio file from Spotify genres list."""
|
|
genre = ""
|
|
if (len(genres) > 0):
|
|
if (len(genres) == 1):
|
|
audio["TCON"] = TCON(encoding=3,text=str(genres[0]))
|
|
else:
|
|
for i in range(len(genres)):
|
|
if (i == 0):
|
|
genre = str(genres[i])
|
|
else:
|
|
genre += "," + str(genres[i])
|
|
|
|
logging.info("genre set to " + genre)
|
|
audio["TCON"] = TCON(encoding=3,text=genre)
|
|
|
|
audio["genre"] = audio["TCON"]
|
|
|
|
def embed_music_file(audiostr: str, coverfile: str):
|
|
try:
|
|
new_audio = ID3(audiostr)
|
|
with open(coverfile,'rb') as albumart:
|
|
new_audio.add(APIC(
|
|
encoding=3,
|
|
mime='image/jpeg',
|
|
type=3, desc=u'Cover image',
|
|
data=albumart.read()
|
|
))
|
|
new_audio.save(audiostr)
|
|
logging.info("Finished!")
|
|
except:
|
|
logging.info("could not embed music file")
|
|
|
|
|
|
def save_album_from_spotify(spotify, audio, x: str, spotify_data: dict) -> bool:
|
|
"""
|
|
Save audio file with metadata and cover art from Spotify album data.
|
|
|
|
Args:
|
|
spotify: Spotify client instance
|
|
audio: Audio file object
|
|
x: Filename
|
|
spotify_data: Dict with album data from spotify_search.search_album()
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
if not spotify_data or not spotify_data.get('found'):
|
|
logging.info("No Spotify album data provided")
|
|
return False
|
|
|
|
logging.info("Applying Spotify album data to file...")
|
|
|
|
# Set artist
|
|
album_artist = spotify_data['artist']
|
|
if x.endswith(".flac"):
|
|
try:
|
|
if str(audio.get("album_artist", "")) != album_artist:
|
|
audio["album_artist"] = album_artist
|
|
except:
|
|
audio["album_artist"] = album_artist
|
|
else:
|
|
if str(audio.get("TPE2", "")) != album_artist:
|
|
audio["TPE2"] = TPE2(encoding=3, text=album_artist)
|
|
|
|
# Set album
|
|
album_name = spotify_data['album']
|
|
if x.endswith(".flac"):
|
|
if str(audio.get("album", "")) != album_name:
|
|
audio["album"] = album_name
|
|
elif str(audio.get("TALB", "")) != album_name:
|
|
audio["TALB"] = TALB(encoding=3, text=album_name)
|
|
|
|
# Parse and set release date
|
|
release_date = spotify_data['release_date']
|
|
try:
|
|
year = str(datetime.strptime(release_date, '%Y-%m-%d').year)
|
|
except:
|
|
try:
|
|
year = str(datetime.strptime(release_date, '%Y-%m').year)
|
|
except:
|
|
try:
|
|
year = str(datetime.strptime(release_date, '%Y').year)
|
|
except:
|
|
year = str(release_date)
|
|
|
|
if x.endswith(".flac"):
|
|
audio["year"] = year
|
|
audio["date"] = release_date
|
|
else:
|
|
audio["TDRC"] = TDRC(encoding=3, text=year)
|
|
audio["TDRL"] = TDRL(encoding=3, text=release_date)
|
|
|
|
# Set genres
|
|
logging.info("genres: " + str(spotify_data['genres']))
|
|
set_genre_tag(spotify_data['genres'], audio)
|
|
|
|
# Set comment
|
|
comment = "Spotify ID: {0}. Release date precision: {1}, total tracks in album: {2}. This album has {3} version(s)".format(
|
|
spotify_data['album_id'],
|
|
spotify_data['release_date_precision'],
|
|
spotify_data['total_tracks'],
|
|
spotify_data['versions_count']
|
|
)
|
|
logging.info("Comment: " + comment)
|
|
if x.endswith(".flac"):
|
|
audio["comment"] = audio.get("comment", "") + comment
|
|
else:
|
|
audio["COMM"] = COMM(encoding=3, text=comment + str(audio.get("COMM", "")))
|
|
|
|
# Save tags
|
|
if x.endswith(".flac"):
|
|
remove_flac_ID3_tags(audio, x)
|
|
audio.save(x)
|
|
|
|
# Create folder structure
|
|
if x.endswith(".flac"):
|
|
artist_path = str(audio["artist"][0])
|
|
album_path = str(audio["album"][0])
|
|
else:
|
|
if "/" in str(audio["TPE2"]):
|
|
audio["TPE2"] = str(audio["TPE2"]).replace("/", "")
|
|
artist_path = str(audio["TPE2"])
|
|
album_path = str(audio["TALB"])
|
|
|
|
songpath = join(".", artist_path, album_path)
|
|
make_folder(join(".", artist_path))
|
|
|
|
# Handle albums with / in the name
|
|
if not x.endswith(".flac") and "/" in album_path:
|
|
logging.info("album contains /")
|
|
folders = album_path.split('/')
|
|
logging.info(folders)
|
|
pos = join(".", artist_path)
|
|
for fold in folders:
|
|
make_folder(join(pos, fold))
|
|
pos = join(pos, fold)
|
|
logging.info(pos)
|
|
|
|
make_folder(songpath)
|
|
os.replace(join(".", x), join(songpath, x))
|
|
logging.info("moved song file, now downloading cover art")
|
|
|
|
# Download and save cover art
|
|
if spotify_data['image_url']:
|
|
img_data = requests.get(spotify_data['image_url']).content
|
|
with open(join(songpath, "Cover.jpg"), 'wb') as handler:
|
|
handler.write(img_data)
|
|
logging.info("done getting cover art!")
|
|
|
|
logging.info("now setting cover art..")
|
|
embed_music_file(join(songpath, x), join(songpath, "Cover.jpg"))
|
|
|
|
return True
|
|
|
|
def save_track_from_spotify(spotify, audio, x: str, spotify_data: dict) -> bool:
|
|
"""
|
|
Save audio file with metadata and cover art from Spotify track data.
|
|
|
|
Args:
|
|
spotify: Spotify client instance
|
|
audio: Audio file object
|
|
x: Filename
|
|
spotify_data: Dict with track data from spotify_search.search_track()
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
if not spotify_data or not spotify_data.get('found'):
|
|
logging.info("No Spotify track data provided")
|
|
return False
|
|
|
|
logging.info("Applying Spotify track data to file...")
|
|
|
|
# Get current artist value for comparison
|
|
if x.endswith(".flac"):
|
|
current_artist = str(audio.get("artist", [""])[0]) if not isinstance(audio.get("artist", ""), str) else str(audio.get("artist", ""))
|
|
else:
|
|
if "artist" in audio:
|
|
current_artist = str(audio["artist"][0]) if not isinstance(audio["artist"], str) else str(audio["artist"])
|
|
elif "TPE2" in audio:
|
|
current_artist = str(audio["TPE2"][0]) if not isinstance(audio["TPE2"], str) else str(audio["TPE2"])
|
|
else:
|
|
current_artist = "Unknown Artist"
|
|
|
|
# Set artist if different
|
|
found_artist = spotify_data['artist']
|
|
if found_artist != current_artist:
|
|
logging.info("Changing album artist from " + current_artist + " to " + found_artist)
|
|
if x.endswith(".flac"):
|
|
audio["album_artist"] = found_artist
|
|
else:
|
|
audio["TPE2"] = TPE2(encoding=3, text=found_artist)
|
|
|
|
# Set album
|
|
found_album = spotify_data['album']
|
|
logging.info("found album name: " + found_album)
|
|
if len(found_album) > 0:
|
|
if x.endswith(".flac"):
|
|
audio["album"] = found_album
|
|
else:
|
|
audio["TALB"] = TALB(encoding=3, text=found_album)
|
|
else:
|
|
# set album to title if no album found
|
|
if x.endswith(".flac"):
|
|
audio["album"] = audio["title"][0]
|
|
else:
|
|
audio["TALB"] = TALB(encoding=3, text=str(audio["TIT2"]))
|
|
|
|
# Add system info to comment
|
|
now = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
cpu_percent = psutil.cpu_percent(interval=1)
|
|
ram_percent = psutil.virtual_memory().percent
|
|
sysinfo = f"This album was downloaded on {now}. The server was using {cpu_percent}% CPU and {ram_percent}% RAM."
|
|
|
|
# Build comment from album metadata
|
|
album_label = spotify_data.get('label', '')
|
|
album_desc = f"Label: {album_label}. " if album_label else ""
|
|
|
|
comment = "Spotify ID: {0}. This album was released on: {1}, total tracks in album: {2}. This album has {3} version(s). {4} {5}".format(
|
|
spotify_data['album_id'],
|
|
spotify_data['release_date'],
|
|
spotify_data['total_tracks'],
|
|
spotify_data['versions_count'],
|
|
album_desc,
|
|
sysinfo
|
|
)
|
|
logging.info("Comment: " + comment)
|
|
if x.endswith(".flac"):
|
|
audio["comment"] = comment
|
|
else:
|
|
audio["COMM"] = COMM(encoding=3, text=comment)
|
|
|
|
# Parse and set release date
|
|
release_date = spotify_data['release_date']
|
|
try:
|
|
year = str(datetime.strptime(release_date, '%Y-%m-%d').year)
|
|
except:
|
|
try:
|
|
year = str(datetime.strptime(release_date, '%Y-%m').year)
|
|
except:
|
|
try:
|
|
year = str(datetime.strptime(release_date, '%Y').year)
|
|
except Exception as err:
|
|
logging.info(err)
|
|
year = str(release_date)
|
|
|
|
if x.endswith(".flac"):
|
|
audio["year"] = year
|
|
audio["date"] = release_date
|
|
else:
|
|
audio["TDRC"] = TDRC(encoding=3, text=year)
|
|
audio["TDRL"] = TDRL(encoding=3, text=release_date)
|
|
|
|
# Set track number
|
|
if x.endswith(".flac"):
|
|
audio["TRACKNUMBER"] = str(spotify_data['track_number']) + "/" + str(spotify_data['total_tracks'])
|
|
else:
|
|
audio["TRCK"] = TRCK(encoding=3, text=str(spotify_data['track_number']) + "/" + str(spotify_data['total_tracks']))
|
|
|
|
# Set popularity
|
|
if x.endswith(".flac"):
|
|
audio["popularity"] = str(spotify_data['popularity'])
|
|
else:
|
|
audio["POPM"] = POPM(encoding=3, text=str(spotify_data['popularity']))
|
|
|
|
# Set genres
|
|
logging.info("genres: " + str(spotify_data['genres']))
|
|
set_genre_tag(spotify_data['genres'], audio)
|
|
|
|
# Save tags
|
|
remove_flac_ID3_tags(audio, x)
|
|
audio.save(x)
|
|
|
|
# Create folder structure
|
|
if x.endswith(".flac"):
|
|
artist_path = str(audio["artist"][0])
|
|
else:
|
|
if audio["TPE2"] is not str:
|
|
artist_path = str(audio["TPE2"][0])
|
|
else:
|
|
artist_path = str(audio["TPE2"])
|
|
|
|
logging.info("artist path: " + artist_path)
|
|
if x.endswith(".flac"):
|
|
songpath = join(".", artist_path, str(audio["ALBUM"][0]))
|
|
else:
|
|
songpath = join(".", artist_path, str(audio["TALB"]))
|
|
logging.info("song path: " + songpath)
|
|
|
|
make_folder(join(".", artist_path))
|
|
|
|
# Handle albums with / in the name
|
|
if not x.endswith(".flac") and "/" in str(audio["TALB"]):
|
|
logging.info("album contains /")
|
|
folders = str(audio["TALB"]).split('/')
|
|
logging.info(folders)
|
|
pos = join(".", str(audio["TPE2"]))
|
|
for fold in folders:
|
|
make_folder(join(pos, fold))
|
|
pos = join(pos, fold)
|
|
logging.info(pos)
|
|
|
|
make_folder(songpath)
|
|
os.replace(join(".", x), join(songpath, x))
|
|
logging.info("moved song file, now downloading cover art")
|
|
|
|
# Download and save cover art
|
|
if spotify_data['image_url']:
|
|
img_data = requests.get(spotify_data['image_url']).content
|
|
with open(join(songpath, "Cover.jpg"), 'wb') as handler:
|
|
handler.write(img_data)
|
|
logging.info("done getting cover art!")
|
|
|
|
logging.info("now setting cover art..")
|
|
embed_music_file(join(songpath, x), join(songpath, "Cover.jpg"))
|
|
|
|
return True
|
|
|
|
def main():
|
|
# Preprocess: rename files with '- Topic -' in the name to 'artist - title'
|
|
for fname in [f for f in listdir(".") if isfile(join(".",f)) and "- Topic -" in f]:
|
|
parts = fname.rsplit("- Topic -", 1)
|
|
if len(parts) == 2:
|
|
artist = parts[0].strip().rstrip("- ")
|
|
title = parts[1].rsplit('.', 1)[0].strip()
|
|
ext = fname.rsplit('.', 1)[-1]
|
|
new_name = f"{artist} - {title}.{ext}"
|
|
if not os.path.exists(new_name):
|
|
logging.info(f"Renaming file '{fname}' to '{new_name}'")
|
|
os.rename(fname, new_name)
|
|
else:
|
|
logging.warning(f"Target filename '{new_name}' already exists. Skipping rename for '{fname}'")
|
|
|
|
# for spotipy to be able to log in, the environment variables SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET have to be set
|
|
# these can be obtained from the spotify developer dashboard
|
|
# they are defined in /etc/profile.d/spotipy.sh
|
|
spotify = spotify_search.init_spotify_client()
|
|
|
|
onlyfiles = [f for f in listdir(".") if (isfile(join(".",f)) and f.split(".")[-1] in ['mp3','mp4','ogg','wav','flac','m4a','MP3','FLAC','OGG','MP4','WAV','M4A'])]
|
|
# TIT2 = title,
|
|
# TPE1 = artist,
|
|
# TPE2 = band,
|
|
# TALB = album,
|
|
# COMM = comment,
|
|
# TCOM = composer,
|
|
# TCON = genre,
|
|
# TRCK = number,
|
|
# TDRC = year,
|
|
# TPUB = publisher
|
|
|
|
# use: audio["TRCK"] = TRCK(encoding=3, text=u'track_number') and replace the tags with appropriate values
|
|
for x in onlyfiles:
|
|
logging.info("------------------------------------------------")
|
|
logging.info(x)
|
|
|
|
# try to open tags, if the file has none, create a new ID3 object
|
|
try:
|
|
audio = mutagen.File(x)
|
|
except mutagen.mp3.HeaderNotFoundError as err:
|
|
logging.info(err)
|
|
logging.info("header not found")
|
|
audio = mutagen.File(x,easy=True)
|
|
audio.add_tags()
|
|
except mutagen.id3.ID3NoHeaderError:
|
|
logging.info("no header")
|
|
audio = mutagen.File(x,easy=True)
|
|
audio.add_tags()
|
|
except:
|
|
logging.info("opening as ID3")
|
|
audio = ID3(x)
|
|
audio.add_tags()
|
|
|
|
logging.info(type(audio))
|
|
try:
|
|
if (audio.tags == None):
|
|
logging.info("audio has no tags")
|
|
audio.add_tags()
|
|
except:
|
|
pass
|
|
|
|
has_valid_artist = check_artist(audio,x)
|
|
has_valid_album = check_tag(audio,x,"TALB","album")
|
|
has_valid_title = check_tag(audio,x,"TIT2","title")
|
|
if (has_valid_title):
|
|
if x.endswith(".flac"):
|
|
logging.info("Found valid title in title tag: " + str(audio["TITLE"]))
|
|
if audio["TITLE"] is not str:
|
|
check_title_songname(audio["TITLE"][0],audio)
|
|
else:
|
|
check_title_songname(audio["TITLE"],audio)
|
|
else:
|
|
logging.info("Found valid title in TTI2 tag: " + str(audio["TIT2"]) + ". type: " + str(type(audio["TIT2"])))
|
|
if not isinstance(audio["TIT2"], str):
|
|
if (isinstance(audio["TIT2"][0], str)):
|
|
check_title_songname(audio["TIT2"][0],audio)
|
|
else:
|
|
logging.info(type(audio["TIT2"][0]))
|
|
check_title_songname(audio["TIT2"][0].text[0],audio)
|
|
else:
|
|
check_title_songname(audio["TIT2"].text[0],audio)
|
|
else:
|
|
logging.info("No valid title found in TTI2 tag, using name " + x)
|
|
check_title_songname(x,audio)
|
|
has_valid_title = True
|
|
|
|
if (has_valid_artist == False):
|
|
logging.info("No valid artist found, checking for artist by songname of the file (" + x + ")")
|
|
if (" - " in x):
|
|
check_artist_songname(x, audio)
|
|
has_valid_artist = True
|
|
|
|
if (has_valid_artist == False and has_valid_title):
|
|
logging.info("No valid artist found but valid title found, checking for multiple artists")
|
|
check_for_multiple_artists(audio,x,str(audio["TIT2"]))
|
|
has_valid_artist = check_artist(audio, x) # check again
|
|
|
|
check_tag(audio,x,"COMM","comment")
|
|
check_tag(audio,x,"TCOM","composer")
|
|
|
|
has_genre = check_tag(audio,x,"TCON","genre")
|
|
if (has_genre):
|
|
if x.endswith(".flac"):
|
|
audio["genre"] = str(audio["genre"]).replace(" & ",",")
|
|
else:
|
|
audio["TCON"] = TCON(encoding=3, text=str(audio["TCON"]).replace(" & ",",")) # convert genres like Hip-Hop & Rap to Hip-Hop,Rap
|
|
|
|
check_tag(audio,x,"TRCK","track")
|
|
check_tag(audio,x,"TDRC","year")
|
|
check_tag(audio,x,"TPUB","publisher")
|
|
|
|
if (has_valid_artist and has_valid_title):
|
|
found = False
|
|
|
|
# Extract artist and title for search
|
|
artist = ""
|
|
track = ""
|
|
if x.endswith(".flac"):
|
|
if audio["artist"] is not str:
|
|
artist = str(audio["artist"][0])
|
|
else:
|
|
artist = str(audio["artist"])
|
|
if audio["title"] is not str:
|
|
track = str(audio["title"][0])
|
|
else:
|
|
track = str(audio["title"])
|
|
else:
|
|
# Prefer 'artist' and 'title' tags if available, fallback to TPE2/TIT2
|
|
if "artist" in audio:
|
|
if audio["artist"] is not str:
|
|
artist = str(audio["artist"][0])
|
|
else:
|
|
artist = str(audio["artist"])
|
|
elif "TPE2" in audio:
|
|
if audio["TPE2"] is not str:
|
|
artist = str(audio["TPE2"][0])
|
|
else:
|
|
artist = str(audio["TPE2"])
|
|
else:
|
|
artist = "Unknown Artist"
|
|
|
|
if "title" in audio:
|
|
if audio["title"] is not str:
|
|
track = str(audio["title"][0])
|
|
else:
|
|
track = str(audio["title"])
|
|
elif "TIT2" in audio:
|
|
if audio["TIT2"] is not str:
|
|
track = str(audio["TIT2"][0])
|
|
else:
|
|
track = str(audio["TIT2"])
|
|
else:
|
|
track = "Unknown Title"
|
|
|
|
# Search Spotify for the track
|
|
try:
|
|
spotify_data = spotify_search.search_track(spotify, artist, track)
|
|
if spotify_data:
|
|
found = save_track_from_spotify(spotify, audio, x, spotify_data)
|
|
except Exception as err:
|
|
logging.error("could not find track on spotify: " + str(err))
|
|
logging.error(err.with_traceback)
|
|
found = False
|
|
|
|
if (found == False):
|
|
if (x.endswith(".flac")):
|
|
logging.info("valid artist found. making folder for artist " + str(audio["artist"][0]))
|
|
make_folder(join(".",str(audio["artist"][0])))
|
|
else:
|
|
# Use TPE2 if available, otherwise fallback to TPE1
|
|
artist_folder = str(audio.get("TPE2", audio.get("TPE1", "Unknown Artist")))
|
|
logging.info("valid artist found. making folder for artist " + artist_folder)
|
|
if "/" in artist_folder:
|
|
artist_folder = artist_folder.replace("/","")
|
|
make_folder(join(".", artist_folder))
|
|
|
|
if (has_valid_album):
|
|
if (x.endswith(".flac")):
|
|
make_folder(join(".",str(audio["artist"][0]),str(audio["album"][0])))
|
|
else:
|
|
make_folder(join(".",str(audio["TPE2"]),str(audio["TALB"])))
|
|
|
|
logging.info("spotify did not find artist and track, searching for album...")
|
|
if (has_valid_album):
|
|
# Extract artist and album for search
|
|
search_artist = ""
|
|
search_album = ""
|
|
if x.endswith(".flac"):
|
|
search_artist = str(audio["artist"])
|
|
search_album = str(audio["album"])
|
|
else:
|
|
search_artist = str(audio["TPE2"])
|
|
search_album = str(audio["TALB"])
|
|
|
|
# Search Spotify for the album
|
|
album_data = spotify_search.search_album(spotify, search_artist, search_album)
|
|
if album_data:
|
|
album_found = save_album_from_spotify(spotify, audio, x, album_data)
|
|
else:
|
|
album_found = False
|
|
|
|
if (album_found == False):
|
|
logging.info("Nothing found on spotify, searching Google Images...")
|
|
search_google_images_and_save(x, audio)
|
|
else:
|
|
# Only set album to title if TALB is not already set (i.e., not found from Spotify)
|
|
if x.endswith(".flac"):
|
|
if not audio.get("album"):
|
|
audio["album"] = audio["title"][0]
|
|
else:
|
|
if not audio.get("TALB") or (isinstance(audio["TALB"], TALB) and not audio["TALB"].text):
|
|
audio["TALB"] = TALB(encoding=3,text=str(audio["TIT2"]))
|
|
search_google_images_and_save(x, audio)
|
|
|
|
logging.info("------------------------------------------------")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|