forked from CopyBot/sptnr
Switch lock file to atomic, to prevent corruption, More ratelimit delay to run on album instead of song.
Don't remove song variations form parentheses (remix, instrumental, etc)
This commit is contained in:
@@ -11,6 +11,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import random
|
import random
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import requests
|
import requests
|
||||||
@@ -107,7 +108,6 @@ def load_lock():
|
|||||||
return json.load(f)
|
return json.load(f)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logging.error(f"{LIGHT_RED}Lock file '{LOCK_FILE}' is corrupt or not valid JSON. Starting with an empty lock.{RESET}")
|
logging.error(f"{LIGHT_RED}Lock file '{LOCK_FILE}' is corrupt or not valid JSON. Starting with an empty lock.{RESET}")
|
||||||
# Optionally, back up the corrupt file
|
|
||||||
os.rename(LOCK_FILE, LOCK_FILE + ".corrupt")
|
os.rename(LOCK_FILE, LOCK_FILE + ".corrupt")
|
||||||
return {}
|
return {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -115,8 +115,12 @@ def load_lock():
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def save_lock(lock):
|
def save_lock(lock):
|
||||||
with open(LOCK_FILE, "w") as f:
|
# Write to a temp file first, then atomically replace the lock file
|
||||||
json.dump(LOCK, f)
|
dir_name = os.path.dirname(LOCK_FILE) or "."
|
||||||
|
with tempfile.NamedTemporaryFile("w", dir=dir_name, delete=False) as tf:
|
||||||
|
json.dump(lock, tf)
|
||||||
|
tempname = tf.name
|
||||||
|
os.replace(tempname, LOCK_FILE)
|
||||||
|
|
||||||
def should_update(song_id):
|
def should_update(song_id):
|
||||||
lock_expiry = get_lock_expiry()
|
lock_expiry = get_lock_expiry()
|
||||||
@@ -242,6 +246,8 @@ logging.info(f"{BOLD}Version:{RESET} {LIGHT_YELLOW}sptnr v{__version__}{RESET}")
|
|||||||
|
|
||||||
LOCK = load_lock()
|
LOCK = load_lock()
|
||||||
|
|
||||||
|
SHOULD_DELAY = False
|
||||||
|
|
||||||
if args.preview:
|
if args.preview:
|
||||||
logging.info(f"{LIGHT_YELLOW}Preview mode, no changes will be made.{RESET}")
|
logging.info(f"{LIGHT_YELLOW}Preview mode, no changes will be made.{RESET}")
|
||||||
PREVIEW = 1
|
PREVIEW = 1
|
||||||
@@ -305,14 +311,14 @@ def process_track(track_id, artist_name, album, track_name):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def search_spotify(query, max_retries=3):
|
def search_spotify(query, max_retries=3):
|
||||||
|
global SHOULD_DELAY
|
||||||
|
SHOULD_DELAY = True
|
||||||
|
|
||||||
SPOTIFY_TOKEN = spotify_token_manager.get_token()
|
SPOTIFY_TOKEN = spotify_token_manager.get_token()
|
||||||
|
|
||||||
spotify_url = f"https://api.spotify.com/v1/search?q={query}&type=track&limit=1"
|
spotify_url = f"https://api.spotify.com/v1/search?q={query}&type=track&limit=1"
|
||||||
headers = {"Authorization": f"Bearer {SPOTIFY_TOKEN}"}
|
headers = {"Authorization": f"Bearer {SPOTIFY_TOKEN}"}
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
response = requests.get(spotify_url, headers=headers, timeout=10)
|
response = requests.get(spotify_url, headers=headers, timeout=10)
|
||||||
@@ -348,7 +354,14 @@ def process_track(track_id, artist_name, album, track_name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def remove_parentheses_content(s):
|
def remove_parentheses_content(s):
|
||||||
return re.sub(r"\s*\(.*?\)\s*", " ", s).strip()
|
# Only remove parentheses if they do NOT contain important keywords
|
||||||
|
keywords = ["remix", "instrumental", "edit", "version", "mix", "karaoke", "live", "acoustic", "demo"]
|
||||||
|
def replacer(match):
|
||||||
|
content = match.group(1).lower()
|
||||||
|
if any(k in content for k in keywords):
|
||||||
|
return f"({match.group(1)})" # Keep it
|
||||||
|
return ""
|
||||||
|
return re.sub(r"\((.*?)\)", replacer, s).strip()
|
||||||
|
|
||||||
search_attempts = [
|
search_attempts = [
|
||||||
# Primary attempt with all info
|
# Primary attempt with all info
|
||||||
@@ -363,6 +376,7 @@ def process_track(track_id, artist_name, album, track_name):
|
|||||||
|
|
||||||
spotify_data = None
|
spotify_data = None
|
||||||
for attempt in search_attempts:
|
for attempt in search_attempts:
|
||||||
|
# logging.info(f"Searching Spotify for: {LIGHT_CYAN}{attempt()}{RESET}")
|
||||||
spotify_data = search_spotify(attempt())
|
spotify_data = search_spotify(attempt())
|
||||||
if spotify_data and spotify_data.get("tracks", {}).get("items"):
|
if spotify_data and spotify_data.get("tracks", {}).get("items"):
|
||||||
break
|
break
|
||||||
@@ -374,7 +388,10 @@ def process_track(track_id, artist_name, album, track_name):
|
|||||||
rating = get_rating_from_popularity(popularity)
|
rating = get_rating_from_popularity(popularity)
|
||||||
popularity_str = f"{popularity} " if 0 <= popularity <= 9 else str(popularity)
|
popularity_str = f"{popularity} " if 0 <= popularity <= 9 else str(popularity)
|
||||||
|
|
||||||
logging.info(f" p:{LIGHT_CYAN}{popularity_str}{RESET} → r:{LIGHT_BLUE}{rating}{RESET} | {LIGHT_GREEN}{track_name}{RESET}")
|
#log matched track name from spotify
|
||||||
|
sp_track_name = track["name"]
|
||||||
|
|
||||||
|
logging.info(f" p:{LIGHT_CYAN}{popularity_str}{RESET} → r:{LIGHT_BLUE}{rating}{RESET} | {LIGHT_GREEN}{track_name} - {sp_track_name}{RESET}")
|
||||||
|
|
||||||
if PREVIEW != 1:
|
if PREVIEW != 1:
|
||||||
try:
|
try:
|
||||||
@@ -389,12 +406,30 @@ def process_track(track_id, artist_name, album, track_name):
|
|||||||
logging.info(f" p:{LIGHT_RED}??{RESET} → r:{LIGHT_BLUE}0{RESET} | {LIGHT_RED}(not found) {track_name}{RESET}")
|
logging.info(f" p:{LIGHT_RED}??{RESET} → r:{LIGHT_BLUE}0{RESET} | {LIGHT_RED}(not found) {track_name}{RESET}")
|
||||||
UNMATCHED_TRACKS.append(f"{artist_name} - {album} - {track_name}")
|
UNMATCHED_TRACKS.append(f"{artist_name} - {album} - {track_name}")
|
||||||
NOT_FOUND += 1
|
NOT_FOUND += 1
|
||||||
LOCK[track_id] = time.time()
|
|
||||||
save_lock(LOCK)
|
# If not found, set rating to 0
|
||||||
|
if PREVIEW != 1:
|
||||||
|
try:
|
||||||
|
nav_url = f"{NAV_BASE_URL}/rest/setRating?u={NAV_USER}&p=enc:{HEX_ENCODED_PASS}&v=1.12.0&c=myapp&id={track_id}&rating=0"
|
||||||
|
requests.get(nav_url, timeout=5)
|
||||||
|
LOCK[track_id] = time.time()
|
||||||
|
save_lock(LOCK)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Failed to update rating in Navidrome: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TOTAL_TRACKS += 1
|
TOTAL_TRACKS += 1
|
||||||
|
|
||||||
def process_album(album_id):
|
def process_album(album_id):
|
||||||
|
|
||||||
|
global SHOULD_DELAY
|
||||||
|
|
||||||
|
if SHOULD_DELAY:
|
||||||
|
# sleep for a short time to avoid hitting rate limits too quickly
|
||||||
|
time.sleep(4)
|
||||||
|
|
||||||
|
SHOULD_DELAY = False
|
||||||
nav_url = f"{NAV_BASE_URL}/rest/getAlbum?id={album_id}&u={NAV_USER}&p=enc:{HEX_ENCODED_PASS}&v=1.12.0&c=spotify_sync&f=json"
|
nav_url = f"{NAV_BASE_URL}/rest/getAlbum?id={album_id}&u={NAV_USER}&p=enc:{HEX_ENCODED_PASS}&v=1.12.0&c=spotify_sync&f=json"
|
||||||
response = requests.get(nav_url).json()
|
response = requests.get(nav_url).json()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user