Add playlist support and ditch sysbox

This commit is contained in:
cool.gitter.choco
2025-01-24 13:45:39 -06:00
parent 21fd2e33e0
commit 30b6929ef8
9 changed files with 1068 additions and 93 deletions

1042
.gitignore vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
## SUPPORT YOUR ARTISTS
As of 2025, Spotify pays an average of $0.005 per stream to the artist. That means that if you give the equivalent of $5 directly to them (like merch, buying cds, or just donating), you can """ethically""" listen to them a total of 1000 times. Of course, nobody thinks spotify payment is fair, so preferably you should give more, but $5 is the bare minimum. Big names prolly don't need those $5 dollars, but it might be _the_ difference between going out of business or not for that indie rock band you like.
## Installation
1. Clone this repo
2. Inside its directory, run `docker build -t zotifarr .`
3. Setup the docker-compose.yaml file (for easier maintenance, preferably in a different directory from the installation)
4. Deploy the container and access {ip of your server}:7070.
5. Start ripping
## Adding accounts
Follow [this repo](https://github.com/dspearson/librespot-auth?tab=readme-ov-file) using a PC/laptop with spotify client installed, and once it generates the credentials.json, you need to modify it as follows:
- Replace `"auth_type": 1` with `"type":"AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS"`
- Rename `"auth_data"` to `"credentials"`
Once you do that, you need to go where your docker-compose.yaml is and create the file `./credentials/{account_custom_name}/credentials.json` and paste the contents from the `credentials.json` you modified previously on your PC/laptop.
Added accounts should be catched automatically on the frontend after reloading it.
## Acknowledgements
[Zotify](https://github.com/zotify-dev/zotify), for basically being 90% of this project. Cool shi my guy, keep the great work.
This project is NOT an NZBDrone fork, despite its -arr suffix. More info [here](https://www.reddit.com/r/radarr/comments/hbwnb2/a_list_of_all_companion_tools_and_software/)
## Roadmap:
* Before downloading from spotify, try to download from another service (deezer, tidal or qobuz) in order to at least try on getting best quality.

2
app.py
View File

@@ -15,4 +15,4 @@ def serve_frontend():
return send_from_directory(app.static_folder, 'index.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7070, debug=True, threaded=True)
app.run(host='0.0.0.0', port=7071, debug=True, threaded=True)

View File

@@ -1,38 +0,0 @@
#!/bin/bash
# Build the "zotify" image using ./builds as the build context
echo "Building zotify image..."
docker build -t zotify ./builds
# Check if the build was successful
if [ $? -eq 0 ]; then
echo "zotify image built successfully."
# Save the zotify image as a tar file
echo "Saving zotify image to ./zotify.tar..."
docker save -o ./zotify.tar zotify
if [ $? -eq 0 ]; then
echo "zotify image saved to ./zotify.tar."
else
echo "Failed to save zotify image."
exit 1
fi
else
echo "Failed to build zotify image."
exit 1
fi
# Build the "zotifarr" image using ./ as the build context
echo "Building zotifarr image..."
docker build -t zotifarr ./
# Check if the build was successful
if [ $? -eq 0 ]; then
echo "zotifarr image built successfully."
else
echo "Failed to build zotifarr image."
exit 1
fi
echo "All tasks completed successfully."

View File

@@ -1,15 +0,0 @@
version: "3.8"
services:
zotifarr:
image: zotifarr
ports:
- "7070:7070"
runtime: sysbox-runc
volumes:
- "./credentials:/app/credentials"
- "./downloads:/app/downloads" # <-- change this mapping for your music library directory
environment:
FLASK_APP: app.py
FLASK_RUN_HOST: 0.0.0.0
restart: unless-stopped

View File

@@ -106,6 +106,7 @@ export function initDownload() {
let currentTrackName = '';
let currentTrackArtists = '';
let playlistName = '';
let albumName = '';
while (true) {
const { done, value } = await reader.read();
@@ -137,6 +138,14 @@ export function initDownload() {
statusLine.textContent = `Starting playlist: ${playlistName} (${totalTracks} tracks)`;
break;
case 'album_start':
totalTracks = data.total_tracks;
albumName = data.album;
completedTracks = 0;
currentTrackNumber = 0;
statusLine.textContent = `Starting album: ${albumName} (${totalTracks} tracks)`;
break;
case 'track_start':
if (type === 'playlist') {
currentTrackNumber++;
@@ -146,10 +155,16 @@ export function initDownload() {
}
break;
case 'album_track_start':
currentTrackNumber = data.number;
currentTrackName = data.track;
statusLine.textContent = `Starting track ${currentTrackNumber}/${totalTracks}: ${currentTrackName}`;
break;
case 'metadata':
if (type === 'track') {
statusLine.textContent = `Downloading: ${data.name} by ${data.artists[0]}`;
} else if (type === 'playlist') {
} else if (type === 'playlist' || type === 'album') {
currentTrackName = data.name;
currentTrackArtists = data.artists.join(', ');
statusLine.textContent = `Downloading track ${currentTrackNumber}/${totalTracks}: ${currentTrackName} by ${currentTrackArtists}`;
@@ -166,9 +181,7 @@ export function initDownload() {
const progress = `${lastPercentage}% of ${formatBytes(data.total)}`;
if (type === 'track') {
statusLine.textContent = `Downloading: ${progress}`;
} else if (type === 'album') {
statusLine.textContent = `Downloading track ${currentTrackNumber}/${totalTracks}: ${currentTrackName} (${progress})`;
} else if (type === 'playlist') {
} else if (type === 'album' || type === 'playlist') {
statusLine.textContent = `Downloading track ${currentTrackNumber}/${totalTracks}: ${currentTrackName} (${progress})`;
} else {
statusLine.textContent = `Downloading track ${completedTracks + 1}: ${progress}`;
@@ -200,6 +213,11 @@ export function initDownload() {
statusLine.classList.add('completed');
break;
case 'album_complete':
statusLine.textContent = `Album "${data.album}" complete! ${data.successful}/${data.total} tracks downloaded`;
statusLine.classList.add('completed');
break;
case 'track_error':
completedTracks++;
statusLine.textContent = `Error: ${data.error}`;
@@ -233,4 +251,4 @@ function formatBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}

View File

@@ -1,4 +1,3 @@
// Account loading functionality
export async function loadAccounts() {
try {
const response = await fetch('/api/check');

View File

@@ -51,4 +51,4 @@ def download():
finally:
proc.wait()
return Response(generate(), mimetype='application/x-ndjson')
return Response(generate(), mimetype='application/x-ndjson')

View File

@@ -9,4 +9,4 @@ def zotify_command_builder(account: str, action: str, args: str) -> list:
return [*base_cmd, args] # args is Spotify URL
elif action == 'search':
return [*base_cmd, '-s', args] # args is search query
raise ValueError(f"Invalid action: {action}")
raise ValueError(f"Invalid action: {action}")