mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2024-11-26 09:53:17 +01:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
2014cfa160
22
.github/workflows/pylint.yml
vendored
Normal file
22
.github/workflows/pylint.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Pylint
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint
|
||||
- name: Analysing the code with pylint
|
||||
run: |
|
||||
pylint `ls -R|grep .py$|xargs`
|
40
CONTRIBUTING.md
Normal file
40
CONTRIBUTING.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Introduction
|
||||
|
||||
### Thank you for contributing
|
||||
|
||||
Without people like you this project wouldn't be anywhere near as polished and feature-rich as it is now.
|
||||
|
||||
### Guidelines
|
||||
|
||||
Following these guidelines helps show that you respect the the time and effort spent by the developers and your fellow contributors making this project.
|
||||
|
||||
### What we are looking for
|
||||
|
||||
ZSpotify is a community-driven project. There are many different ways to contribute. From providing tutorials and examples to help new users, reporting bugs, requesting new features, writing new code that can be added to the project, or even writing documentation.
|
||||
|
||||
### What we aren't looking for
|
||||
|
||||
Please don't use the issues section to request help installing or setting up the project. It should be reserved for bugs when running the code, and feature requqests. Instead use the support channel in either our Discord or Matrix server.
|
||||
|
||||
# Ground rules
|
||||
|
||||
### Expectations
|
||||
* Ensure all code is linted with pylint before pushing.
|
||||
* Ensure all code passes the [testing criteria](#testing-criteria).
|
||||
* If you're planning on contributing a new feature, join the Discord or Matrix and discuss it with the Dev Team.
|
||||
* Please don't commit multiple new features at once.
|
||||
* Follow the [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/)
|
||||
|
||||
# Your first contribution
|
||||
|
||||
Unsure where to start? Have a look for any issues tagged "good first issue". They should be minor bugs that only require a few lines to fix.
|
||||
Here are a couple of friendly tutorials on making pull requests: http://makeapullrequest.com/ and http://www.firsttimersonly.com/
|
||||
|
||||
# Code review process
|
||||
|
||||
The dev team looks at Pull Requests around once per day. After feedback has been given we expect responses within one week. After a week we may close the pull request if it isn't showing any activity.
|
||||
> ZSpotify updates very frequently, often multiple times per day. If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your branch so it's easier to merge.
|
||||
|
||||
# Community
|
||||
|
||||
Come and chat with us on Discord or Matrix. Devs try to respond to mentions at least once per day.
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM python:3.9-alpine as base
|
||||
|
||||
RUN apk --update add git ffmpeg
|
||||
|
||||
FROM base as builder
|
||||
RUN mkdir /install
|
||||
WORKDIR /install
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN apk add gcc libc-dev zlib zlib-dev jpeg-dev \
|
||||
&& pip install --prefix="/install" -r /requirements.txt
|
||||
|
||||
|
||||
FROM base
|
||||
|
||||
COPY --from=builder /install /usr/local
|
||||
COPY src /app
|
||||
COPY zs_config.json /
|
||||
WORKDIR /app
|
||||
ENTRYPOINT ["/usr/local/bin/python", "app.py"]
|
24
README.md
24
README.md
@ -14,7 +14,7 @@ Requirements:
|
||||
|
||||
Binaries
|
||||
|
||||
- Python 3.8 or greater
|
||||
- Python 3.9 or greater
|
||||
- ffmpeg*
|
||||
- Git**
|
||||
|
||||
@ -29,9 +29,9 @@ Python packages:
|
||||
\*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows.
|
||||
```
|
||||
Command line usage:
|
||||
python zspotify.py Loads search prompt to find then download a specific track, album or playlist
|
||||
python zspotify.py <track/album/playlist/episode url> Downloads the track, album, playlist or podcast episode specified as a command line argument
|
||||
python zspotify.py <artist url> Downloads all albums by specified artist
|
||||
python app.py Loads search prompt to find then download a specific track, album or playlist
|
||||
python app.py <track/album/playlist/episode url> Downloads the track, album, playlist or podcast episode specified as a command line argument
|
||||
python app.py <artist url> Downloads all albums by specified artist
|
||||
|
||||
Extra command line options:
|
||||
-p, --playlist Downloads a saved playlist from your account
|
||||
@ -43,8 +43,7 @@ Options that can be configured in zs_config.json:
|
||||
|
||||
SKIP_EXISTING_FILES Set this to false if you want ZSpotify to overwrite files with the same name rather than skipping the song
|
||||
|
||||
MUSIC_FORMAT Set this to "ogg" if you would rather that format audio over "mp3"
|
||||
RAW_AUDIO_AS_IS Set this to true to only stream the audio to a file and do no re-encoding or post processing
|
||||
MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higer quality as it is not trsnacoded.
|
||||
|
||||
FORCE_PREMIUM Set this to true if ZSpotify isn't automatically detecting that you are using a premium account
|
||||
|
||||
@ -60,9 +59,20 @@ This isn't to say _you_ won't get banned as it is technically against Spotify's
|
||||
If you see this, don't worry! Just try logging back in. If you see the incorrect username or password error, reset your password and you should be able to log back in and continue using Spotify.
|
||||
|
||||
### Contributing
|
||||
Please be sure to lint your code with pylint before issuing a pull-request, thanks!
|
||||
Please refer to CONTRIBUTING.md
|
||||
|
||||
## **Changelog:**
|
||||
**v2.2 (24 Oct 2021):**
|
||||
- Added basic support for downloading an entire podcast series.
|
||||
- Split code into multiple files for easier maintenance.
|
||||
- Changed initial launch script to app.py
|
||||
- Simplified audio formats.
|
||||
- Added prebuild exe for Windows users.
|
||||
- Added Docker file.
|
||||
- Added CONTRIBUTING.md.
|
||||
- Fixed artist names getting cutoff in metadata.
|
||||
- Removed data sanitization of metadata tags.
|
||||
|
||||
**v2.1 (23 Oct 2021):**
|
||||
- Moved configuration from hard-coded values to separate zs_config.json file.
|
||||
- Add subfolders for each disc.
|
||||
|
@ -28,7 +28,7 @@ def get_album_tracks(album_id):
|
||||
def get_album_name(album_id):
|
||||
""" Returns album name """
|
||||
resp = ZSpotify.invoke_url(f'{ALBUM_URL}/{album_id}')
|
||||
return sanitize_data(resp[ARTISTS][0][NAME]), sanitize_data(resp[NAME])
|
||||
return resp[ARTISTS][0][NAME], sanitize_data(resp[NAME])
|
||||
|
||||
|
||||
def get_artist_albums(artist_id):
|
||||
|
@ -84,8 +84,6 @@ SKIP_EXISTING_FILES = 'SKIP_EXISTING_FILES'
|
||||
|
||||
DOWNLOAD_FORMAT = 'DOWNLOAD_FORMAT'
|
||||
|
||||
RAW_AUDIO_AS_IS = 'RAW_AUDIO_AS_IS'
|
||||
|
||||
FORCE_PREMIUM = 'FORCE_PREMIUM'
|
||||
|
||||
ANTI_BAN_WAIT_TIME = 'ANTI_BAN_WAIT_TIME'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from librespot.audio.decoders import VorbisOnlyAudioQuality
|
||||
@ -8,6 +9,7 @@ from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE
|
||||
from utils import sanitize_data, create_download_directory, MusicFormat
|
||||
from zspotify import ZSpotify
|
||||
|
||||
|
||||
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
|
||||
SHOWS_URL = 'https://api.spotify.com/v1/shows'
|
||||
|
||||
@ -21,11 +23,16 @@ def get_episode_info(episode_id_str) -> tuple[Optional[str], Optional[str]]:
|
||||
|
||||
def get_show_episodes(show_id_str) -> list:
|
||||
episodes = []
|
||||
offset = 0
|
||||
limit = 50
|
||||
|
||||
resp = ZSpotify.invoke_url(f'{SHOWS_URL}/{show_id_str}/episodes')
|
||||
|
||||
for episode in resp[ITEMS]:
|
||||
episodes.append(episode[ID])
|
||||
while True:
|
||||
resp = ZSpotify.invoke_url_with_params(f'{SHOWS_URL}/{show_id_str}/episodes', limit=limit, offset=offset)
|
||||
offset += limit
|
||||
for episode in resp[ITEMS]:
|
||||
episodes.append(episode[ID])
|
||||
if len(resp[ITEMS]) < limit:
|
||||
break
|
||||
|
||||
return episodes
|
||||
|
||||
@ -43,10 +50,11 @@ def download_episode(episode_id) -> None:
|
||||
episode_id = EpisodeId.from_base62(episode_id)
|
||||
stream = ZSpotify.get_content_stream(episode_id, ZSpotify.DOWNLOAD_QUALITY)
|
||||
|
||||
create_download_directory(ZSpotify.get_config(ROOT_PODCAST_PATH) + extra_paths)
|
||||
download_directory = os.path.dirname(__file__) + ZSpotify.get_config(ROOT_PODCAST_PATH) + extra_paths
|
||||
create_download_directory(download_directory)
|
||||
|
||||
total_size = stream.input_stream.size
|
||||
with open(ZSpotify.get_config(ROOT_PODCAST_PATH) + extra_paths + filename + MusicFormat.WAV.value,
|
||||
with open(download_directory + filename + MusicFormat.OGG.value,
|
||||
'wb') as file, tqdm(
|
||||
desc=filename,
|
||||
total=total_size,
|
||||
@ -59,6 +67,4 @@ def download_episode(episode_id) -> None:
|
||||
stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE))))
|
||||
|
||||
# convert_audio_format(ROOT_PODCAST_PATH +
|
||||
# extra_paths + filename + '.wav')
|
||||
|
||||
# related functions that do stuff with the spotify API
|
||||
# extra_paths + filename + '.ogg')
|
28
src/track.py
28
src/track.py
@ -9,7 +9,7 @@ from tqdm import tqdm
|
||||
|
||||
from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \
|
||||
RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \
|
||||
SKIP_EXISTING_FILES, RAW_AUDIO_AS_IS, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT
|
||||
SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT
|
||||
from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory, \
|
||||
MusicFormat
|
||||
from zspotify import ZSpotify
|
||||
@ -37,9 +37,9 @@ def get_song_info(song_id) -> tuple[list[str], str, str, Any, Any, Any, Any, Any
|
||||
|
||||
artists = []
|
||||
for data in info[TRACKS][0][ARTISTS]:
|
||||
artists.append(data[NAME])
|
||||
album_name = info[TRACKS][0][ALBUM][NAME]
|
||||
name = info[TRACKS][0][NAME]
|
||||
artists.append(sanitize_data(data[NAME]))
|
||||
album_name = sanitize_data(info[TRACKS][0][ALBUM][NAME])
|
||||
name = sanitize_data(info[TRACKS][0][NAME])
|
||||
image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL]
|
||||
release_year = info[TRACKS][0][ALBUM][RELEASE_DATE].split('-')[0]
|
||||
disc_number = info[TRACKS][0][DISC_NUMBER]
|
||||
@ -53,21 +53,22 @@ def get_song_info(song_id) -> tuple[list[str], str, str, Any, Any, Any, Any, Any
|
||||
# noinspection PyBroadException
|
||||
def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', disable_progressbar=False) -> None:
|
||||
""" Downloads raw song audio from Spotify """
|
||||
download_directory = os.path.join(os.path.dirname(__file__), ZSpotify.get_config(ROOT_PATH), extra_paths)
|
||||
try:
|
||||
(artists, album_name, name, image_url, release_year, disc_number,
|
||||
track_number, scraped_song_id, is_playable) = get_song_info(track_id)
|
||||
|
||||
song_name = sanitize_data(artists[0]) + ' - ' + sanitize_data(name)
|
||||
song_name = artists[0] + ' - ' + name
|
||||
if prefix:
|
||||
song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit(
|
||||
) else f'{prefix_value} - {song_name}'
|
||||
|
||||
if ZSpotify.get_config(SPLIT_ALBUM_DISCS):
|
||||
filename = os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths, 'Disc ' + str(
|
||||
disc_number) + '/' + song_name + '.' + ZSpotify.get_config(DOWNLOAD_FORMAT))
|
||||
filename = os.path.join(download_directory, f'Disc {disc_number}',
|
||||
f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}')
|
||||
else:
|
||||
filename = os.path.join(ZSpotify.get_config(ROOT_PATH), extra_paths,
|
||||
song_name + '.' + ZSpotify.get_config(DOWNLOAD_FORMAT))
|
||||
filename = os.path.join(download_directory,
|
||||
f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}')
|
||||
except Exception:
|
||||
print('### SKIPPING SONG - FAILED TO QUERY METADATA ###')
|
||||
else:
|
||||
@ -84,7 +85,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
||||
track_id = scraped_song_id
|
||||
track_id = TrackId.from_base62(track_id)
|
||||
stream = ZSpotify.get_content_stream(track_id, ZSpotify.DOWNLOAD_QUALITY)
|
||||
create_download_directory(ZSpotify.get_config(ROOT_PATH) + extra_paths)
|
||||
create_download_directory(download_directory)
|
||||
total_size = stream.input_stream.size
|
||||
|
||||
with open(filename, 'wb') as file, tqdm(
|
||||
@ -99,7 +100,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
||||
p_bar.update(file.write(
|
||||
stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE))))
|
||||
|
||||
if not ZSpotify.get_config(RAW_AUDIO_AS_IS):
|
||||
if ZSpotify.get_config(DOWNLOAD_FORMAT) == 'mp3':
|
||||
convert_audio_format(filename)
|
||||
set_audio_tags(filename, artists, name, album_name,
|
||||
release_year, disc_number, track_number)
|
||||
@ -107,15 +108,16 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
||||
|
||||
if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT):
|
||||
time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME))
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print('### SKIPPING:', song_name,
|
||||
'(GENERAL DOWNLOAD ERROR) ###')
|
||||
print(e)
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
|
||||
|
||||
def convert_audio_format(filename) -> None:
|
||||
""" Converts raw audio into playable mp3 or ogg vorbis """
|
||||
""" Converts raw audio into playable mp3 """
|
||||
# print('### CONVERTING TO ' + MUSIC_FORMAT.upper() + ' ###')
|
||||
raw_audio = AudioSegment.from_file(filename, format=MusicFormat.OGG.value,
|
||||
frame_rate=44100, channels=2, sample_width=2)
|
||||
|
@ -14,8 +14,7 @@ from const import SANITIZE, ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNU
|
||||
class MusicFormat(str, Enum):
|
||||
MP3 = 'mp3',
|
||||
OGG = 'ogg',
|
||||
WAV = 'wav'
|
||||
|
||||
|
||||
|
||||
def create_download_directory(download_path: str) -> None:
|
||||
os.makedirs(download_path, exist_ok=True)
|
||||
|
@ -17,7 +17,7 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality
|
||||
from librespot.core import Session
|
||||
|
||||
from const import CREDENTIALS_JSON, TYPE, \
|
||||
PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, CONFIG_FILE_PATH, FORCE_PREMIUM, RAW_AUDIO_AS_IS, \
|
||||
PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, CONFIG_FILE_PATH, FORCE_PREMIUM, \
|
||||
PLAYLIST_READ_PRIVATE
|
||||
from utils import MusicFormat
|
||||
|
||||
@ -29,7 +29,6 @@ class ZSpotify:
|
||||
|
||||
def __init__(self):
|
||||
ZSpotify.load_config()
|
||||
ZSpotify.check_raw()
|
||||
ZSpotify.login()
|
||||
|
||||
@classmethod
|
||||
@ -55,18 +54,14 @@ class ZSpotify:
|
||||
|
||||
@classmethod
|
||||
def load_config(cls) -> None:
|
||||
with open(CONFIG_FILE_PATH, encoding='utf-8') as config_file:
|
||||
app_dir = os.path.dirname(__file__)
|
||||
with open(os.path.join(app_dir, CONFIG_FILE_PATH), encoding='utf-8') as config_file:
|
||||
cls.CONFIG = json.load(config_file)
|
||||
|
||||
@classmethod
|
||||
def get_config(cls, key) -> Any:
|
||||
return cls.CONFIG.get(key)
|
||||
|
||||
@classmethod
|
||||
def check_raw(cls) -> None:
|
||||
if cls.get_config(RAW_AUDIO_AS_IS):
|
||||
cls.DOWNLOAD_FORMAT = MusicFormat.WAV
|
||||
|
||||
@classmethod
|
||||
def get_content_stream(cls, content_id, quality):
|
||||
return cls.SESSION.content_feeder().load(content_id, VorbisOnlyAudioQuality(quality), False, None)
|
||||
|
@ -3,7 +3,6 @@
|
||||
"ROOT_PODCAST_PATH": "../ZSpotify Podcasts/",
|
||||
"SKIP_EXISTING_FILES": true,
|
||||
"DOWNLOAD_FORMAT": "mp3",
|
||||
"RAW_AUDIO_AS_IS": false,
|
||||
"FORCE_PREMIUM": false,
|
||||
"ANTI_BAN_WAIT_TIME": 1,
|
||||
"OVERRIDE_AUTO_WAIT": false,
|
||||
|
Loading…
Reference in New Issue
Block a user