6 Commits

Author SHA1 Message Date
Kevin Restaino ffc501a48d feat: arm64 and amd64 builds 2024-01-09 13:59:10 -05:00
Kevin Restaino e20f9155cb chore: create LICENSE 2024-01-08 18:15:10 -05:00
Kevin Restaino 816444e309 chore: bump version 2024-01-08 17:33:30 -05:00
Kevin Restaino fce99b39cb feat: third search, replace "part" with "pt." 2024-01-08 17:30:03 -05:00
Kevin Restaino f3fe277017 feat: log version 2024-01-08 16:47:53 -05:00
Kevin Restaino 1396d6d3ed feat: error handling 2024-01-08 16:23:49 -05:00
4 changed files with 160 additions and 43 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Kevin Restaino
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -1 +1 @@
1.0.0
1.3.0
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Ensure the script stops if there is an error
set -e
# Read version from the VERSION file
VERSION=$(cat VERSION)
# Set up the builder instance (only needs to be done once, so you can comment this out after the first run)
# docker buildx create --name mybuilder --use
# docker buildx inspect mybuilder --bootstrap
# Build and push the Docker image for both arm64 and amd64 platforms with the version tag
docker buildx build --platform linux/arm64,linux/amd64 -t krestaino/sptnr:$VERSION . --push
# Build and push the 'latest' tag as well
docker buildx build --platform linux/arm64,linux/amd64 -t krestaino/sptnr:latest . --push
echo "Docker images tagged and pushed: $VERSION and latest"
+119 -42
View File
@@ -1,4 +1,4 @@
with open('VERSION', 'r') as file:
with open("VERSION", "r") as file:
__version__ = file.read().strip()
import argparse
@@ -30,28 +30,7 @@ NAV_PASS = os.getenv("NAV_PASS")
SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
# Setup logs
LOG_DIR = "logs"
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
LOGFILE = os.path.join(LOG_DIR, f"spotify-popularity_{int(time.time())}.log")
# Auth
HEX_ENCODED_PASS = NAV_PASS.encode().hex()
TOKEN_AUTH = base64.b64encode(
f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}".encode()
).decode()
TOKEN_URL = "https://accounts.spotify.com/api/token"
response = requests.post(
TOKEN_URL,
headers={"Authorization": f"Basic {TOKEN_AUTH}"},
data={"grant_type": "client_credentials"},
)
SPOTIFY_TOKEN = json.loads(response.text)["access_token"]
init(autoreset=True)
# Colors
LIGHT_PURPLE = Fore.MAGENTA + Style.BRIGHT
LIGHT_GREEN = Fore.GREEN + Style.BRIGHT
LIGHT_RED = Fore.RED + Style.BRIGHT
@@ -61,22 +40,14 @@ LIGHT_YELLOW = Fore.YELLOW + Style.BRIGHT
BOLD = Style.BRIGHT
RESET = Style.RESET_ALL
# Default flags
PREVIEW = 0
START = 0
LIMIT = 0
ARTIST_IDs = []
ALBUM_IDs = []
# Setup logs
LOG_DIR = "logs"
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
# Variables
ARTISTS_PROCESSED = 0
TOTAL_TRACKS = 0
FOUND_AND_UPDATED = 0
NOT_FOUND = 0
UNMATCHED_TRACKS = []
LOGFILE = os.path.join(LOG_DIR, f"spotify-popularity_{int(time.time())}.log")
# Setup logging
class NoColorFormatter(logging.Formatter):
ansi_escape = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
@@ -95,6 +66,44 @@ file_handler = logging.FileHandler(LOGFILE, "a")
file_handler.setFormatter(NoColorFormatter("[%(asctime)s] %(message)s"))
logging.getLogger().addHandler(file_handler)
# Auth
HEX_ENCODED_PASS = NAV_PASS.encode().hex()
TOKEN_AUTH = base64.b64encode(
f"{SPOTIFY_CLIENT_ID}:{SPOTIFY_CLIENT_SECRET}".encode()
).decode()
TOKEN_URL = "https://accounts.spotify.com/api/token"
response = requests.post(
TOKEN_URL,
headers={"Authorization": f"Basic {TOKEN_AUTH}"},
data={"grant_type": "client_credentials"},
)
if response.status_code != 200:
error_info = response.json() # Assuming the error response is in JSON format
error_description = error_info.get("error_description", "Unknown error")
logging.error(
f"{LIGHT_RED}Spotify Authentication Error: {error_description}{RESET}"
)
sys.exit(1)
SPOTIFY_TOKEN = response.json()["access_token"]
init(autoreset=True)
# Default flags
PREVIEW = 0
START = 0
LIMIT = 0
ARTIST_IDs = []
ALBUM_IDs = []
# Variables
ARTISTS_PROCESSED = 0
TOTAL_TRACKS = 0
FOUND_AND_UPDATED = 0
NOT_FOUND = 0
UNMATCHED_TRACKS = []
# Parse arguments
description_text = "process command-line flags for sync"
parser = argparse.ArgumentParser()
@@ -134,6 +143,11 @@ parser.add_argument(
help="limit to processing [NUM] artists from the start index",
)
parser.add_argument(
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
)
args = parser.parse_args()
ARTIST_IDs = args.artist if args.artist else []
@@ -141,6 +155,8 @@ ALBUM_IDs = args.album if args.album else []
START = args.start
LIMIT = args.limit
logging.info(f"{BOLD}Version:{RESET} {LIGHT_YELLOW}sptnr v{__version__}{RESET}")
if args.preview:
logging.info(f"{LIGHT_YELLOW}Preview mode, no changes will be made.{RESET}")
PREVIEW = 1
@@ -159,6 +175,20 @@ if not args.preview:
)
def validate_url(url):
if not re.match(r"https?://", url):
logging.error(
f"{LIGHT_RED}Config Error: URL must start with 'http://' or 'https://'.{RESET}"
)
return False
if url.endswith("/"):
logging.error(
f"{LIGHT_RED}Config Error: URL must not end with a trailing slash.{RESET}"
)
return False
return True
def url_encode(string):
return urllib.parse.quote_plus(string)
@@ -187,24 +217,24 @@ def process_track(track_id, artist_name, album, track_name):
try:
response = requests.get(spotify_url, headers=headers)
except requests.exceptions.ConnectionError:
logging.error(f"{LIGHT_RED}Error: Unable to reach server.{RESET}")
logging.error(f"{LIGHT_RED}Spotify Error: Unable to reach server.{RESET}")
sys.exit(1)
if response.status_code != 200:
if response.status_code == 429:
logging.error(
f"{LIGHT_RED}Error {response.status_code}: Retry after {BOLD}{response.headers.get('Retry-After', 'some time')}s{RESET}"
f"{LIGHT_RED}Spotify Error {response.status_code}: Retry after {BOLD}{response.headers.get('Retry-After', 'some time')}s{RESET}"
)
else:
logging.error(
f"{LIGHT_RED}Error {response.status_code}: {response.text}{RESET}"
f"{LIGHT_RED}Spotify Error {response.status_code}: {response.text}{RESET}"
)
sys.exit(1)
try:
return response.json()
except ValueError as e:
logging.error(
f"{LIGHT_RED}Error decoding JSON from Spotify API: {e}{RESET}"
f"{LIGHT_RED}Spotify Error: Error decoding JSON from Spotify API: {e}{RESET}"
)
sys.exit(1)
@@ -231,6 +261,15 @@ def process_track(track_id, artist_name, album, track_name):
)
found_track = len(spotify_data.get("tracks", {}).get("items", [])) > 0
if not found_track:
# Tertiary Search (replace 'Part' with 'Pt.')
modified_track_name = track_name.replace("Part", "Pt.")
encoded_modified_track_name = url_encode(modified_track_name)
spotify_data = search_spotify(
f"{encoded_modified_track_name}%20artist:{encoded_artist_name}"
)
found_track = len(spotify_data.get("tracks", {}).get("items", []))
if found_track:
popularity = spotify_data["tracks"]["items"][0].get("popularity", 0)
rating = get_rating_from_popularity(popularity)
@@ -285,9 +324,47 @@ def process_artist(artist_id):
def fetch_data(url):
response = requests.get(url)
return json.loads(response.text)["subsonic-response"]
try:
response = requests.get(url)
response_data = json.loads(response.text)
if "subsonic-response" not in response_data:
logging.error(
f"{LIGHT_RED}Unexpected response format from Navidrome.{RESET}"
)
sys.exit(1)
nav_response = response_data["subsonic-response"]
if "error" in nav_response:
error_message = nav_response["error"].get("message", "Unknown error")
logging.error(f"{LIGHT_RED}Navidrome Error: {error_message}{RESET}")
sys.exit(1)
return nav_response
except requests.exceptions.ConnectionError:
logging.error(
f"{LIGHT_RED}Connection Error: Failed to connect to the provided URL. Please check if the URL is correct and the server is reachable.{RESET}"
)
sys.exit(1)
except requests.exceptions.RequestException as e:
logging.error(
f"{LIGHT_RED}Connection Error: An error occurred while trying to connect to Navidrome: {e}{RESET}"
)
sys.exit(1)
except json.JSONDecodeError:
logging.error(
f"{LIGHT_RED}JSON Parsing Error: Failed to parse JSON response from Navidrome. Please check if the provided URL is a valid Navidrome server.{RESET}"
)
sys.exit(1)
try:
validate_url(NAV_BASE_URL)
except ValueError as e:
logging.error(f"{LIGHT_RED}{e}{RESET}")
sys.exit(1)
if ARTIST_IDs:
for ARTIST_ID in ARTIST_IDs: