Merge pull request #28 from kepoorhampond/random-unicode-character-errors

fixed random unicode character error and removed the need to install ffmpeg.
This commit is contained in:
Kepoor Hampond 2017-06-10 18:16:43 -07:00 committed by GitHub
commit 573bc7c656
12 changed files with 139 additions and 55 deletions

3
.gitignore vendored
View file

@ -4,6 +4,7 @@
/*.egg-info/ /*.egg-info/
/build/ /build/
__pycache__/ __pycache__/
.eggs
# For easy updating of stuff. # For easy updating of stuff.
update_pypi_and_github.py update_pypi_and_github.py
@ -21,4 +22,4 @@ update_pypi_and_github.py
.coverage .coverage
# Temporarily built binaries # Temporarily built binaries
ffmpeg binaries/ ffmpeg binaries/

View file

@ -4,13 +4,8 @@ python:
- "3.5" - "3.5"
- "3.6" - "3.6"
before_script:
- sudo aptitude -y -q install ffmpeg libavcodec-extra-53 lame libmp3lame0
# These dependencies are necessary for ffmpeg. I currently hate all things
# doing with Travis and ffmpeg because I have no direct access to test stuff.
# I've gone through 25 seperate commits to get it working.
install: install:
- pip install -r requirements.txt
- python setup.py install - python setup.py install
script: script:

View file

@ -1,7 +1,6 @@
<div align="center"><img src ="http://i.imgur.com/VbsyTe7.png" /></div> <div align="center"><img src ="http://i.imgur.com/VbsyTe7.png" /></div>
Ironic Redistribution System # Ironic Redistribution System
===
[![License: GNU](https://img.shields.io/badge/license-gnu-yellow.svg?style=flat-square)](http://www.gnu.org/licenses/gpl.html) [![License: GNU](https://img.shields.io/badge/license-gnu-yellow.svg?style=flat-square)](http://www.gnu.org/licenses/gpl.html)
[![Stars](https://img.shields.io/github/stars/kepoorhampond/irs.svg?style=flat-square)](https://github.com/kepoorhampond/irs/stargazers) [![Stars](https://img.shields.io/github/stars/kepoorhampond/irs.svg?style=flat-square)](https://github.com/kepoorhampond/irs/stargazers)
@ -14,10 +13,9 @@ Ironic Redistribution System
> A music downloader that understands your metadata needs. > A music downloader that understands your metadata needs.
A tool to download your music with metadata. It uses [Spotify](https://www.spotify.com/) for finding metadata and [Youtube](https://www.youtube.com/) for the actual audio source. A tool to download your music with metadata. It uses [Spotify](https://www.spotify.com/) for finding metadata and [Youtube](https://www.youtube.com/) for the actual audio source. You will need to have some Spotify tokens, the instructions to set them up are [here](https://github.com/kepoorhampond/irs#spotify-tokens).
Works with Python 2 and 3. Works with Python 2 and 3.
___
## Demo and Usages ## Demo and Usages
@ -59,7 +57,7 @@ $ irs -s "Bohemian Rhapsody"
$ irs -p "Best Nirvana" $ irs -p "Best Nirvana"
``` ```
## Install & The Dependencies <sub><sup>(my new band name)</sub></sup> ## Install & The Dependencies <sub><sup>(my new band name <sub><sup><sup><sub>\s</sub></sup></sup></sub>)</sub></sup>
Really there's only one actual external dependency: `ffmpeg`. For windows, you'll want to follow [this](http://www.wikihow.com/Install-FFmpeg-on-Windows) guide. For OSX, you'll want to install it through [`brew`](https://brew.sh/) with this command: Really there's only one actual external dependency: `ffmpeg`. For windows, you'll want to follow [this](http://www.wikihow.com/Install-FFmpeg-on-Windows) guide. For OSX, you'll want to install it through [`brew`](https://brew.sh/) with this command:
``` ```
@ -76,9 +74,11 @@ Other than `ffmpeg` though, all other dependencies are automatically installed w
$ sudo pip install irs $ sudo pip install irs
``` ```
## Spotify Playlists **You will need to have some Spotify tokens, the instructions to set them up are [here](https://github.com/kepoorhampond/irs#spotify-tokens).**
To download spotify playlists, you'll want to head to their Dev Apps page, [here](https://developer.spotify.com/my-applications/). After doing that you'll want to create a new app. Name it whatever you want and then once you've done that, find the `Client ID` and `Client Secret` keys. You'll want to take those keys and paste them into your system's environment variables as `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, correspondingly. Viola! You can now download playlists! ## Spotify Tokens
To download metadata through spotify, you'll want to head to their Dev Apps page, [here](https://developer.spotify.com/my-applications/). After doing that you'll want to create a new app. Name it whatever you want and then once you've done that, find the `Client ID` and `Client Secret` keys. You'll want to take those keys and paste them into your system's environment variables as `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, correspondingly. Viola! You can now download metadata with IRS!
## Metadata ## Metadata

View file

@ -53,7 +53,7 @@ directory to place files in.")
} }
} }
# Combiner args from argparse and the ripper_args as above and then # Combine args from argparse and the ripper_args as above and then
# remove all keys with the value of "None" # remove all keys with the value of "None"
ripper_args.update(vars(args)) ripper_args.update(vars(args))

View file

@ -1,24 +1,9 @@
CONFIG = dict( import sys
from os import path
default_flags = ['-o'],
# For default flags. Right now, it organizes your files into an
# artist/album/song structure.
# To add a flag or argument, add an element to the index:
# default_flags = ['-o', '-l', '~/Music']
SPOTIFY_CLIENT_ID = '', sys.path.append(path.expanduser("~/.irs")) # Add config to path
SPOTIFY_CLIENT_SECRET = '',
# You can either specify Spotify keys here, or in environment variables.
additional_search_terms = 'lyrics', import config_ # from "~/.irs/config_.py"
# Search terms for youtube
organize = True, CONFIG = config_.CONFIG
# True always forces organization.
# False always forces non-organization.
# None allows options and flags to determine if the files
# will be organized.
custom_directory = "",
# Defaults to '~/Music'
)

24
irs/config_preset Normal file
View file

@ -0,0 +1,24 @@
CONFIG = dict(
default_flags = ['-o'],
# For default flags. Right now, it organizes your files into an
# artist/album/song structure.
# To add a flag or argument, add an element to the index:
# default_flags = ['-o', '-l', '~/Music']
SPOTIFY_CLIENT_ID = '',
SPOTIFY_CLIENT_SECRET = '',
# You can either specify Spotify keys here, or in environment variables.
additional_search_terms = 'lyrics',
# Search terms for youtube
organize = True,
# True always forces organization.
# False always forces non-organization.
# None allows options and flags to determine if the files
# will be organized.
custom_directory = "",
# When blank, defaults to '~/Music'
)

View file

@ -56,9 +56,9 @@ class Metadata:
mp3.save() mp3.save()
def find_album_and_track(song, artist): def find_album_and_track(song, artist, spotify=spotipy.Spotify()):
tracks = spotipy.Spotify().search(q=song, type="track" tracks = spotify.search(q=song, type="track")["tracks"]["items"]
)["tracks"]["items"]
for track in tracks: for track in tracks:
if om.blank_include(track["name"], song): if om.blank_include(track["name"], song):
if om.blank_include(track["artists"][0]["name"], artist): if om.blank_include(track["artists"][0]["name"], artist):

View file

@ -1,14 +1,19 @@
# Powered by:
import youtube_dl
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
# System # System
import sys import sys
import os import os
import glob import glob
import shutil import shutil
# Add youtube-dl binary to path
sys.path.append(os.path.expanduser("~/.irs/bin/youtube-dl"))
# Powered by:
import youtube_dl # Locally imported from the binary
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
# Local utilities # Local utilities
from .utils import YdlUtils, ObjManip, Config from .utils import YdlUtils, ObjManip, Config
from .metadata import Metadata from .metadata import Metadata
@ -80,7 +85,7 @@ class Ripper:
# those flaws for being exclusive to them. # those flaws for being exclusive to them.
# And if those flaws are really enough to turn you off of them, # And if those flaws are really enough to turn you off of them,
# then you *probably* don't really want to be with them anyways. # then you *probably* don't really want to be with them anyways.
# Either way, it's up to you. # Either way, it's up to you. (I'd just ignore this)
if Config.parse_organize(self): if Config.parse_organize(self):
if self.type in ("album", "song"): if self.type in ("album", "song"):
@ -125,7 +130,7 @@ class Ripper:
def find_yt_url(self, song=None, artist=None, additional_search=None): def find_yt_url(self, song=None, artist=None, additional_search=None):
if additional_search is None: if additional_search is None:
additional_search = Config.parse_search_terms(self) additional_search = Config.parse_search_terms(self)
print(self.args["hook-text"].get("youtube")) print(str(self.args["hook-text"].get("youtube")))
try: try:
if not song: if not song:
@ -136,7 +141,8 @@ class Ripper:
raise ValueError("Must specify song_title/artist in `args` with \ raise ValueError("Must specify song_title/artist in `args` with \
init, or in method arguments.") init, or in method arguments.")
search_terms = song + " " + artist + " " + additional_search search_terms = str(song) + " " + str(artist
) + " " + str(additional_search)
query_string = urlencode({"search_query": ( query_string = urlencode({"search_query": (
search_terms.encode('utf-8'))}) search_terms.encode('utf-8'))})
link = "http://www.youtube.com/results?" + query_string link = "http://www.youtube.com/results?" + query_string
@ -194,7 +200,7 @@ album".split(" ")
return ("https://youtube.com" + self.code["href"], self.code["title"]) return ("https://youtube.com" + self.code["href"], self.code["title"])
def album(self, title, artist=None): # Alias for `spotify_list("album", ...)` def album(self, title, artist=None): # Alias for spotify_list("album", ..)
return self.spotify_list("album", title=title, artist=artist) return self.spotify_list("album", title=title, artist=artist)
def playlist(self, title, username): def playlist(self, title, username):
@ -321,7 +327,7 @@ with init, or in method arguments.")
return locations return locations
def parse_song_data(self, song, artist): def parse_song_data(self, song, artist):
album, track = find_album_and_track(song, artist) album, track = find_album_and_track(song, artist, self.spotify)
if album is False: if album is False:
return {} return {}
@ -372,8 +378,7 @@ init, or in method arguments.")
print(self.args["hook-text"].get("song").format(song, artist)) print(self.args["hook-text"].get("song").format(song, artist))
file_name = str(data["file_prefix"] + ObjManip.blank(song, False) + file_name = data["file_prefix"] + ObjManip.blank(song, False) + ".mp3"
".mp3")
ydl_opts = { ydl_opts = {
'format': 'bestaudio/best', 'format': 'bestaudio/best',
@ -386,6 +391,7 @@ init, or in method arguments.")
'progress_hooks': [YdlUtils.my_hook], 'progress_hooks': [YdlUtils.my_hook],
'output': "tmp_file", 'output': "tmp_file",
'prefer-ffmpeg': True, 'prefer-ffmpeg': True,
'ffmpeg_location': os.path.expanduser("~/.irs/bin/")
} }
with youtube_dl.YoutubeDL(ydl_opts) as ydl: with youtube_dl.YoutubeDL(ydl_opts) as ydl:

View file

@ -15,7 +15,11 @@ from time import sleep
import pkg_resources import pkg_resources
# Config File and Flags # Config File and Flags
from .config import CONFIG if sys.version_info[0] == 2:
import config
CONFIG = config.CONFIG
else:
from irs.config import CONFIG
# ================== # ==================
@ -56,6 +60,22 @@ class YdlUtils:
# Object Manipulation and Checking # Object Manipulation and Checking
# ================================ # ================================
def set_encoding(ld, encoding): # ld => list or dictionary with strings in it
if type(ld) == dict:
for k in ld:
if type(ld[k]) == dict or type(ld[k]) == list:
ld[k] = set_encoding(ld[k], encoding)
elif type(ld[k]) == str:
ld[k] = encoding(ld[k])
elif type(ld) == list:
for index, datum in enumerate(ld):
if type(datum) == str:
ld[index] = encoding(datum)
elif type(ld[k]) == dict or type(ld[k]) == list:
ld[k] = set_encoding(ld[k], encoding)
return ld
@staticmethods @staticmethods
class ObjManip: # Object Manipulation class ObjManip: # Object Manipulation
def limit_song_name(song): def limit_song_name(song):
@ -77,7 +97,7 @@ class ObjManip: # Object Manipulation
def blank(string, downcase=True): def blank(string, downcase=True):
import re import re
regex = re.compile('[^a-zA-Z0-9\ ]') regex = re.compile('[^a-zA-Z0-9\ ]')
string = regex.sub('', string) string = regex.sub('', str(string))
if downcase: if downcase:
string = string.lower() string = string.lower()
return ' '.join(string.split()) return ' '.join(string.split())
@ -118,11 +138,18 @@ class ObjManip: # Object Manipulation
del new_d[x] del new_d[x]
return new_d return new_d
# ld => a list or dictionary with strings in it
def set_utf8_encoding(ld):
return set_encoding(ld, lambda x: x.encode('utf-8'))
def set_encoding(*args):
return set_encoding(*args)
# ======================================== # ========================================
# Download Log Reading/Updating/Formatting # Download Log Reading/Updating/Formatting
# ======================================== # ========================================
@staticmethods @staticmethods
class DLog: class DLog:
def format_download_log_line(t, download_status="not downloaded"): def format_download_log_line(t, download_status="not downloaded"):

7
requirements.txt Normal file
View file

@ -0,0 +1,7 @@
--index-url https://pypi.python.org/simple/
bs4
mutagen
requests
spotipy
ydl-binaries

View file

@ -1,4 +1,39 @@
from setuptools import setup from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
class PostDevelopCommand(develop):
"""Post-installation for development mode."""
def run(self):
# PUT YOUR PRE-INSTALL SCRIPT HERE or CALL A FUNCTION
develop.run(self)
# PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION
class PostInstallCommand(install):
"""Post-installation for installation mode."""
def run(self):
install.run(self) # Actually install the module and dependencies
try:
import ydl_binaries
except ImportError:
import pip
pip.main(['install', "ydl-binaries"])
import ydl_binaries
from os import path
from shutil import copyfile
print("\n\nDownloading Dependencies:\n")
ydl_binaries.download_ffmpeg("~/.irs/bin")
ydl_binaries.update_ydl("~/.irs/bin")
config_file = path.expanduser("~/.irs/config_.py")
if not path.isfile(config_file):
copyfile("irs/config_preset", config_file)
setup( setup(
name = 'irs', name = 'irs',
@ -9,14 +44,18 @@ setup(
author_email = 'kepoorh@gmail.com', author_email = 'kepoorh@gmail.com',
license = 'GPL', license = 'GPL',
packages = ['irs'], packages = ['irs'],
install_requires=[ install_requires = [
'bs4', 'bs4',
'mutagen', 'mutagen',
'youtube-dl',
'requests', 'requests',
'spotipy', 'spotipy',
'ydl-binaries'
], ],
entry_points = { entry_points = {
'console_scripts': ['irs = irs.cli:main'], 'console_scripts': ['irs = irs.cli:main'],
}, },
cmdclass = {
'develop': PostDevelopCommand,
'install': PostInstallCommand,
},
) )

View file

@ -2,4 +2,4 @@ from irs.ripper import Ripper
print ("[*] Testing `song.py`") print ("[*] Testing `song.py`")
Ripper().song("Da Frame 2R", "Arctic Monkeys") Ripper().song("Da Frame 2R", "Arctic Monkeys")
print ("[+] Passed!") print ("[+] Passed!")