2015-05-14 00:41:45 -07:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- encoding: utf-8 -*-
|
|
|
|
|
|
2017-12-10 06:43:07 -08:00
|
|
|
|
"""scdl allows you to download music from Soundcloud
|
2014-10-20 13:23:46 -07:00
|
|
|
|
|
2014-10-14 10:43:30 -07:00
|
|
|
|
Usage:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
scdl -l <track_url> [-a | -f | -C | -t | -p | -r][-c | --force-metadata][-n <maxtracks>]
|
2020-06-10 10:27:55 -07:00
|
|
|
|
[-o <offset>][--hidewarnings][--debug | --error][--path <path>][--addtofile][--addtimestamp]
|
2020-06-10 10:37:45 -07:00
|
|
|
|
[--onlymp3][--hide-progress][--min-size <size>][--max-size <size>][--remove][--no-album-tag]
|
2021-11-17 15:17:37 -08:00
|
|
|
|
[--no-playlist-folder][--download-archive <file>][--extract-artist][--flac][--original-art]
|
|
|
|
|
[--original-name][--no-original][--only-original][--name-format <format>]
|
|
|
|
|
[--playlist-name-format <format>][--client-id <id>][--auth-token <token>][--overwrite]
|
2015-01-14 08:55:14 -08:00
|
|
|
|
scdl -h | --help
|
|
|
|
|
scdl --version
|
2014-10-14 10:43:30 -07:00
|
|
|
|
|
2014-10-20 13:23:46 -07:00
|
|
|
|
|
2014-10-14 10:43:30 -07:00
|
|
|
|
Options:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
-h --help Show this screen
|
|
|
|
|
--version Show version
|
|
|
|
|
-l [url] URL can be track/playlist/user
|
|
|
|
|
-n [maxtracks] Download the n last tracks of a playlist according to the creation date
|
|
|
|
|
-s Download the stream of a user (token needed)
|
|
|
|
|
-a Download all tracks of user (including reposts)
|
|
|
|
|
-t Download all uploads of a user (no reposts)
|
|
|
|
|
-f Download all favorites of a user
|
|
|
|
|
-C Download all commented by a user
|
|
|
|
|
-p Download all playlists of a user
|
|
|
|
|
-r Download all reposts of user
|
|
|
|
|
-c Continue if a downloaded file already exists
|
|
|
|
|
--force-metadata This will set metadata on already downloaded track
|
|
|
|
|
-o [offset] Begin with a custom offset
|
|
|
|
|
--addtimestamp Add track creation timestamp to filename,
|
|
|
|
|
which allows for chronological sorting
|
|
|
|
|
--addtofile Add artist to filename if missing
|
|
|
|
|
--debug Set log level to DEBUG
|
|
|
|
|
--download-archive [file] Keep track of track IDs in an archive file,
|
|
|
|
|
and skip already-downloaded files
|
|
|
|
|
--error Set log level to ERROR
|
|
|
|
|
--extract-artist Set artist tag from title instead of username
|
|
|
|
|
--hide-progress Hide the wget progress bar
|
|
|
|
|
--hidewarnings Hide Warnings. (use with precaution)
|
|
|
|
|
--max-size [max-size] Skip tracks larger than size (k/m/g)
|
|
|
|
|
--min-size [min-size] Skip tracks smaller than size (k/m/g)
|
|
|
|
|
--no-playlist-folder Download playlist tracks into main directory,
|
|
|
|
|
instead of making a playlist subfolder
|
|
|
|
|
--onlymp3 Download only the streamable mp3 file,
|
|
|
|
|
even if track has a Downloadable file
|
|
|
|
|
--path [path] Use a custom path for downloaded files
|
|
|
|
|
--remove Remove any files not downloaded from execution
|
|
|
|
|
--flac Convert original files to .flac
|
|
|
|
|
--no-album-tag On some player track get the same cover art if from the same album, this prevent it
|
|
|
|
|
--original-art Download original cover art
|
|
|
|
|
--original-name Do not change name of original file downloads
|
|
|
|
|
--no-original Do not download original file; only mp3 or m4a
|
|
|
|
|
--only-original Only download songs with original file available
|
|
|
|
|
--name-format [format] Specify the downloaded file name format
|
|
|
|
|
--playlist-name-format [format] Specify the downloaded file name format, if it is being downloaded as part of a playlist
|
|
|
|
|
--client-id [id] Specify the client_id to use
|
|
|
|
|
--auth-token [token] Specify the auth token to use
|
|
|
|
|
--overwrite Overwrite file if it already exists
|
2014-10-14 10:43:30 -07:00
|
|
|
|
"""
|
2015-05-09 04:01:49 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
import configparser
|
2015-05-09 04:01:49 -07:00
|
|
|
|
import logging
|
2021-11-17 15:17:37 -08:00
|
|
|
|
import mimetypes
|
|
|
|
|
import pathlib
|
|
|
|
|
|
|
|
|
|
mimetypes.init()
|
|
|
|
|
|
2014-10-20 13:23:46 -07:00
|
|
|
|
import os
|
2021-11-17 15:17:37 -08:00
|
|
|
|
import re
|
|
|
|
|
import shutil
|
2014-10-20 13:23:46 -07:00
|
|
|
|
import signal
|
2021-11-17 15:17:37 -08:00
|
|
|
|
import subprocess
|
2014-10-20 13:23:46 -07:00
|
|
|
|
import sys
|
2021-11-17 15:17:37 -08:00
|
|
|
|
import tempfile
|
2014-10-23 08:12:24 -07:00
|
|
|
|
import time
|
2015-05-09 04:10:15 -07:00
|
|
|
|
import warnings
|
2021-11-17 15:17:37 -08:00
|
|
|
|
from dataclasses import asdict
|
2015-01-19 13:11:55 -08:00
|
|
|
|
|
2015-05-09 04:10:15 -07:00
|
|
|
|
import mutagen
|
2021-11-17 15:17:37 -08:00
|
|
|
|
from mutagen.easymp4 import EasyMP4
|
2014-11-12 08:00:27 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
EasyMP4.RegisterTextKey("website", "purl")
|
|
|
|
|
import requests
|
|
|
|
|
from clint.textui import progress
|
|
|
|
|
from docopt import docopt
|
|
|
|
|
from pathvalidate import sanitize_filename
|
|
|
|
|
from soundcloud import BasicAlbumPlaylist, BasicTrack, MiniTrack, SoundCloud
|
2015-05-09 04:01:49 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
from scdl import __version__, utils
|
2016-08-27 07:33:16 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
|
|
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
2015-05-09 04:01:49 -07:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
logger.setLevel(logging.INFO)
|
2015-05-14 00:36:19 -07:00
|
|
|
|
logger.addFilter(utils.ColorizeFilter())
|
2015-05-09 04:01:49 -07:00
|
|
|
|
|
2017-10-27 14:47:25 -07:00
|
|
|
|
fileToKeep = []
|
2014-10-12 15:16:18 -07:00
|
|
|
|
|
2017-12-26 03:05:15 -08:00
|
|
|
|
|
2014-10-14 10:43:30 -07:00
|
|
|
|
def main():
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
Main function, parses the URL from command line arguments
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
# Parse arguments
|
2015-01-19 11:23:46 -08:00
|
|
|
|
arguments = docopt(__doc__, version=__version__)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
python_args = {
|
|
|
|
|
"offset": 1
|
|
|
|
|
}
|
2015-01-14 08:55:14 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if arguments["--debug"]:
|
2015-05-09 04:01:49 -07:00
|
|
|
|
logger.level = logging.DEBUG
|
2021-11-17 15:17:37 -08:00
|
|
|
|
elif arguments["--error"]:
|
2015-05-09 04:01:49 -07:00
|
|
|
|
logger.level = logging.ERROR
|
2021-11-17 15:17:37 -08:00
|
|
|
|
|
|
|
|
|
if "XDG_CONFIG_HOME" in os.environ:
|
|
|
|
|
config_file = pathlib.Path(os.environ["XDG_CONFIG_HOME"], "scdl", "scdl.cfg")
|
|
|
|
|
else:
|
|
|
|
|
config_file = pathlib.Path.home().joinpath(".config", "scdl", "scdl.cfg")
|
2015-01-14 08:55:14 -08:00
|
|
|
|
|
2017-01-27 08:27:53 -08:00
|
|
|
|
# import conf file
|
2021-11-17 15:17:37 -08:00
|
|
|
|
config = get_config(config_file)
|
|
|
|
|
|
|
|
|
|
# change download path
|
|
|
|
|
path = config["scdl"]["path"]
|
|
|
|
|
if os.path.exists(path):
|
|
|
|
|
os.chdir(path)
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"Invalid download path '{path}' in {config_file}")
|
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
|
|
logger.info("Soundcloud Downloader")
|
2015-05-09 04:01:49 -07:00
|
|
|
|
logger.debug(arguments)
|
2014-11-26 10:45:28 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not arguments["--client-id"]:
|
|
|
|
|
arguments["--client-id"] = config["scdl"]["client_id"]
|
|
|
|
|
|
|
|
|
|
if not arguments["--auth-token"]:
|
|
|
|
|
arguments["--auth-token"] = config["scdl"]["auth_token"]
|
|
|
|
|
|
|
|
|
|
client_id, token = arguments["--client-id"], arguments["--auth-token"]
|
|
|
|
|
|
|
|
|
|
client = SoundCloud(client_id, token if token else None)
|
|
|
|
|
|
|
|
|
|
if not client.is_client_id_valid():
|
|
|
|
|
raise ValueError(f"client_id is not valid")
|
|
|
|
|
|
|
|
|
|
if token and not client.is_auth_token_valid():
|
|
|
|
|
raise ValueError(f"auth_token is not valid")
|
|
|
|
|
|
|
|
|
|
if arguments["-o"] is not None:
|
2014-11-16 09:19:42 -08:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
python_args["offset"] = int(arguments["-o"])
|
|
|
|
|
if python_args["offset"] < 1:
|
|
|
|
|
raise ValueError()
|
2014-11-16 09:19:42 -08:00
|
|
|
|
except:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Offset should be a positive integer...")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.debug("offset: %d", python_args["offset"])
|
2014-11-16 09:19:42 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if arguments["--min-size"] is not None:
|
2016-07-20 04:01:22 -07:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
arguments["--min-size"] = utils.size_in_bytes(arguments["--min-size"])
|
2016-07-20 04:01:22 -07:00
|
|
|
|
except:
|
|
|
|
|
logger.exception(
|
2021-11-17 15:17:37 -08:00
|
|
|
|
"Min size should be an integer with a possible unit suffix"
|
2016-07-20 04:01:22 -07:00
|
|
|
|
)
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.debug("min-size: %d", arguments["--min-size"])
|
2016-07-20 04:01:22 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if arguments["--max-size"] is not None:
|
2016-07-20 04:01:22 -07:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
arguments["--max-size"] = utils.size_in_bytes(arguments["--max-size"])
|
2016-07-20 04:01:22 -07:00
|
|
|
|
except:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Max size should be an integer with a possible unit suffix")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.debug("max-size: %d", arguments["--max-size"])
|
2016-07-20 04:01:22 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if arguments["--hidewarnings"]:
|
|
|
|
|
warnings.filterwarnings("ignore")
|
2014-11-16 09:19:42 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if arguments["--path"] is not None:
|
|
|
|
|
if os.path.exists(arguments["--path"]):
|
|
|
|
|
os.chdir(arguments["--path"])
|
2014-12-02 17:16:04 -08:00
|
|
|
|
else:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Invalid path in arguments...")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.debug("Downloading to " + os.getcwd() + "...")
|
|
|
|
|
|
|
|
|
|
if not arguments["--name-format"]:
|
|
|
|
|
arguments["--name-format"] = config["scdl"]["name_format"]
|
|
|
|
|
|
|
|
|
|
if not arguments["--playlist-name-format"]:
|
|
|
|
|
arguments["--playlist-name-format"] = config["scdl"]["playlist_name_format"]
|
|
|
|
|
|
|
|
|
|
# convert arguments dict to python_args (kwargs-friendly args)
|
|
|
|
|
for key, value in arguments.items():
|
|
|
|
|
key = key.strip("-").replace("-", "_")
|
|
|
|
|
python_args[key] = value
|
|
|
|
|
|
|
|
|
|
if arguments["-l"]:
|
|
|
|
|
download_url(client, **python_args)
|
|
|
|
|
|
|
|
|
|
if arguments["--remove"]:
|
2017-12-10 06:43:07 -08:00
|
|
|
|
remove_files()
|
2017-10-27 14:47:25 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def get_config(config_file: pathlib.Path) -> configparser.ConfigParser:
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
Gets config from scdl.cfg
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
|
|
|
|
config = configparser.ConfigParser()
|
2021-11-17 15:17:37 -08:00
|
|
|
|
|
|
|
|
|
default_config_file = pathlib.Path(__file__).with_name("scdl.cfg")
|
2020-03-30 10:45:34 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
# load default config first
|
|
|
|
|
config.read(default_config_file)
|
|
|
|
|
|
|
|
|
|
# load config file if it exists
|
|
|
|
|
if config_file.exists():
|
|
|
|
|
config.read(config_file)
|
|
|
|
|
|
|
|
|
|
# save config to disk
|
|
|
|
|
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
with open(config_file, "w", encoding="UTF-8") as f:
|
|
|
|
|
config.write(f)
|
|
|
|
|
|
|
|
|
|
return config
|
2014-10-23 08:22:58 -07:00
|
|
|
|
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def download_url(client: SoundCloud, **kwargs):
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
Detects if a URL is a track or a playlist, and parses the track(s)
|
2016-02-08 05:32:59 -08:00
|
|
|
|
to the track downloader
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
url = kwargs.get("l")
|
|
|
|
|
item = client.resolve(url)
|
2016-03-01 14:58:59 -08:00
|
|
|
|
logger.debug(item)
|
2014-11-16 09:19:42 -08:00
|
|
|
|
if not item:
|
|
|
|
|
return
|
2021-11-17 15:17:37 -08:00
|
|
|
|
elif item.kind == "track":
|
|
|
|
|
logger.info("Found a track")
|
|
|
|
|
download_track(client, item, **kwargs)
|
|
|
|
|
elif item.kind == "playlist":
|
|
|
|
|
logger.info("Found a playlist")
|
|
|
|
|
download_playlist(client, item, **kwargs)
|
|
|
|
|
elif item.kind == "user":
|
|
|
|
|
user = item
|
|
|
|
|
logger.info("Found a user profile")
|
|
|
|
|
if kwargs.get("f"):
|
|
|
|
|
logger.info(f"Retrieving all likes of user {user.username}...")
|
|
|
|
|
resources = client.get_user_likes(user.id, limit=1000)
|
|
|
|
|
for i, like in enumerate(resources, 1):
|
|
|
|
|
logger.info(f"like n°{i} of {user.likes_count}")
|
|
|
|
|
if hasattr(like, "track"):
|
|
|
|
|
download_track(client, like.track, **kwargs)
|
|
|
|
|
elif hasattr(like, "playlist"):
|
|
|
|
|
download_playlist(client, client.get_playlist(like.playlist.id), **kwargs)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unknown like type {like}")
|
|
|
|
|
logger.info(f"Downloaded all likes of user {user.username}!")
|
|
|
|
|
elif kwargs.get("C"):
|
|
|
|
|
logger.info(f"Retrieving all commented tracks of user {user.username}...")
|
|
|
|
|
resources = client.get_user_comments(user.id, limit=1000)
|
|
|
|
|
for i, comment in enumerate(resources, 1):
|
|
|
|
|
logger.info(f"comment n°{i} of {user.comments_count}")
|
|
|
|
|
download_track(client, client.get_track(comment.track.id), **kwargs)
|
|
|
|
|
logger.info(f"Downloaded all commented tracks of user {user.username}!")
|
|
|
|
|
elif kwargs.get("t"):
|
|
|
|
|
logger.info(f"Retrieving all tracks of user {user.username}...")
|
|
|
|
|
resources = client.get_user_tracks(user.id, limit=1000)
|
|
|
|
|
for i, track in enumerate(resources, 1):
|
|
|
|
|
logger.info(f"track n°{i} of {user.track_count}")
|
|
|
|
|
download_track(client, track, **kwargs)
|
|
|
|
|
logger.info(f"Downloaded all tracks of user {user.username}!")
|
|
|
|
|
elif kwargs.get("a"):
|
|
|
|
|
logger.info(f"Retrieving all tracks & reposts of user {user.username}...")
|
|
|
|
|
resources = client.get_user_stream(user.id, limit=1000)
|
|
|
|
|
for i, item in enumerate(resources, 1):
|
|
|
|
|
logger.info(f"item n°{i} of {user.track_count + user.reposts_count if user.reposts_count else '?'}")
|
|
|
|
|
if item.type in ("track", "track-repost"):
|
|
|
|
|
download_track(client, item.track, **kwargs)
|
|
|
|
|
elif item.type in ("playlist", "playlist-repost"):
|
|
|
|
|
download_playlist(client, item.playlist, **kwargs)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unknown item type {item.type}")
|
|
|
|
|
logger.info(f"Downloaded all tracks & reposts of user {user.username}!")
|
|
|
|
|
elif kwargs.get("p"):
|
|
|
|
|
logger.info(f"Retrieving all playlists of user {user.username}...")
|
|
|
|
|
resources = client.get_user_playlists(user.id, limit=1000)
|
|
|
|
|
for i, playlist in enumerate(resources, 1):
|
|
|
|
|
logger.info(f"playlist n°{i} of {user.playlist_count}")
|
|
|
|
|
download_playlist(client, playlist, **kwargs)
|
|
|
|
|
logger.info(f"Downloaded all playlists of user {user.username}!")
|
|
|
|
|
elif kwargs.get("r"):
|
|
|
|
|
logger.info(f"Retrieving all reposts of user {user.username}...")
|
|
|
|
|
resources = client.get_user_reposts(user.id, limit=1000)
|
|
|
|
|
for i, item in enumerate(resources, 1):
|
|
|
|
|
logger.info(f"item n°{i} of {user.reposts_count or '?'}")
|
|
|
|
|
if item.type == "track-repost":
|
|
|
|
|
download_track(client, item.track, **kwargs)
|
|
|
|
|
elif item.type == "playlist-repost":
|
|
|
|
|
download_playlist(client, item.playlist, **kwargs)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unknown item type {item.type}")
|
|
|
|
|
logger.info(f"Downloaded all reposts of user {user.username}!")
|
2014-11-16 09:19:42 -08:00
|
|
|
|
else:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Please provide a download type...")
|
2014-11-16 09:19:42 -08:00
|
|
|
|
else:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Unknown item type {0}".format(item.kind))
|
2017-10-27 14:47:25 -07:00
|
|
|
|
|
2017-12-10 06:43:07 -08:00
|
|
|
|
def remove_files():
|
2017-10-27 14:47:25 -07:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
Removes any pre-existing tracks that were not just downloaded
|
2017-10-27 14:47:25 -07:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
logger.info("Removing local track files that were not downloaded...")
|
2021-11-17 15:17:37 -08:00
|
|
|
|
files = [f for f in os.listdir(".") if os.path.isfile(f)]
|
2017-10-27 14:47:25 -07:00
|
|
|
|
for f in files:
|
2017-12-26 03:05:15 -08:00
|
|
|
|
if f not in fileToKeep:
|
2017-10-27 14:47:25 -07:00
|
|
|
|
os.remove(f)
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def download_playlist(client: SoundCloud, playlist: BasicAlbumPlaylist, **kwargs):
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
Downloads a playlist
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
playlist_name = playlist.title.encode("utf-8", "ignore")
|
|
|
|
|
playlist_name = playlist_name.decode("utf8")
|
|
|
|
|
playlist_name = sanitize_filename(playlist_name)
|
2015-01-19 13:11:55 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not kwargs.get("no_playlist_folder"):
|
2017-12-22 00:55:19 -08:00
|
|
|
|
if not os.path.exists(playlist_name):
|
|
|
|
|
os.makedirs(playlist_name)
|
|
|
|
|
os.chdir(playlist_name)
|
2015-01-19 13:11:55 -08:00
|
|
|
|
|
2017-03-08 02:05:23 -08:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("n"): # Order by creation date and get the n lasts tracks
|
|
|
|
|
playlist.tracks.sort(
|
|
|
|
|
key=lambda track: track.created_at, reverse=True
|
|
|
|
|
)
|
|
|
|
|
playlist.tracks = playlist.tracks[: int(kwargs.get("n"))]
|
|
|
|
|
else:
|
|
|
|
|
del playlist.tracks[: kwargs.get("offset") - 1]
|
|
|
|
|
tracknumber_digits = len(str(len(playlist.tracks)))
|
|
|
|
|
for counter, track in enumerate(playlist.tracks, kwargs.get("offset")):
|
|
|
|
|
logger.debug(track)
|
|
|
|
|
logger.info(f"Track n°{counter}")
|
|
|
|
|
playlist_info = {
|
|
|
|
|
"author": playlist.user.username,
|
|
|
|
|
"title": playlist.title,
|
|
|
|
|
"tracknumber": str(counter).zfill(tracknumber_digits),
|
|
|
|
|
}
|
|
|
|
|
if isinstance(track, MiniTrack):
|
|
|
|
|
track = client.get_track(track.id)
|
|
|
|
|
download_track(client, track, playlist_info, **kwargs)
|
2017-03-08 02:05:23 -08:00
|
|
|
|
finally:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not kwargs.get("no_playlist_folder"):
|
|
|
|
|
os.chdir("..")
|
2017-12-26 03:05:15 -08:00
|
|
|
|
|
2017-08-30 16:19:37 -07:00
|
|
|
|
def try_utime(path, filetime):
|
|
|
|
|
try:
|
|
|
|
|
os.utime(path, (time.time(), filetime))
|
|
|
|
|
except:
|
2020-06-08 11:51:21 -07:00
|
|
|
|
logger.error("Cannot update utime of file")
|
2017-08-30 16:19:37 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def get_filename(track: BasicTrack, original_filename=None, aac=False, playlist_info=None, **kwargs):
|
|
|
|
|
|
|
|
|
|
if kwargs.get("original_name") and original_filename:
|
|
|
|
|
return original_filename
|
|
|
|
|
|
|
|
|
|
username = track.user.username
|
|
|
|
|
title = track.title.encode("utf-8", "ignore").decode("utf-8")
|
2017-08-30 16:19:37 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("addtofile"):
|
|
|
|
|
if username not in title and "-" not in title:
|
|
|
|
|
title = "{0} - {1}".format(username, title)
|
2017-12-24 02:35:27 -08:00
|
|
|
|
logger.debug('Adding "{0}" to filename'.format(username))
|
2017-10-13 07:52:41 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
timestamp = str(int(track.created_at.timestamp()))
|
|
|
|
|
if kwargs.get("addtimestamp"):
|
|
|
|
|
title = timestamp + "_" + title
|
|
|
|
|
|
|
|
|
|
if not kwargs.get("addtofile") and not kwargs.get("addtimestamp"):
|
|
|
|
|
if playlist_info:
|
|
|
|
|
title = kwargs.get("playlist_name_format").format(**asdict(track), playlist=playlist_info, timestamp=timestamp)
|
|
|
|
|
else:
|
|
|
|
|
title = kwargs.get("name_format").format(**asdict(track), timestamp=timestamp)
|
2017-10-13 07:52:41 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
ext = ".m4a" if aac else ".mp3" # contain aac in m4a to write metadata
|
2018-01-24 05:01:15 -08:00
|
|
|
|
if original_filename is not None:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
original_filename.encode("utf-8", "ignore").decode("utf8")
|
2018-01-24 05:01:15 -08:00
|
|
|
|
ext = os.path.splitext(original_filename)[1]
|
2021-11-17 15:17:37 -08:00
|
|
|
|
# get filename to 255 bytes
|
|
|
|
|
while len(title.encode("utf-8")) > 255 - len(ext.encode("utf-8")):
|
|
|
|
|
title = title[:-1]
|
|
|
|
|
filename = title + ext.lower()
|
|
|
|
|
filename = sanitize_filename(filename)
|
2017-11-21 08:38:14 -08:00
|
|
|
|
return filename
|
2017-01-27 08:01:31 -08:00
|
|
|
|
|
2015-01-05 14:22:14 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def download_original_file(client: SoundCloud, track: BasicTrack, title: str, playlist_info=None, **kwargs):
|
|
|
|
|
logger.info("Downloading the original file.")
|
2018-01-24 05:01:15 -08:00
|
|
|
|
|
2019-11-27 07:53:53 -08:00
|
|
|
|
# Get the requests stream
|
2021-11-17 15:17:37 -08:00
|
|
|
|
url = client.get_track_original_download(track.id)
|
|
|
|
|
|
|
|
|
|
if not url:
|
|
|
|
|
logger.info("Could not get original download link")
|
|
|
|
|
return (None, False)
|
|
|
|
|
|
|
|
|
|
r = requests.get(url, stream=True)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
if r.status_code == 401:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.info("The original file has no download left.")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
return (None, False)
|
2020-03-30 10:45:34 -07:00
|
|
|
|
|
2019-12-19 03:32:31 -08:00
|
|
|
|
if r.status_code == 404:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.info("Could not get name from stream - using basic name")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
return (None, False)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
# Find filename
|
2021-11-17 15:17:37 -08:00
|
|
|
|
d = r.headers.get("content-disposition")
|
2020-04-08 04:30:19 -07:00
|
|
|
|
filename = re.findall("filename=(.+)", d)[0]
|
2021-11-17 15:17:37 -08:00
|
|
|
|
filename, ext = os.path.splitext(filename)
|
|
|
|
|
|
|
|
|
|
# Find file extension
|
|
|
|
|
mime = r.headers.get("content-type")
|
|
|
|
|
ext = mimetypes.guess_extension(mime) or ext
|
|
|
|
|
filename += ext
|
|
|
|
|
|
|
|
|
|
filename = get_filename(track, filename, playlist_info=playlist_info, **kwargs)
|
|
|
|
|
logger.debug(f"filename : {filename}")
|
2014-11-16 09:19:42 -08:00
|
|
|
|
|
2017-12-24 02:35:27 -08:00
|
|
|
|
# Skip if file ID or filename already exists
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if already_downloaded(track, title, filename, **kwargs):
|
|
|
|
|
if kwargs.get("flac") and can_convert(filename):
|
2020-06-10 10:27:55 -07:00
|
|
|
|
filename = filename[:-4] + ".flac"
|
2020-06-08 11:51:21 -07:00
|
|
|
|
return (filename, True)
|
2016-07-20 04:01:22 -07:00
|
|
|
|
|
2019-11-27 07:53:53 -08:00
|
|
|
|
# Write file
|
2021-11-17 15:17:37 -08:00
|
|
|
|
total_length = int(r.headers.get("content-length"))
|
2019-11-27 07:53:53 -08:00
|
|
|
|
temp = tempfile.NamedTemporaryFile(delete=False)
|
2019-12-07 07:59:31 -08:00
|
|
|
|
received = 0
|
2017-12-24 02:35:27 -08:00
|
|
|
|
with temp as f:
|
|
|
|
|
for chunk in progress.bar(
|
2021-11-17 15:17:37 -08:00
|
|
|
|
r.iter_content(chunk_size=1024),
|
|
|
|
|
expected_size=(total_length / 1024) + 1,
|
|
|
|
|
hide=True if kwargs.get("hide_progress") else False,
|
2017-12-24 02:35:27 -08:00
|
|
|
|
):
|
|
|
|
|
if chunk:
|
2019-12-07 07:59:31 -08:00
|
|
|
|
received += len(chunk)
|
2017-12-24 02:35:27 -08:00
|
|
|
|
f.write(chunk)
|
|
|
|
|
f.flush()
|
|
|
|
|
|
2018-01-24 04:07:45 -08:00
|
|
|
|
if received != total_length:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("connection closed prematurely, download incomplete")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
2018-01-24 04:07:45 -08:00
|
|
|
|
|
2017-12-24 02:35:27 -08:00
|
|
|
|
shutil.move(temp.name, os.path.join(os.getcwd(), filename))
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("flac") and can_convert(filename):
|
|
|
|
|
logger.info("Converting to .flac...")
|
2018-04-10 04:10:08 -07:00
|
|
|
|
newfilename = filename[:-4] + ".flac"
|
2020-03-30 10:45:34 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
commands = ["ffmpeg", "-i", filename, newfilename, "-loglevel", "error"]
|
|
|
|
|
logger.debug(f"Commands: {commands}")
|
2020-01-21 01:35:31 -08:00
|
|
|
|
subprocess.call(commands)
|
2018-04-10 06:06:51 -07:00
|
|
|
|
os.remove(filename)
|
2018-04-10 04:10:08 -07:00
|
|
|
|
filename = newfilename
|
|
|
|
|
|
2020-06-08 11:51:21 -07:00
|
|
|
|
return (filename, False)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def get_track_m3u8(client: SoundCloud, track: BasicTrack, aac=False):
|
2019-11-27 07:53:53 -08:00
|
|
|
|
url = None
|
2021-11-17 15:17:37 -08:00
|
|
|
|
for transcoding in track.media.transcodings:
|
|
|
|
|
if transcoding.format.protocol == "hls":
|
|
|
|
|
if (not aac and transcoding.format.mime_type == "audio/mpeg") or (
|
|
|
|
|
aac and transcoding.format.mime_type.startswith("audio/mp4")
|
|
|
|
|
):
|
|
|
|
|
url = transcoding.url
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
if url is not None:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
headers = client.get_default_headers()
|
|
|
|
|
if client.auth_token:
|
|
|
|
|
headers["Authorization"] = f"OAuth {client.auth_token}"
|
|
|
|
|
r = requests.get(url, params={"client_id": client.client_id}, headers=headers)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
logger.debug(r.url)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
return r.json()["url"]
|
|
|
|
|
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def download_hls(client: SoundCloud, track: BasicTrack, title: str, playlist_info=None, **kwargs):
|
|
|
|
|
|
|
|
|
|
if kwargs["onlymp3"]:
|
|
|
|
|
aac = False
|
|
|
|
|
else:
|
|
|
|
|
aac = any(
|
|
|
|
|
t.format.mime_type.startswith("audio/mp4")
|
|
|
|
|
for t in track.media.transcodings
|
|
|
|
|
)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
filename = get_filename(track, None, aac, playlist_info, **kwargs)
|
|
|
|
|
logger.debug(f"filename : {filename}")
|
2019-11-27 07:53:53 -08:00
|
|
|
|
# Skip if file ID or filename already exists
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if already_downloaded(track, title, filename, **kwargs):
|
2020-06-08 11:51:21 -07:00
|
|
|
|
return (filename, True)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
# Get the requests stream
|
2021-11-17 15:17:37 -08:00
|
|
|
|
url = get_track_m3u8(client, track, aac)
|
2020-01-21 01:35:31 -08:00
|
|
|
|
filename_path = os.path.abspath(filename)
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
p = subprocess.run(
|
|
|
|
|
["ffmpeg", "-i", url, "-c", "copy", filename_path, "-loglevel", "error"],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
)
|
|
|
|
|
if p.stderr:
|
|
|
|
|
logger.error(p.stderr.decode("utf-8"))
|
2020-06-08 11:51:21 -07:00
|
|
|
|
return (filename, False)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def download_track(client: SoundCloud, track: BasicTrack, playlist_info=None, **kwargs):
|
2019-11-27 07:53:53 -08:00
|
|
|
|
"""
|
|
|
|
|
Downloads a track
|
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
title = track.title
|
|
|
|
|
title = title.encode("utf-8", "ignore").decode("utf8")
|
|
|
|
|
logger.info(f"Downloading {title}")
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
# Not streamable
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not track.streamable:
|
|
|
|
|
logger.error(f"{title} is not streamable...")
|
2019-11-27 07:53:53 -08:00
|
|
|
|
return
|
|
|
|
|
|
2020-05-01 07:10:10 -07:00
|
|
|
|
# Geoblocked track
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if track.policy == "BLOCK":
|
|
|
|
|
logger.error(f"{title} is not available in your location...\n")
|
2020-05-01 07:10:10 -07:00
|
|
|
|
return
|
|
|
|
|
|
2019-11-27 07:53:53 -08:00
|
|
|
|
# Downloadable track
|
|
|
|
|
filename = None
|
2020-06-08 11:51:21 -07:00
|
|
|
|
is_already_downloaded = False
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if (
|
|
|
|
|
track.downloadable
|
|
|
|
|
and track.has_downloads_left
|
|
|
|
|
and not kwargs["onlymp3"]
|
|
|
|
|
and not kwargs.get("no_original")
|
|
|
|
|
):
|
|
|
|
|
filename, is_already_downloaded = download_original_file(client, track, title, playlist_info, **kwargs)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
|
|
|
|
if filename is None:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("only_original"):
|
|
|
|
|
logger.info(f'Track "{title}" does not have original file available. Skipping...')
|
|
|
|
|
return
|
|
|
|
|
filename, is_already_downloaded = download_hls(client, track, title, playlist_info, **kwargs)
|
2019-11-27 07:53:53 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("remove"):
|
2019-11-27 07:53:53 -08:00
|
|
|
|
fileToKeep.append(filename)
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
record_download_archive(track, **kwargs)
|
2020-06-08 11:51:21 -07:00
|
|
|
|
|
2020-06-08 09:42:08 -07:00
|
|
|
|
# Skip if file ID or filename already exists
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if is_already_downloaded and not kwargs.get("force_metadata"):
|
|
|
|
|
logger.info(f'Track "{title}" already downloaded.')
|
2020-06-08 09:42:08 -07:00
|
|
|
|
return
|
|
|
|
|
|
2020-12-28 04:57:11 -08:00
|
|
|
|
# If file does not exist an error occurred
|
2020-06-08 11:51:21 -07:00
|
|
|
|
if not os.path.isfile(filename):
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error(f"An error occurred downloading {filename}.\n")
|
|
|
|
|
logger.error("Exiting...")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
|
|
# Try to set the metadata
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if (
|
|
|
|
|
filename.endswith(".mp3")
|
|
|
|
|
or filename.endswith(".flac")
|
|
|
|
|
or filename.endswith(".m4a")
|
|
|
|
|
):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
set_metadata(track, filename, playlist_info, **kwargs)
|
|
|
|
|
except:
|
|
|
|
|
logger.exception("Error trying to set the tags...")
|
2017-12-24 02:35:27 -08:00
|
|
|
|
else:
|
|
|
|
|
logger.error("This type of audio doesn't support tagging...")
|
2017-08-30 16:19:37 -07:00
|
|
|
|
|
2017-12-26 03:05:15 -08:00
|
|
|
|
# Try to change the real creation date
|
2021-11-17 15:17:37 -08:00
|
|
|
|
filetime = int(time.mktime(track.created_at.timetuple()))
|
2017-12-26 03:05:15 -08:00
|
|
|
|
try_utime(filename, filetime)
|
2017-08-30 16:19:37 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.info(f"{filename} Downloaded.\n")
|
2017-12-24 02:35:27 -08:00
|
|
|
|
|
|
|
|
|
|
2018-04-11 09:27:32 -07:00
|
|
|
|
def can_convert(filename):
|
|
|
|
|
ext = os.path.splitext(filename)[1]
|
2021-11-17 15:17:37 -08:00
|
|
|
|
return "wav" in ext or "aif" in ext
|
2018-04-11 09:27:32 -07:00
|
|
|
|
|
2019-12-07 07:59:31 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def already_downloaded(track: BasicTrack, title: str, filename: str, **kwargs):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
"""
|
|
|
|
|
Returns True if the file has already been downloaded
|
|
|
|
|
"""
|
|
|
|
|
already_downloaded = False
|
|
|
|
|
|
2018-04-11 09:27:32 -07:00
|
|
|
|
if os.path.isfile(filename):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
already_downloaded = True
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("overwrite"):
|
|
|
|
|
os.remove(filename)
|
|
|
|
|
already_downloaded = False
|
|
|
|
|
if (
|
|
|
|
|
kwargs.get("flac")
|
|
|
|
|
and can_convert(filename)
|
|
|
|
|
and os.path.isfile(filename[:-4] + ".flac")
|
|
|
|
|
):
|
2018-04-11 08:30:55 -07:00
|
|
|
|
already_downloaded = True
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("overwrite"):
|
|
|
|
|
os.remove(filename[:-4] + ".flac")
|
|
|
|
|
already_downloaded = False
|
|
|
|
|
if kwargs.get("download_archive") and in_download_archive(track, **kwargs):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
already_downloaded = True
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("flac") and can_convert(filename) and os.path.isfile(filename):
|
2018-04-11 09:27:32 -07:00
|
|
|
|
already_downloaded = False
|
|
|
|
|
|
2017-12-24 02:35:27 -08:00
|
|
|
|
if already_downloaded:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if kwargs.get("c") or kwargs.get("remove") or kwargs.get("force_metadata"):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
return True
|
2014-11-16 09:19:42 -08:00
|
|
|
|
else:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error(f'Track "{title}" already exists!')
|
|
|
|
|
logger.error("Exiting... (run again with -c to continue)")
|
2020-06-08 11:51:21 -07:00
|
|
|
|
sys.exit(-1)
|
2017-12-24 02:35:27 -08:00
|
|
|
|
return False
|
2014-11-16 09:19:42 -08:00
|
|
|
|
|
2017-12-24 02:35:27 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def in_download_archive(track: BasicTrack, **kwargs):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
"""
|
|
|
|
|
Returns True if a track_id exists in the download archive
|
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not kwargs.get("download_archive"):
|
2017-12-26 03:05:15 -08:00
|
|
|
|
return
|
2017-12-24 02:35:27 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
archive_filename = kwargs.get("download_archive")
|
2017-12-24 02:35:27 -08:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
with open(archive_filename, "a+", encoding="utf-8") as file:
|
2017-12-24 02:35:27 -08:00
|
|
|
|
file.seek(0)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
track_id = str(track.id)
|
2017-12-24 02:35:27 -08:00
|
|
|
|
for line in file:
|
|
|
|
|
if line.strip() == track_id:
|
|
|
|
|
return True
|
|
|
|
|
except IOError as ioe:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Error trying to read download archive...")
|
|
|
|
|
logger.error(ioe)
|
2017-12-24 02:35:27 -08:00
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def record_download_archive(track: BasicTrack, **kwargs):
|
2017-12-24 02:35:27 -08:00
|
|
|
|
"""
|
|
|
|
|
Write the track_id in the download archive
|
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not kwargs.get("download_archive"):
|
2017-12-26 03:05:15 -08:00
|
|
|
|
return
|
2017-12-24 02:35:27 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
archive_filename = kwargs.get("download_archive")
|
2017-12-24 02:35:27 -08:00
|
|
|
|
try:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
with open(archive_filename, "a", encoding="utf-8") as file:
|
|
|
|
|
file.write(f"{track.id}\n")
|
2017-12-24 02:35:27 -08:00
|
|
|
|
except IOError as ioe:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.error("Error trying to write to download archive...")
|
|
|
|
|
logger.error(ioe)
|
2014-11-16 09:19:42 -08:00
|
|
|
|
|
2014-10-20 13:23:46 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
def set_metadata(track: BasicTrack, filename: str, playlist_info=None, **kwargs):
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
Sets the mp3 file metadata using the Python module Mutagen
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.info("Setting tags...")
|
|
|
|
|
artwork_url = track.artwork_url
|
|
|
|
|
user = track.user
|
2016-02-08 05:32:59 -08:00
|
|
|
|
if not artwork_url:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
artwork_url = user.avatar_url
|
|
|
|
|
response = None
|
|
|
|
|
if kwargs.get("original_art"):
|
|
|
|
|
new_artwork_url = artwork_url.replace("large", "original")
|
|
|
|
|
try:
|
|
|
|
|
response = requests.get(new_artwork_url, stream=True)
|
|
|
|
|
if response.headers["Content-Type"] not in (
|
|
|
|
|
"image/png",
|
|
|
|
|
"image/jpeg",
|
|
|
|
|
"image/jpg",
|
|
|
|
|
):
|
|
|
|
|
response = None
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
if response is None:
|
|
|
|
|
new_artwork_url = artwork_url.replace("large", "t500x500")
|
|
|
|
|
response = requests.get(new_artwork_url, stream=True)
|
|
|
|
|
if response.headers["Content-Type"] not in (
|
|
|
|
|
"image/png",
|
|
|
|
|
"image/jpeg",
|
|
|
|
|
"image/jpg",
|
|
|
|
|
):
|
|
|
|
|
response = None
|
|
|
|
|
if response is None:
|
|
|
|
|
logger.error(f"Could not get cover art at {new_artwork_url}")
|
2016-03-01 14:12:34 -08:00
|
|
|
|
with tempfile.NamedTemporaryFile() as out_file:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if response:
|
|
|
|
|
shutil.copyfileobj(response.raw, out_file)
|
|
|
|
|
out_file.seek(0)
|
|
|
|
|
|
|
|
|
|
track.date = track.created_at.strftime("%Y-%m-%d %H::%M::%S")
|
|
|
|
|
|
|
|
|
|
track.artist = user.username
|
|
|
|
|
if kwargs.get("extract_artist"):
|
|
|
|
|
for dash in [" - ", " − ", " – ", " — ", " ― "]:
|
|
|
|
|
if dash in track.title:
|
|
|
|
|
artist_title = track.title.split(dash)
|
|
|
|
|
track.artist = artist_title[0].strip()
|
|
|
|
|
track.title = artist_title[1].strip()
|
2018-02-09 06:49:12 -08:00
|
|
|
|
break
|
2018-01-08 05:54:04 -08:00
|
|
|
|
|
2018-04-10 06:06:51 -07:00
|
|
|
|
audio = mutagen.File(filename, easy=True)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
audio.delete()
|
|
|
|
|
audio["title"] = track.title
|
|
|
|
|
audio["artist"] = track.artist
|
|
|
|
|
if track.genre:
|
|
|
|
|
audio["genre"] = track.genre
|
|
|
|
|
if track.permalink_url:
|
|
|
|
|
audio["website"] = track.permalink_url
|
|
|
|
|
if track.date:
|
|
|
|
|
audio["date"] = track.date
|
2020-04-27 05:13:29 -07:00
|
|
|
|
if playlist_info:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if not kwargs.get("no_album_tag"):
|
|
|
|
|
audio["album"] = playlist_info["title"]
|
|
|
|
|
audio["tracknumber"] = str(playlist_info["tracknumber"])
|
2020-04-27 05:13:29 -07:00
|
|
|
|
|
2018-04-11 08:30:55 -07:00
|
|
|
|
audio.save()
|
|
|
|
|
|
|
|
|
|
a = mutagen.File(filename)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if track.description:
|
2018-04-11 09:38:04 -07:00
|
|
|
|
if a.__class__ == mutagen.flac.FLAC:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
a["description"] = track.description
|
2018-04-11 09:38:04 -07:00
|
|
|
|
elif a.__class__ == mutagen.mp3.MP3:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
a["COMM"] = mutagen.id3.COMM(
|
|
|
|
|
encoding=3, lang="ENG", text=track.description
|
2018-04-10 06:06:51 -07:00
|
|
|
|
)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
elif a.__class__ == mutagen.mp4.MP4:
|
|
|
|
|
a["\xa9cmt"] = track.description
|
|
|
|
|
if response:
|
2018-04-11 09:38:04 -07:00
|
|
|
|
if a.__class__ == mutagen.flac.FLAC:
|
2018-04-11 08:30:55 -07:00
|
|
|
|
p = mutagen.flac.Picture()
|
|
|
|
|
p.data = out_file.read()
|
2021-11-17 15:17:37 -08:00
|
|
|
|
p.mime = "image/jpeg"
|
2018-04-11 08:30:55 -07:00
|
|
|
|
p.type = mutagen.id3.PictureType.COVER_FRONT
|
|
|
|
|
a.add_picture(p)
|
2018-04-11 09:38:04 -07:00
|
|
|
|
elif a.__class__ == mutagen.mp3.MP3:
|
2021-11-17 15:17:37 -08:00
|
|
|
|
a["APIC"] = mutagen.id3.APIC(
|
|
|
|
|
encoding=3,
|
|
|
|
|
mime="image/jpeg",
|
|
|
|
|
type=3,
|
|
|
|
|
desc="Cover",
|
|
|
|
|
data=out_file.read(),
|
2016-03-01 14:12:34 -08:00
|
|
|
|
)
|
2021-11-17 15:17:37 -08:00
|
|
|
|
elif a.__class__ == mutagen.mp4.MP4:
|
|
|
|
|
a["covr"] = [mutagen.mp4.MP4Cover(out_file.read())]
|
2018-04-11 08:30:55 -07:00
|
|
|
|
a.save()
|
2014-11-16 09:19:42 -08:00
|
|
|
|
|
2014-10-23 07:14:29 -07:00
|
|
|
|
|
2014-10-20 13:23:46 -07:00
|
|
|
|
def signal_handler(signal, frame):
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2017-12-10 06:43:07 -08:00
|
|
|
|
Handle keyboard interrupt
|
2014-11-16 09:19:42 -08:00
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
logger.info("\nGood bye!")
|
2014-11-16 09:19:42 -08:00
|
|
|
|
sys.exit(0)
|
2014-10-12 15:16:18 -07:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
|
2020-09-28 06:41:41 -07:00
|
|
|
|
def is_ffmpeg_available():
|
|
|
|
|
"""
|
|
|
|
|
Returns true if ffmpeg is available in the operating system
|
|
|
|
|
"""
|
2021-11-17 15:17:37 -08:00
|
|
|
|
return shutil.which("ffmpeg") is not None
|
|
|
|
|
|
2019-12-07 07:59:31 -08:00
|
|
|
|
|
2021-11-17 15:17:37 -08:00
|
|
|
|
if __name__ == "__main__":
|
2014-11-16 09:19:42 -08:00
|
|
|
|
main()
|