mirror of
https://github.com/rembo10/headphones.git
synced 2026-04-21 12:29:29 +01:00
pep8, pyflakes, and pylint suggested fixes
This commit is contained in:
@@ -16,21 +16,21 @@
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of
|
||||
# XBian - XBMC on the Raspberry Pi
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import webbrowser
|
||||
import sqlite3
|
||||
import cherrypy
|
||||
import datetime
|
||||
|
||||
import os
|
||||
import cherrypy
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from headphones import versioncheck, logger
|
||||
import headphones.config
|
||||
|
||||
|
||||
# (append new extras to the end)
|
||||
POSSIBLE_EXTRAS = [
|
||||
"single",
|
||||
@@ -94,7 +94,6 @@ UMASK = None
|
||||
|
||||
|
||||
def initialize(config_file):
|
||||
|
||||
with INIT_LOCK:
|
||||
|
||||
global CONFIG
|
||||
@@ -131,11 +130,11 @@ def initialize(config_file):
|
||||
|
||||
if not QUIET:
|
||||
sys.stderr.write("Unable to create the log directory. " \
|
||||
"Logging to screen only.\n")
|
||||
"Logging to screen only.\n")
|
||||
|
||||
# Start the logger, disable console if needed
|
||||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
verbose=VERBOSE)
|
||||
verbose=VERBOSE)
|
||||
|
||||
if not CONFIG.CACHE_DIR:
|
||||
# Put the cache dir in the data dir for now
|
||||
@@ -246,7 +245,6 @@ def daemonize():
|
||||
|
||||
|
||||
def launch_browser(host, port, root):
|
||||
|
||||
if host == '0.0.0.0':
|
||||
host = 'localhost'
|
||||
|
||||
@@ -287,17 +285,19 @@ def initialize_scheduler():
|
||||
hours = CONFIG.UPDATE_DB_INTERVAL
|
||||
schedule_job(updater.dbUpdate, 'MusicBrainz Update', hours=hours, minutes=0)
|
||||
|
||||
#Update check
|
||||
# Update check
|
||||
if CONFIG.CHECK_GITHUB:
|
||||
if CONFIG.CHECK_GITHUB_INTERVAL:
|
||||
minutes = CONFIG.CHECK_GITHUB_INTERVAL
|
||||
else:
|
||||
minutes = 0
|
||||
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes)
|
||||
schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0,
|
||||
minutes=minutes)
|
||||
|
||||
# Remove Torrent + data if Post Processed and finished Seeding
|
||||
minutes = CONFIG.TORRENT_REMOVAL_INTERVAL
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0, minutes=minutes)
|
||||
schedule_job(torrentfinished.checkTorrentFinished, 'Torrent removal check', hours=0,
|
||||
minutes=minutes)
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
@@ -306,8 +306,8 @@ def initialize_scheduler():
|
||||
except Exception as e:
|
||||
logger.info(e)
|
||||
|
||||
# Debug
|
||||
#SCHED.print_jobs()
|
||||
# Debug
|
||||
# SCHED.print_jobs()
|
||||
|
||||
|
||||
def schedule_job(function, name, hours=0, minutes=0):
|
||||
@@ -334,7 +334,6 @@ def schedule_job(function, name, hours=0, minutes=0):
|
||||
|
||||
|
||||
def start():
|
||||
|
||||
global started
|
||||
|
||||
if _INITIALIZED:
|
||||
@@ -349,7 +348,6 @@ def sig_handler(signum=None, frame=None):
|
||||
|
||||
|
||||
def dbcheck():
|
||||
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
c = conn.cursor()
|
||||
c.execute(
|
||||
@@ -609,7 +607,6 @@ def dbcheck():
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False):
|
||||
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ def switch(AlbumID, ReleaseID):
|
||||
c.get_artwork_from_cache(AlbumID=AlbumID)
|
||||
|
||||
for track in newtrackdata:
|
||||
|
||||
controlValueDict = {"TrackID": track['TrackID'],
|
||||
"AlbumID": AlbumID}
|
||||
|
||||
@@ -79,15 +78,18 @@ def switch(AlbumID, ReleaseID):
|
||||
have_track_count = len(myDB.select(
|
||||
'SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID]))
|
||||
|
||||
if oldalbumdata['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
if oldalbumdata['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (
|
||||
headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
myDB.action(
|
||||
'UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
|
||||
|
||||
# Update have track counts on index
|
||||
totaltracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']]))
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
||||
[newalbumdata['ArtistID']]))
|
||||
havetracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']]))
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[newalbumdata['ArtistID']]))
|
||||
|
||||
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
|
||||
|
||||
|
||||
@@ -13,21 +13,25 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, logger
|
||||
|
||||
import headphones
|
||||
import json
|
||||
|
||||
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched', 'getSimilar', 'getHistory', 'getLogs',
|
||||
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
|
||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate',
|
||||
'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt',
|
||||
from headphones import db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, \
|
||||
logger
|
||||
import headphones
|
||||
|
||||
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSnatched',
|
||||
'getSimilar', 'getHistory', 'getLogs',
|
||||
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist',
|
||||
'refreshArtist',
|
||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess',
|
||||
'forceActiveArtistsUpdate',
|
||||
'getVersion', 'checkGithub', 'shutdown', 'restart', 'update', 'getArtistArt',
|
||||
'getAlbumArt',
|
||||
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb', 'clearLogs',
|
||||
'choose_specific_download', 'download_specific_release']
|
||||
|
||||
|
||||
class Api(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.apikey = None
|
||||
@@ -170,7 +174,7 @@ class Api(object):
|
||||
self.data = self._dic_from_query(
|
||||
"SELECT * from albums WHERE Status='Snatched'")
|
||||
return
|
||||
|
||||
|
||||
def _getSimilar(self, **kwargs):
|
||||
self.data = self._dic_from_query('SELECT * from lastfmcloud')
|
||||
return
|
||||
@@ -432,7 +436,6 @@ class Api(object):
|
||||
results_as_dicts = []
|
||||
|
||||
for result in results:
|
||||
|
||||
result_dict = {
|
||||
'title': result[0],
|
||||
'size': result[1],
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import headphones
|
||||
|
||||
import headphones
|
||||
from headphones import db, helpers, logger, lastfm, request
|
||||
|
||||
LASTFM_API_KEY = "690e1ed3bc00bc91804cd8f7fe5ed6d4"
|
||||
@@ -45,8 +45,8 @@ class Cache(object):
|
||||
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
|
||||
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
|
||||
self.id_type = None # 'artist' or 'album' - set automatically depending on whether ArtistID or AlbumID is passed
|
||||
self.query_type = None # 'artwork','thumb' or 'info' - set automatically
|
||||
|
||||
self.artwork_files = []
|
||||
self.thumb_files = []
|
||||
@@ -182,13 +182,18 @@ class Cache(object):
|
||||
if ArtistID:
|
||||
self.id = ArtistID
|
||||
self.id_type = 'artist'
|
||||
db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?', [self.id]).fetchone()
|
||||
db_info = myDB.action(
|
||||
'SELECT Summary, Content, LastUpdated FROM descriptions WHERE ArtistID=?',
|
||||
[self.id]).fetchone()
|
||||
else:
|
||||
self.id = AlbumID
|
||||
self.id_type = 'album'
|
||||
db_info = myDB.action('SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?', [self.id]).fetchone()
|
||||
db_info = myDB.action(
|
||||
'SELECT Summary, Content, LastUpdated FROM descriptions WHERE ReleaseGroupID=?',
|
||||
[self.id]).fetchone()
|
||||
|
||||
if not db_info or not db_info['LastUpdated'] or not self._is_current(date=db_info['LastUpdated']):
|
||||
if not db_info or not db_info['LastUpdated'] or not self._is_current(
|
||||
date=db_info['LastUpdated']):
|
||||
|
||||
self._update_cache()
|
||||
info_dict = {'Summary': self.info_summary, 'Content': self.info_content}
|
||||
@@ -309,13 +314,19 @@ class Cache(object):
|
||||
logger.debug('No artist thumbnail image found')
|
||||
|
||||
else:
|
||||
dbalbum = myDB.action('SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?', [self.id]).fetchone()
|
||||
dbalbum = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, ReleaseID FROM albums WHERE AlbumID=?',
|
||||
[self.id]).fetchone()
|
||||
if dbalbum['ReleaseID'] != self.id:
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'], api_key=LASTFM_API_KEY)
|
||||
data = lastfm.request_lastfm("album.getinfo", mbid=dbalbum['ReleaseID'],
|
||||
api_key=LASTFM_API_KEY)
|
||||
if not data:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'],
|
||||
api_key=LASTFM_API_KEY)
|
||||
else:
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'], album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
data = lastfm.request_lastfm("album.getinfo", artist=dbalbum['ArtistName'],
|
||||
album=dbalbum['AlbumTitle'], api_key=LASTFM_API_KEY)
|
||||
|
||||
if not data:
|
||||
return
|
||||
@@ -357,7 +368,8 @@ class Cache(object):
|
||||
# Save the image URL to the database
|
||||
if image_url:
|
||||
if self.id_type == 'artist':
|
||||
myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?', [image_url, self.id])
|
||||
myDB.action('UPDATE artists SET ArtworkURL=? WHERE ArtistID=?',
|
||||
[image_url, self.id])
|
||||
else:
|
||||
myDB.action('UPDATE albums SET ArtworkURL=? WHERE AlbumID=?', [image_url, self.id])
|
||||
|
||||
@@ -378,7 +390,8 @@ class Cache(object):
|
||||
if not os.path.isdir(self.path_to_art_cache):
|
||||
try:
|
||||
os.makedirs(self.path_to_art_cache)
|
||||
os.chmod(self.path_to_art_cache, int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
os.chmod(self.path_to_art_cache,
|
||||
int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except OSError as e:
|
||||
logger.error('Unable to create artwork cache dir. Error: %s', e)
|
||||
self.artwork_errors = True
|
||||
@@ -393,7 +406,8 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
artwork_path = os.path.join(self.path_to_art_cache, self.id + '.' + helpers.today() + ext)
|
||||
artwork_path = os.path.join(self.path_to_art_cache,
|
||||
self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(artwork_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
@@ -406,7 +420,8 @@ class Cache(object):
|
||||
|
||||
# Grab the thumbnail as well if we're getting the full artwork (as long
|
||||
# as it's missing/outdated.
|
||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (self.thumb_files and self._is_current(self.thumb_files[0])):
|
||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
||||
self.thumb_files and self._is_current(self.thumb_files[0])):
|
||||
artwork = request.request_content(thumb_url, timeout=20)
|
||||
|
||||
if artwork:
|
||||
@@ -414,7 +429,8 @@ class Cache(object):
|
||||
if not os.path.isdir(self.path_to_art_cache):
|
||||
try:
|
||||
os.makedirs(self.path_to_art_cache)
|
||||
os.chmod(self.path_to_art_cache, int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
os.chmod(self.path_to_art_cache,
|
||||
int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except OSError as e:
|
||||
logger.error('Unable to create artwork cache dir. Error: %s' + e)
|
||||
self.thumb_errors = True
|
||||
@@ -429,7 +445,8 @@ class Cache(object):
|
||||
|
||||
ext = os.path.splitext(image_url)[1]
|
||||
|
||||
thumb_path = os.path.join(self.path_to_art_cache, 'T_' + self.id + '.' + helpers.today() + ext)
|
||||
thumb_path = os.path.join(self.path_to_art_cache,
|
||||
'T_' + self.id + '.' + helpers.today() + ext)
|
||||
try:
|
||||
with open(thumb_path, 'wb') as f:
|
||||
f.write(artwork)
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#########################################
|
||||
## Stolen from Sick-Beard's classes.py ##
|
||||
#########################################
|
||||
#######################################
|
||||
# Stolen from Sick-Beard's classes.py #
|
||||
#######################################
|
||||
|
||||
|
||||
import urllib
|
||||
@@ -133,4 +133,5 @@ class Proper:
|
||||
self.episode = -1
|
||||
|
||||
def __str__(self):
|
||||
return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(self.episode) + " of " + str(self.tvdbid)
|
||||
return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(
|
||||
self.episode) + " of " + str(self.tvdbid)
|
||||
|
||||
@@ -20,15 +20,16 @@ Created on Aug 1, 2011
|
||||
'''
|
||||
import platform
|
||||
import operator
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from headphones import version
|
||||
|
||||
#Identify Our Application
|
||||
|
||||
# Identify Our Application
|
||||
USER_AGENT = 'Headphones/-' + version.HEADPHONES_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
|
||||
|
||||
### Notification Types
|
||||
# Notification Types
|
||||
NOTIFY_SNATCH = 1
|
||||
NOTIFY_DOWNLOAD = 2
|
||||
|
||||
@@ -36,26 +37,25 @@ notifyStrings = {}
|
||||
notifyStrings[NOTIFY_SNATCH] = "Started Download"
|
||||
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
|
||||
|
||||
### Release statuses
|
||||
UNKNOWN = -1 # should never happen
|
||||
UNAIRED = 1 # releases that haven't dropped yet
|
||||
SNATCHED = 2 # qualified with quality
|
||||
WANTED = 3 # releases we don't have but want to get
|
||||
DOWNLOADED = 4 # qualified with quality
|
||||
SKIPPED = 5 # releases we don't want
|
||||
ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
|
||||
IGNORED = 7 # releases that you don't want included in your download stats
|
||||
SNATCHED_PROPER = 9 # qualified with quality
|
||||
# Release statuses
|
||||
UNKNOWN = -1 # should never happen
|
||||
UNAIRED = 1 # releases that haven't dropped yet
|
||||
SNATCHED = 2 # qualified with quality
|
||||
WANTED = 3 # releases we don't have but want to get
|
||||
DOWNLOADED = 4 # qualified with quality
|
||||
SKIPPED = 5 # releases we don't want
|
||||
ARCHIVED = 6 # releases that you don't have locally (counts toward download completion stats)
|
||||
IGNORED = 7 # releases that you don't want included in your download stats
|
||||
SNATCHED_PROPER = 9 # qualified with quality
|
||||
|
||||
|
||||
class Quality:
|
||||
|
||||
NONE = 0
|
||||
B192 = 1 << 1 # 2
|
||||
VBR = 1 << 2 # 4
|
||||
B256 = 1 << 3 # 8
|
||||
B320 = 1 << 4 #16
|
||||
FLAC = 1 << 5 #32
|
||||
B192 = 1 << 1 # 2
|
||||
VBR = 1 << 2 # 4
|
||||
B256 = 1 << 3 # 8
|
||||
B320 = 1 << 4 # 16
|
||||
FLAC = 1 << 5 # 32
|
||||
|
||||
# put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
|
||||
UNKNOWN = 1 << 15
|
||||
@@ -75,7 +75,8 @@ class Quality:
|
||||
def _getStatusStrings(status):
|
||||
toReturn = {}
|
||||
for x in Quality.qualityStrings.keys():
|
||||
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + Quality.qualityStrings[x] + ")"
|
||||
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \
|
||||
Quality.qualityStrings[x] + ")"
|
||||
return toReturn
|
||||
|
||||
@staticmethod
|
||||
@@ -103,6 +104,9 @@ class Quality:
|
||||
@staticmethod
|
||||
def nameQuality(name):
|
||||
|
||||
def checkName(list, func):
|
||||
return func([re.search(x, name, re.I) for x in list])
|
||||
|
||||
name = os.path.basename(name)
|
||||
|
||||
# if we have our exact text then assume we put it there
|
||||
@@ -115,9 +119,7 @@ class Quality:
|
||||
if regex_match:
|
||||
return x
|
||||
|
||||
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
|
||||
|
||||
#TODO: fix quality checking here
|
||||
# TODO: fix quality checking here
|
||||
if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
|
||||
return Quality.B192
|
||||
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
|
||||
@@ -131,7 +133,6 @@ class Quality:
|
||||
|
||||
@staticmethod
|
||||
def assumeQuality(name):
|
||||
|
||||
if name.lower().endswith(".mp3"):
|
||||
return Quality.MP3
|
||||
elif name.lower().endswith(".flac"):
|
||||
@@ -167,13 +168,16 @@ class Quality:
|
||||
SNATCHED = None
|
||||
SNATCHED_PROPER = None
|
||||
|
||||
|
||||
Quality.DOWNLOADED = [Quality.compositeStatus(DOWNLOADED, x) for x in Quality.qualityStrings.keys()]
|
||||
Quality.SNATCHED = [Quality.compositeStatus(SNATCHED, x) for x in Quality.qualityStrings.keys()]
|
||||
Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in Quality.qualityStrings.keys()]
|
||||
Quality.SNATCHED_PROPER = [Quality.compositeStatus(SNATCHED_PROPER, x) for x in
|
||||
Quality.qualityStrings.keys()]
|
||||
|
||||
MP3 = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR], [])
|
||||
LOSSLESS = Quality.combineQualities([Quality.FLAC], [])
|
||||
ANY = Quality.combineQualities([Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
|
||||
ANY = Quality.combineQualities(
|
||||
[Quality.B192, Quality.B256, Quality.B320, Quality.VBR, Quality.FLAC], [])
|
||||
|
||||
qualityPresets = (MP3, LOSSLESS, ANY)
|
||||
qualityPresetStrings = {MP3: "MP3 (All bitrates 192+)",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import headphones.logger
|
||||
import itertools
|
||||
|
||||
import os
|
||||
import re
|
||||
import headphones.logger
|
||||
from configobj import ConfigObj
|
||||
|
||||
|
||||
@@ -14,6 +15,7 @@ def bool_int(value):
|
||||
value = 0
|
||||
return int(bool(value))
|
||||
|
||||
|
||||
_CONFIG_DEFINITIONS = {
|
||||
'ADD_ALBUM_ART': (int, 'General', 0),
|
||||
'ADVANCEDENCODER': (str, 'General', ''),
|
||||
@@ -254,7 +256,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'UTORRENT_PASSWORD': (str, 'uTorrent', ''),
|
||||
'UTORRENT_USERNAME': (str, 'uTorrent', ''),
|
||||
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
|
||||
'WAIT_UNTIL_RELEASE_DATE' : (int, 'General', 0),
|
||||
'WAIT_UNTIL_RELEASE_DATE': (int, 'General', 0),
|
||||
'WAFFLES': (int, 'Waffles', 0),
|
||||
'WAFFLES_PASSKEY': (str, 'Waffles', ''),
|
||||
'WAFFLES_RATIO': (str, 'Waffles', ''),
|
||||
@@ -272,6 +274,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'XLDPROFILE': (str, 'General', '')
|
||||
}
|
||||
|
||||
|
||||
# pylint:disable=R0902
|
||||
# it might be nice to refactor for fewer instance variables
|
||||
class Config(object):
|
||||
@@ -348,7 +351,7 @@ class Config(object):
|
||||
""" Return the extra newznab tuples """
|
||||
extra_newznabs = list(
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_NEWZNABS, i, None, 3)
|
||||
for i in range(3)])
|
||||
for i in range(3)])
|
||||
)
|
||||
return extra_newznabs
|
||||
|
||||
@@ -367,7 +370,7 @@ class Config(object):
|
||||
""" Return the extra torznab tuples """
|
||||
extra_torznabs = list(
|
||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
|
||||
for i in range(3)])
|
||||
for i in range(3)])
|
||||
)
|
||||
return extra_torznabs
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
# Most of this lifted from here: https://github.com/SzieberthAdam/gneposis-cdgrab
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import copy
|
||||
import glob
|
||||
|
||||
import os
|
||||
import re
|
||||
import headphones
|
||||
from headphones import logger
|
||||
from mutagen.flac import FLAC
|
||||
@@ -62,7 +62,7 @@ WAVE_FILE_TYPE_BY_EXTENSION = {
|
||||
'.flac': 'Free Lossless Audio Codec'
|
||||
}
|
||||
|
||||
#SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio")
|
||||
# SHNTOOL_COMPATIBLE = ("Free Lossless Audio Codec", "Waveform Audio", "Monkey's Audio")
|
||||
|
||||
# TODO: Make this better!
|
||||
# this module-level variable is bad. :(
|
||||
@@ -288,7 +288,7 @@ class CueFile(File):
|
||||
global line_content
|
||||
c = self.content.splitlines()
|
||||
header_dict = {}
|
||||
#remaining_headers = CUE_HEADER
|
||||
# remaining_headers = CUE_HEADER
|
||||
remaining_headers = copy.copy(CUE_HEADER)
|
||||
line_index = 0
|
||||
match = True
|
||||
@@ -314,7 +314,8 @@ class CueFile(File):
|
||||
line_content = c[line_index]
|
||||
search_result = re.search(CUE_TRACK, line_content, re.I)
|
||||
if not search_result:
|
||||
raise ValueError('inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1))
|
||||
raise ValueError(
|
||||
'inconsistent CUE sheet, TRACK expected at line {0}'.format(line_index + 1))
|
||||
track_nr = int(search_result.group(1))
|
||||
line_index += 1
|
||||
next_track = False
|
||||
@@ -353,7 +354,8 @@ class CueFile(File):
|
||||
track_meta['dcpflag'] = True
|
||||
line_index += 1
|
||||
else:
|
||||
raise ValueError('unknown entry in track error, line {0}'.format(line_index + 1))
|
||||
raise ValueError(
|
||||
'unknown entry in track error, line {0}'.format(line_index + 1))
|
||||
else:
|
||||
next_track = True
|
||||
|
||||
@@ -371,8 +373,8 @@ class CueFile(File):
|
||||
|
||||
if not self.content:
|
||||
try:
|
||||
with open(self.name, encoding="cp1252") as cue_file:
|
||||
self.content = cue_file.read()
|
||||
with open(self.name, encoding="cp1252") as cue_file:
|
||||
self.content = cue_file.read()
|
||||
except:
|
||||
raise ValueError('Cant encode CUE Sheet.')
|
||||
|
||||
@@ -406,9 +408,11 @@ class CueFile(File):
|
||||
for i in range(len(self.tracks)):
|
||||
if self.tracks[i]:
|
||||
if self.tracks[i].get('artist'):
|
||||
content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get('artist') + '\n'
|
||||
content += 'track' + int_to_str(i) + 'artist' + '\t' + self.tracks[i].get(
|
||||
'artist') + '\n'
|
||||
if self.tracks[i].get('title'):
|
||||
content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get('title') + '\n'
|
||||
content += 'track' + int_to_str(i) + 'title' + '\t' + self.tracks[i].get(
|
||||
'title') + '\n'
|
||||
return content
|
||||
|
||||
def htoa(self):
|
||||
@@ -449,7 +453,8 @@ class MetaFile(File):
|
||||
raise ValueError('Syntax error in album meta file')
|
||||
if not content['tracks'][int(parsed_track.group(1))]:
|
||||
content['tracks'][int(parsed_track.group(1))] = dict()
|
||||
content['tracks'][int(parsed_track.group(1))][parsed_track.group(2)] = parsed_line.group(2)
|
||||
content['tracks'][int(parsed_track.group(1))][
|
||||
parsed_track.group(2)] = parsed_line.group(2)
|
||||
else:
|
||||
content[parsed_line.group(1)] = parsed_line.group(2)
|
||||
|
||||
@@ -472,15 +477,16 @@ class MetaFile(File):
|
||||
if 'genre' in CUE_META.content:
|
||||
common_tags['genre'] = CUE_META.content['genre']
|
||||
|
||||
#freeform tags
|
||||
#freeform_tags['country'] = self.content['country']
|
||||
#freeform_tags['releasedate'] = self.content['releasedate']
|
||||
# freeform tags
|
||||
# freeform_tags['country'] = self.content['country']
|
||||
# freeform_tags['releasedate'] = self.content['releasedate']
|
||||
|
||||
return common_tags, freeform_tags
|
||||
|
||||
def folders(self):
|
||||
artist = self.content['artist']
|
||||
album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content['label'] + ' - ' + self.content['catalog'] + ')'
|
||||
album = self.content['date'] + ' - ' + self.content['title'] + ' (' + self.content[
|
||||
'label'] + ' - ' + self.content['catalog'] + ')'
|
||||
return artist, album
|
||||
|
||||
def complete(self):
|
||||
@@ -535,6 +541,7 @@ class WaveFile(File):
|
||||
if self.type == 'Free Lossless Audio Codec':
|
||||
return FLAC(self.name)
|
||||
|
||||
|
||||
def split(albumpath):
|
||||
global CUE_META
|
||||
os.chdir(albumpath)
|
||||
@@ -577,7 +584,8 @@ def split(albumpath):
|
||||
import getXldProfile
|
||||
xldprofile, xldformat, _ = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
|
||||
if not xldformat:
|
||||
raise ValueError('Details for xld profile "%s" not found, cannot split cue' % (xldprofile))
|
||||
raise ValueError(
|
||||
'Details for xld profile "%s" not found, cannot split cue' % (xldprofile))
|
||||
else:
|
||||
if headphones.CONFIG.ENCODERFOLDER:
|
||||
splitter = os.path.join(headphones.CONFIG.ENCODERFOLDER, 'xld')
|
||||
@@ -590,7 +598,7 @@ def split(albumpath):
|
||||
splitter = 'shntool'
|
||||
|
||||
if splitter == 'shntool' and not check_splitter(splitter):
|
||||
raise ValueError('Command not found, ensure shntool or xld installed')
|
||||
raise ValueError('Command not found, ensure shntool or xld installed')
|
||||
|
||||
# Determine if file can be split
|
||||
if wave.name_ext not in WAVE_FILE_TYPE_BY_EXTENSION.keys():
|
||||
|
||||
@@ -13,44 +13,41 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#####################################
|
||||
## Stolen from Sick-Beard's db.py ##
|
||||
#####################################
|
||||
###################################
|
||||
# Stolen from Sick-Beard's db.py #
|
||||
###################################
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
import os
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def dbFilename(filename="headphones.db"):
|
||||
|
||||
return os.path.join(headphones.DATA_DIR, filename)
|
||||
|
||||
|
||||
def getCacheSize():
|
||||
#this will protect against typecasting problems produced by empty string and None settings
|
||||
# this will protect against typecasting problems produced by empty string and None settings
|
||||
if not headphones.CONFIG.CACHE_SIZEMB:
|
||||
#sqlite will work with this (very slowly)
|
||||
# sqlite will work with this (very slowly)
|
||||
return 0
|
||||
return int(headphones.CONFIG.CACHE_SIZEMB)
|
||||
|
||||
|
||||
class DBConnection:
|
||||
|
||||
def __init__(self, filename="headphones.db"):
|
||||
|
||||
self.filename = filename
|
||||
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
||||
#don't wait for the disk to finish writing
|
||||
# don't wait for the disk to finish writing
|
||||
self.connection.execute("PRAGMA synchronous = OFF")
|
||||
#journal disabled since we never do rollbacks
|
||||
# journal disabled since we never do rollbacks
|
||||
self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE)
|
||||
#64mb of cache memory,probably need to make it user configurable
|
||||
# 64mb of cache memory,probably need to make it user configurable
|
||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
|
||||
@@ -92,17 +89,20 @@ class DBConnection:
|
||||
|
||||
def upsert(self, tableName, valueDict, keyDict):
|
||||
|
||||
def genParams(myDict):
|
||||
return [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
changesBefore = self.connection.total_changes
|
||||
|
||||
genParams = lambda myDict: [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
update_query = "UPDATE " + tableName + " SET " + ", ".join(
|
||||
genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
|
||||
self.action(update_query, valueDict.values() + keyDict.values())
|
||||
|
||||
if self.connection.total_changes == changesBefore:
|
||||
insert_query = (
|
||||
"INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" +
|
||||
"INSERT INTO " + tableName + " (" + ", ".join(
|
||||
valueDict.keys() + keyDict.keys()) + ")" +
|
||||
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
|
||||
)
|
||||
try:
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import os.path
|
||||
|
||||
import biplist
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def getXldProfile(xldProfile):
|
||||
|
||||
xldProfileNotFound = xldProfile
|
||||
|
||||
expanded = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist')
|
||||
if not os.path.isfile(expanded):
|
||||
logger.warn("Could not find xld preferences at: %s", expanded)
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
# Get xld preferences plist
|
||||
try:
|
||||
preferences = biplist.readPlist(expanded)
|
||||
except (biplist.InvalidPlistException, biplist.NotBinaryPlistException), e:
|
||||
logger.error("Error reading xld preferences plist: %s", e)
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
if not isinstance(preferences, dict):
|
||||
logger.error("Error reading xld preferences plist, not a dict: %r", preferences)
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
profiles = preferences.get('Profiles', []) # pylint:disable=E1103
|
||||
profiles = preferences.get('Profiles', []) # pylint:disable=E1103
|
||||
|
||||
xldProfile = xldProfile.lower()
|
||||
for profile in profiles:
|
||||
@@ -175,6 +175,6 @@ def getXldProfile(xldProfile):
|
||||
if xldFormat and not xldBitrate:
|
||||
xldBitrate = 400
|
||||
|
||||
return(xldProfileForCmd, xldFormat, xldBitrate)
|
||||
return (xldProfileForCmd, xldFormat, xldBitrate)
|
||||
|
||||
return(xldProfileNotFound, None, None)
|
||||
return (xldProfileNotFound, None, None)
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
import unicodedata
|
||||
import headphones
|
||||
import datetime
|
||||
import fnmatch
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
import os
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
import headphones
|
||||
|
||||
|
||||
# Modified from https://github.com/Verrus/beets-plugin-featInTitle
|
||||
RE_FEATURING = re.compile(r"[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.")
|
||||
@@ -35,7 +35,9 @@ RE_CD = re.compile(r"^(CD|dics)\s*[0-9]+$", re.I)
|
||||
|
||||
|
||||
def multikeysort(items, columns):
|
||||
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
||||
comparers = [
|
||||
((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1))
|
||||
for col in columns]
|
||||
|
||||
def comparer(left, right):
|
||||
for fn, mult in comparers:
|
||||
@@ -56,7 +58,6 @@ def checked(variable):
|
||||
|
||||
|
||||
def radio(variable, pos):
|
||||
|
||||
if variable == pos:
|
||||
return 'Checked'
|
||||
else:
|
||||
@@ -107,7 +108,6 @@ def latinToAscii(unicrap):
|
||||
|
||||
|
||||
def convert_milliseconds(ms):
|
||||
|
||||
seconds = ms / 1000
|
||||
gmtime = time.gmtime(seconds)
|
||||
if seconds > 3600:
|
||||
@@ -119,7 +119,6 @@ def convert_milliseconds(ms):
|
||||
|
||||
|
||||
def convert_seconds(s):
|
||||
|
||||
gmtime = time.gmtime(s)
|
||||
if s > 3600:
|
||||
minutes = time.strftime("%H:%M:%S", gmtime)
|
||||
@@ -141,7 +140,6 @@ def now():
|
||||
|
||||
|
||||
def get_age(date):
|
||||
|
||||
try:
|
||||
split_date = date.split('-')
|
||||
except:
|
||||
@@ -149,14 +147,13 @@ def get_age(date):
|
||||
|
||||
try:
|
||||
days_old = int(split_date[0]) * 365 + int(split_date[1]) * 30 + int(split_date[2])
|
||||
except (IndexError,ValueError):
|
||||
except (IndexError, ValueError):
|
||||
days_old = False
|
||||
|
||||
return days_old
|
||||
|
||||
|
||||
def bytes_to_mb(bytes):
|
||||
|
||||
mb = int(bytes) / 1048576
|
||||
size = '%.1f MB' % mb
|
||||
return size
|
||||
@@ -172,7 +169,7 @@ def piratesize(size):
|
||||
split = size.split(" ")
|
||||
factor = float(split[0])
|
||||
unit = split[1].upper()
|
||||
|
||||
|
||||
if unit == 'MIB':
|
||||
size = factor * 1048576
|
||||
elif unit == 'MB':
|
||||
@@ -194,7 +191,6 @@ def piratesize(size):
|
||||
|
||||
|
||||
def replace_all(text, dic, normalize=False):
|
||||
|
||||
if not text:
|
||||
return ''
|
||||
|
||||
@@ -221,15 +217,14 @@ def replace_illegal_chars(string, type="file"):
|
||||
|
||||
|
||||
def cleanName(string):
|
||||
|
||||
pass1 = latinToAscii(string).lower()
|
||||
out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '', pass1).encode('utf-8')
|
||||
out_string = re.sub('[\.\-\/\!\@\#\$\%\^\&\*\(\)\+\-\"\'\,\;\:\[\]\{\}\<\>\=\_]', '',
|
||||
pass1).encode('utf-8')
|
||||
|
||||
return out_string
|
||||
|
||||
|
||||
def cleanTitle(title):
|
||||
|
||||
title = re.sub('[\.\-\/\_]', ' ', title).lower()
|
||||
|
||||
# Strip out extra whitespace
|
||||
@@ -312,16 +307,22 @@ def expand_subfolders(f):
|
||||
difference = max(path_depths) - min(path_depths)
|
||||
|
||||
if difference > 0:
|
||||
logger.info("Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.", len(media_folders), difference)
|
||||
logger.info(
|
||||
"Found %d media folders, but depth difference between lowest and deepest media folder is %d (expected zero). If this is a discography or a collection of albums, make sure albums are per folder.",
|
||||
len(media_folders), difference)
|
||||
|
||||
# While already failed, advice the user what he could try. We assume the
|
||||
# directory may contain separate CD's and maybe some extra's. The
|
||||
# structure may look like X albums at same depth, and (one or more)
|
||||
# extra folders with a higher depth.
|
||||
extra_media_folders = [media_folder[:min(path_depths)] for media_folder in media_folders if len(media_folder) > min(path_depths)]
|
||||
extra_media_folders = list(set([os.path.join(*media_folder) for media_folder in extra_media_folders]))
|
||||
extra_media_folders = [media_folder[:min(path_depths)] for media_folder in media_folders if
|
||||
len(media_folder) > min(path_depths)]
|
||||
extra_media_folders = list(
|
||||
set([os.path.join(*media_folder) for media_folder in extra_media_folders]))
|
||||
|
||||
logger.info("Please look at the following folder(s), since they cause the depth difference: %s", extra_media_folders)
|
||||
logger.info(
|
||||
"Please look at the following folder(s), since they cause the depth difference: %s",
|
||||
extra_media_folders)
|
||||
return
|
||||
|
||||
# Convert back to paths and remove duplicates, which may be there after
|
||||
@@ -368,7 +369,7 @@ def path_filter_patterns(paths, patterns, root=None):
|
||||
for path in paths[:]:
|
||||
if path_match_patterns(path, patterns):
|
||||
logger.debug("Path ignored by pattern: %s",
|
||||
os.path.join(root or "", path))
|
||||
os.path.join(root or "", path))
|
||||
|
||||
ignored += 1
|
||||
paths.remove(path)
|
||||
@@ -378,11 +379,11 @@ def path_filter_patterns(paths, patterns, root=None):
|
||||
|
||||
|
||||
def extract_data(s):
|
||||
|
||||
s = s.replace('_', ' ')
|
||||
|
||||
#headphones default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s[\[\(](?P<year>.*?)[\]\)]', re.VERBOSE)
|
||||
# headphones default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s[\[\(](?P<year>.*?)[\]\)]',
|
||||
re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
|
||||
if match:
|
||||
@@ -391,7 +392,7 @@ def extract_data(s):
|
||||
year = match.group("year")
|
||||
return (name, album, year)
|
||||
|
||||
#Gonna take a guess on this one - might be enough to search on mb
|
||||
# Gonna take a guess on this one - might be enough to search on mb
|
||||
pat = re.compile(r"(?P<name>.*?)\s*-\s*(?P<album>[^\[(-]*)")
|
||||
|
||||
match = pat.match(s)
|
||||
@@ -468,7 +469,8 @@ def extract_metadata(f):
|
||||
old_album = new_albums[index]
|
||||
new_albums[index] = RE_CD_ALBUM.sub("", album).strip()
|
||||
|
||||
logger.debug("Stripped albumd number identifier: %s -> %s", old_album, new_albums[index])
|
||||
logger.debug("Stripped albumd number identifier: %s -> %s", old_album,
|
||||
new_albums[index])
|
||||
|
||||
# Remove duplicates
|
||||
new_albums = list(set(new_albums))
|
||||
@@ -498,7 +500,8 @@ def extract_metadata(f):
|
||||
return (artist, albums[0], years[0])
|
||||
|
||||
# Not sure what to do here.
|
||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists), len(albums), len(years))
|
||||
logger.info("Found %d artists, %d albums and %d years in metadata, so ignoring", len(artists),
|
||||
len(albums), len(years))
|
||||
logger.debug("Artists: %s, Albums: %s, Years: %s", artists, albums, years)
|
||||
|
||||
return (None, None, None)
|
||||
@@ -524,8 +527,10 @@ def preserve_torrent_directory(albumpath):
|
||||
Copy torrent directory to headphones-modified to keep files for seeding.
|
||||
"""
|
||||
from headphones import logger
|
||||
new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
new_folder = os.path.join(albumpath,
|
||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
try:
|
||||
shutil.copytree(albumpath, new_folder)
|
||||
return new_folder
|
||||
@@ -578,7 +583,9 @@ def cue_split(albumpath):
|
||||
|
||||
def extract_logline(s):
|
||||
# Default log format
|
||||
pattern = re.compile(r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)', re.VERBOSE)
|
||||
pattern = re.compile(
|
||||
r'(?P<timestamp>.*?)\s\-\s(?P<level>.*?)\s*\:\:\s(?P<thread>.*?)\s\:\s(?P<message>.*)',
|
||||
re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
if match:
|
||||
timestamp = match.group("timestamp")
|
||||
@@ -593,7 +600,7 @@ def extract_logline(s):
|
||||
def extract_song_data(s):
|
||||
from headphones import logger
|
||||
|
||||
#headphones default format
|
||||
# headphones default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\[(?P<year>.*?)\]', re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
|
||||
@@ -605,7 +612,7 @@ def extract_song_data(s):
|
||||
else:
|
||||
logger.info("Couldn't parse %s into a valid default format", s)
|
||||
|
||||
#newzbin default format
|
||||
# newzbin default format
|
||||
pattern = re.compile(r'(?P<name>.*?)\s\-\s(?P<album>.*?)\s\((?P<year>\d+?\))', re.VERBOSE)
|
||||
match = pattern.match(s)
|
||||
if match:
|
||||
@@ -619,7 +626,6 @@ def extract_song_data(s):
|
||||
|
||||
|
||||
def smartMove(src, dest, delete=True):
|
||||
|
||||
from headphones import logger
|
||||
|
||||
source_dir = os.path.dirname(src)
|
||||
@@ -640,7 +646,8 @@ def smartMove(src, dest, delete=True):
|
||||
os.rename(src, os.path.join(source_dir, newfile))
|
||||
filename = newfile
|
||||
except Exception as e:
|
||||
logger.warn('Error renaming %s: %s', src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
logger.warn('Error renaming %s: %s',
|
||||
src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
break
|
||||
|
||||
try:
|
||||
@@ -650,7 +657,9 @@ def smartMove(src, dest, delete=True):
|
||||
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
e)
|
||||
|
||||
|
||||
def walk_directory(basedir, followlinks=True):
|
||||
"""
|
||||
@@ -672,8 +681,8 @@ def walk_directory(basedir, followlinks=True):
|
||||
real_path = os.path.abspath(os.readlink(path))
|
||||
|
||||
if real_path in traversed:
|
||||
logger.debug("Skipping '%s' since it is a symlink to "\
|
||||
"'%s', which is already visited.", path, real_path)
|
||||
logger.debug("Skipping '%s' since it is a symlink to " \
|
||||
"'%s', which is already visited.", path, real_path)
|
||||
else:
|
||||
traversed.append(real_path)
|
||||
|
||||
@@ -689,8 +698,9 @@ def walk_directory(basedir, followlinks=True):
|
||||
for result in _inner(*args):
|
||||
yield result
|
||||
|
||||
|
||||
#########################
|
||||
#Sab renaming functions #
|
||||
# Sab renaming functions #
|
||||
#########################
|
||||
|
||||
# TODO: Grab config values from sab to know when these options are checked. For now we'll just iterate through all combinations
|
||||
@@ -739,18 +749,20 @@ def sab_sanitize_foldername(name):
|
||||
if not name:
|
||||
name = 'unknown'
|
||||
|
||||
#maxlen = cfg.folder_max_length()
|
||||
#if len(name) > maxlen:
|
||||
# maxlen = cfg.folder_max_length()
|
||||
# if len(name) > maxlen:
|
||||
# name = name[:maxlen]
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def split_string(mystring, splitvar=','):
|
||||
mylist = []
|
||||
for each_word in mystring.split(splitvar):
|
||||
mylist.append(each_word.strip())
|
||||
return mylist
|
||||
|
||||
|
||||
def create_https_certificates(ssl_cert, ssl_key):
|
||||
"""
|
||||
Create a pair of self-signed HTTPS certificares and store in them in
|
||||
@@ -768,11 +780,13 @@ def create_https_certificates(ssl_cert, ssl_key):
|
||||
# Create the CA Certificate
|
||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
cacert = createCertificate(careq, (careq, cakey), serial,
|
||||
(0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||
req = createCertRequest(pkey, CN="Headphones")
|
||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
cert = createCertificate(req, (cacert, cakey), serial,
|
||||
(0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
# Save the key and certificate to disk
|
||||
try:
|
||||
|
||||
@@ -13,39 +13,39 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import logger, helpers, db, mb, lastfm, metacritic
|
||||
|
||||
from beets.mediafile import MediaFile
|
||||
|
||||
import time
|
||||
|
||||
from headphones import logger, helpers, db, mb, lastfm, metacritic
|
||||
from beets.mediafile import MediaFile
|
||||
import headphones
|
||||
|
||||
blacklisted_special_artist_names = ['[anonymous]', '[data]', '[no artist]',
|
||||
'[traditional]', '[unknown]', 'Various Artists']
|
||||
'[traditional]', '[unknown]', 'Various Artists']
|
||||
blacklisted_special_artists = ['f731ccc4-e22a-43af-a747-64213329e088',
|
||||
'33cf029c-63b0-41a0-9855-be2a3665fb3b',
|
||||
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
|
||||
'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
|
||||
'9be7f096-97ec-4615-8957-8d40b5dcbc41',
|
||||
'125ec42a-7229-4250-afc5-e057484327fe',
|
||||
'89ad4ac3-39f7-470e-963a-56509c546377']
|
||||
'33cf029c-63b0-41a0-9855-be2a3665fb3b',
|
||||
'314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc',
|
||||
'eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61',
|
||||
'9be7f096-97ec-4615-8957-8d40b5dcbc41',
|
||||
'125ec42a-7229-4250-afc5-e057484327fe',
|
||||
'89ad4ac3-39f7-470e-963a-56509c546377']
|
||||
|
||||
|
||||
def is_exists(artistid):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# See if the artist is already in the database
|
||||
artistlist = myDB.select('SELECT ArtistID, ArtistName from artists WHERE ArtistID=?', [artistid])
|
||||
artistlist = myDB.select('SELECT ArtistID, ArtistName from artists WHERE ArtistID=?',
|
||||
[artistid])
|
||||
|
||||
if any(artistid in x for x in artistlist):
|
||||
logger.info(artistlist[0][1] + u" is already in the database. Updating 'have tracks', but not artist information")
|
||||
logger.info(artistlist[0][
|
||||
1] + u" is already in the database. Updating 'have tracks', but not artist information")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def artistlist_to_mbids(artistlist, forced=False):
|
||||
|
||||
for artist in artistlist:
|
||||
|
||||
if not artist and artist != ' ':
|
||||
@@ -77,9 +77,12 @@ def artistlist_to_mbids(artistlist, forced=False):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
if not forced:
|
||||
bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?', [artistid]).fetchone()
|
||||
bl_artist = myDB.action('SELECT * FROM blacklist WHERE ArtistID=?',
|
||||
[artistid]).fetchone()
|
||||
if bl_artist or artistid in blacklisted_special_artists:
|
||||
logger.info("Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must do it manually (Artist ID: %s)" % (artist, artistid))
|
||||
logger.info(
|
||||
"Artist ID for '%s' is either blacklisted or Various Artists. To add artist, you must do it manually (Artist ID: %s)" % (
|
||||
artist, artistid))
|
||||
continue
|
||||
|
||||
# Add to database if it doesn't exist
|
||||
@@ -88,7 +91,9 @@ def artistlist_to_mbids(artistlist, forced=False):
|
||||
|
||||
# Just update the tracks if it does
|
||||
else:
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
|
||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
|
||||
|
||||
# Delete it from the New Artists if the request came from there
|
||||
@@ -112,7 +117,6 @@ def addArtistIDListToDB(artistidlist):
|
||||
|
||||
|
||||
def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
|
||||
# Putting this here to get around the circular import. We're using this to update thumbnails for artist/albums
|
||||
from headphones import cache
|
||||
|
||||
@@ -142,7 +146,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
"Status": "Loading",
|
||||
"IncludeExtras": headphones.CONFIG.INCLUDE_EXTRAS,
|
||||
"Extras": headphones.CONFIG.EXTRAS}
|
||||
if type=="series":
|
||||
if type == "series":
|
||||
newValueDict['Type'] = "series"
|
||||
else:
|
||||
newValueDict = {"Status": "Loading"}
|
||||
@@ -151,7 +155,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
if type=="series":
|
||||
if type == "series":
|
||||
artist = mb.getSeries(artistid)
|
||||
else:
|
||||
artist = mb.getArtist(artistid, extrasonly)
|
||||
@@ -159,7 +163,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
if artist and artist.get('artist_name') in blacklisted_special_artist_names:
|
||||
logger.warn('Cannot import blocked special purpose artist: %s' % artist.get('artist_name'))
|
||||
myDB.action('DELETE from artists WHERE ArtistID=?', [artistid])
|
||||
#in case it's already in the db
|
||||
# in case it's already in the db
|
||||
myDB.action('DELETE from albums WHERE ArtistID=?', [artistid])
|
||||
myDB.action('DELETE from tracks WHERE ArtistID=?', [artistid])
|
||||
return
|
||||
@@ -168,7 +172,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
logger.warn("Error fetching artist info. ID: " + artistid)
|
||||
if dbartist is None:
|
||||
newValueDict = {"ArtistName": "Fetch failed, try refreshing. (%s)" % (artistid),
|
||||
"Status": "Active"}
|
||||
"Status": "Active"}
|
||||
else:
|
||||
newValueDict = {"Status": "Active"}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
@@ -191,7 +195,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
# See if we need to grab extras. Artist specific extras take precedence
|
||||
# over global option. Global options are set when adding a new artist
|
||||
try:
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone()
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?',
|
||||
[artistid]).fetchone()
|
||||
includeExtras = db_artist['IncludeExtras']
|
||||
except IndexError:
|
||||
includeExtras = False
|
||||
@@ -206,9 +211,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
for groups in artist['releasegroups']:
|
||||
group_list.append(groups['id'])
|
||||
if not extrasonly:
|
||||
remove_missing_groups_from_albums = myDB.select("SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid])
|
||||
remove_missing_groups_from_albums = myDB.select(
|
||||
"SELECT AlbumID FROM albums WHERE ArtistID=?", [artistid])
|
||||
else:
|
||||
remove_missing_groups_from_albums = myDB.select('SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', [artistid])
|
||||
remove_missing_groups_from_albums = myDB.select(
|
||||
'SELECT AlbumID FROM albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"',
|
||||
[artistid])
|
||||
for items in remove_missing_groups_from_albums:
|
||||
if items['AlbumID'] not in group_list:
|
||||
# Remove all from albums/tracks that aren't in release groups
|
||||
@@ -217,12 +225,16 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
myDB.action("DELETE FROM tracks WHERE AlbumID=?", [items['AlbumID']])
|
||||
myDB.action("DELETE FROM alltracks WHERE AlbumID=?", [items['AlbumID']])
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [items['AlbumID']])
|
||||
logger.info("[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (artist['artist_name'], items['AlbumID']))
|
||||
logger.info(
|
||||
"[%s] Removing all references to release group %s to reflect MusicBrainz refresh" % (
|
||||
artist['artist_name'], items['AlbumID']))
|
||||
if not extrasonly:
|
||||
force_repackage = 1
|
||||
else:
|
||||
if not extrasonly:
|
||||
logger.info("[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" % artist['artist_name'])
|
||||
logger.info(
|
||||
"[%s] There was either an error pulling data from MusicBrainz or there might not be any releases for this category" %
|
||||
artist['artist_name'])
|
||||
|
||||
# Then search for releases within releasegroups, if releases don't exist, then remove from allalbums/alltracks
|
||||
album_searches = []
|
||||
@@ -232,7 +244,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
today = helpers.today()
|
||||
rgid = rg['id']
|
||||
skip_log = 0
|
||||
#Make a user configurable variable to skip update of albums with release dates older than this date (in days)
|
||||
# Make a user configurable variable to skip update of albums with release dates older than this date (in days)
|
||||
pause_delta = headphones.CONFIG.MB_IGNORE_AGE
|
||||
|
||||
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
|
||||
@@ -247,12 +259,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
new_release_group = True
|
||||
|
||||
if new_release_group:
|
||||
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
|
||||
logger.info("[%s] Now adding: %s (New Release Group)" % (
|
||||
artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras)
|
||||
|
||||
else:
|
||||
if check_release_date is None or check_release_date == u"None":
|
||||
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
|
||||
logger.info("[%s] Now updating: %s (No Release Date)" % (
|
||||
artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
||||
else:
|
||||
if len(check_release_date) == 10:
|
||||
@@ -264,20 +278,24 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
else:
|
||||
release_date = today
|
||||
if helpers.get_age(today) - helpers.get_age(release_date) < pause_delta:
|
||||
logger.info("[%s] Now updating: %s (Release Date <%s Days)", artist['artist_name'], rg['title'], pause_delta)
|
||||
logger.info("[%s] Now updating: %s (Release Date <%s Days)",
|
||||
artist['artist_name'], rg['title'], pause_delta)
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras, True)
|
||||
else:
|
||||
logger.info("[%s] Skipping: %s (Release Date >%s Days)", artist['artist_name'], rg['title'], pause_delta)
|
||||
logger.info("[%s] Skipping: %s (Release Date >%s Days)",
|
||||
artist['artist_name'], rg['title'], pause_delta)
|
||||
skip_log = 1
|
||||
new_releases = 0
|
||||
|
||||
if force_repackage == 1:
|
||||
new_releases = -1
|
||||
logger.info('[%s] Forcing repackage of %s (Release Group Removed)', artist['artist_name'], al_title)
|
||||
logger.info('[%s] Forcing repackage of %s (Release Group Removed)',
|
||||
artist['artist_name'], al_title)
|
||||
else:
|
||||
new_releases = new_releases
|
||||
else:
|
||||
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'], rg['title'])
|
||||
logger.info("[%s] Now adding/updating: %s (Comprehensive Force)", artist['artist_name'],
|
||||
rg['title'])
|
||||
new_releases = mb.get_new_releases(rgid, includeExtras, forcefull)
|
||||
|
||||
if new_releases != 0:
|
||||
@@ -291,23 +309,26 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
# This will be used later to build a hybrid release
|
||||
fullreleaselist = []
|
||||
# Search for releases within a release group
|
||||
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?", [rg['id']])
|
||||
find_hybrid_releases = myDB.action("SELECT * from allalbums WHERE AlbumID=?",
|
||||
[rg['id']])
|
||||
|
||||
# Build the dictionary for the fullreleaselist
|
||||
for items in find_hybrid_releases:
|
||||
if items['ReleaseID'] != rg['id']: #don't include hybrid information, since that's what we're replacing
|
||||
if items['ReleaseID'] != rg[
|
||||
'id']: # don't include hybrid information, since that's what we're replacing
|
||||
hybrid_release_id = items['ReleaseID']
|
||||
newValueDict = {"ArtistID": items['ArtistID'],
|
||||
"ArtistName": items['ArtistName'],
|
||||
"AlbumTitle": items['AlbumTitle'],
|
||||
"AlbumID": items['AlbumID'],
|
||||
"AlbumASIN": items['AlbumASIN'],
|
||||
"ReleaseDate": items['ReleaseDate'],
|
||||
"Type": items['Type'],
|
||||
"ReleaseCountry": items['ReleaseCountry'],
|
||||
"ReleaseFormat": items['ReleaseFormat']
|
||||
}
|
||||
find_hybrid_tracks = myDB.action("SELECT * from alltracks WHERE ReleaseID=?", [hybrid_release_id])
|
||||
"ArtistName": items['ArtistName'],
|
||||
"AlbumTitle": items['AlbumTitle'],
|
||||
"AlbumID": items['AlbumID'],
|
||||
"AlbumASIN": items['AlbumASIN'],
|
||||
"ReleaseDate": items['ReleaseDate'],
|
||||
"Type": items['Type'],
|
||||
"ReleaseCountry": items['ReleaseCountry'],
|
||||
"ReleaseFormat": items['ReleaseFormat']
|
||||
}
|
||||
find_hybrid_tracks = myDB.action("SELECT * from alltracks WHERE ReleaseID=?",
|
||||
[hybrid_release_id])
|
||||
totalTracks = 1
|
||||
hybrid_track_array = []
|
||||
for hybrid_tracks in find_hybrid_tracks:
|
||||
@@ -315,9 +336,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
'number': hybrid_tracks['TrackNumber'],
|
||||
'title': hybrid_tracks['TrackTitle'],
|
||||
'id': hybrid_tracks['TrackID'],
|
||||
#'url': hybrid_tracks['TrackURL'],
|
||||
# 'url': hybrid_tracks['TrackURL'],
|
||||
'duration': hybrid_tracks['TrackDuration']
|
||||
})
|
||||
})
|
||||
totalTracks += 1
|
||||
newValueDict['ReleaseID'] = hybrid_release_id
|
||||
newValueDict['Tracks'] = hybrid_track_array
|
||||
@@ -327,10 +348,12 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
# This may end up being called with an empty fullreleaselist
|
||||
try:
|
||||
hybridrelease = getHybridRelease(fullreleaselist)
|
||||
logger.info('[%s] Packaging %s releases into hybrid title' % (artist['artist_name'], rg['title']))
|
||||
logger.info('[%s] Packaging %s releases into hybrid title' % (
|
||||
artist['artist_name'], rg['title']))
|
||||
except Exception as e:
|
||||
errors = True
|
||||
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (artist['artist_name'], rg['title'], e))
|
||||
logger.warn('[%s] Unable to get hybrid release information for %s: %s' % (
|
||||
artist['artist_name'], rg['title'], e))
|
||||
continue
|
||||
|
||||
# Use the ReleaseGroupID as the ReleaseID for the hybrid release to differentiate it
|
||||
@@ -345,13 +368,14 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
"AlbumASIN": hybridrelease['AlbumASIN'],
|
||||
"ReleaseDate": hybridrelease['ReleaseDate'],
|
||||
"Type": rg['type']
|
||||
}
|
||||
}
|
||||
|
||||
myDB.upsert("allalbums", newValueDict, controlValueDict)
|
||||
|
||||
for track in hybridrelease['Tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
|
||||
cleanname = helpers.cleanName(
|
||||
artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": rg['id']}
|
||||
@@ -365,25 +389,29 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
}
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?',
|
||||
[cleanname]).fetchone()
|
||||
|
||||
if not match:
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [artist['artist_name'], rg['title'], track['title']]).fetchone()
|
||||
#if not match:
|
||||
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[artist['artist_name'], rg['title'], track['title']]).fetchone()
|
||||
# if not match:
|
||||
# match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
if match:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (rg['id'], match['Location']))
|
||||
# myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?',
|
||||
(rg['id'], match['Location']))
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
# Delete matched tracks from the have table
|
||||
#myDB.action('DELETE from have WHERE Matched="True"')
|
||||
# myDB.action('DELETE from have WHERE Matched="True"')
|
||||
|
||||
# If there's no release in the main albums tables, add the default (hybrid)
|
||||
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
|
||||
@@ -408,7 +436,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
"Type": album['Type'],
|
||||
"ReleaseCountry": album['ReleaseCountry'],
|
||||
"ReleaseFormat": album['ReleaseFormat']
|
||||
}
|
||||
}
|
||||
|
||||
if rg_exists:
|
||||
newValueDict['DateAdded'] = rg_exists['DateAdded']
|
||||
@@ -425,14 +453,17 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
newValueDict['Status'] = "Wanted"
|
||||
# Sometimes "new" albums are added to musicbrainz after their release date, so let's try to catch these
|
||||
# The first test just makes sure we have year-month-day
|
||||
elif helpers.get_age(album['ReleaseDate']) and helpers.get_age(today) - helpers.get_age(album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING:
|
||||
elif helpers.get_age(album['ReleaseDate']) and helpers.get_age(
|
||||
today) - helpers.get_age(
|
||||
album['ReleaseDate']) < 21 and headphones.CONFIG.AUTOWANT_UPCOMING:
|
||||
newValueDict['Status'] = "Wanted"
|
||||
else:
|
||||
newValueDict['Status'] = "Skipped"
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [releaseid]).fetchall()
|
||||
tracks = myDB.action('SELECT * from alltracks WHERE ReleaseID=?',
|
||||
[releaseid]).fetchall()
|
||||
|
||||
# This is used to see how many tracks you have from an album - to
|
||||
# mark it as downloaded. Default is 80%, can be set in config as
|
||||
@@ -441,7 +472,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
|
||||
if total_track_count == 0:
|
||||
logger.warning("Total track count is zero for Release ID " +
|
||||
"'%s', skipping.", releaseid)
|
||||
"'%s', skipping.", releaseid)
|
||||
continue
|
||||
|
||||
for track in tracks:
|
||||
@@ -449,35 +480,43 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
"AlbumID": rg['id']}
|
||||
|
||||
newValueDict = {"ArtistID": track['ArtistID'],
|
||||
"ArtistName": track['ArtistName'],
|
||||
"AlbumTitle": track['AlbumTitle'],
|
||||
"AlbumASIN": track['AlbumASIN'],
|
||||
"ReleaseID": track['ReleaseID'],
|
||||
"TrackTitle": track['TrackTitle'],
|
||||
"TrackDuration": track['TrackDuration'],
|
||||
"TrackNumber": track['TrackNumber'],
|
||||
"CleanName": track['CleanName'],
|
||||
"Location": track['Location'],
|
||||
"Format": track['Format'],
|
||||
"BitRate": track['BitRate']
|
||||
}
|
||||
"ArtistName": track['ArtistName'],
|
||||
"AlbumTitle": track['AlbumTitle'],
|
||||
"AlbumASIN": track['AlbumASIN'],
|
||||
"ReleaseID": track['ReleaseID'],
|
||||
"TrackTitle": track['TrackTitle'],
|
||||
"TrackDuration": track['TrackDuration'],
|
||||
"TrackNumber": track['TrackNumber'],
|
||||
"CleanName": track['CleanName'],
|
||||
"Location": track['Location'],
|
||||
"Format": track['Format'],
|
||||
"BitRate": track['BitRate']
|
||||
}
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
|
||||
have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [rg['id']]))
|
||||
have_track_count = len(
|
||||
myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL',
|
||||
[rg['id']]))
|
||||
marked_as_downloaded = False
|
||||
|
||||
if rg_exists:
|
||||
if rg_exists['Status'] == 'Skipped' and ((have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']])
|
||||
if rg_exists['Status'] == 'Skipped' and (
|
||||
(have_track_count / float(total_track_count)) >= (
|
||||
headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0)):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?',
|
||||
['Downloaded', rg['id']])
|
||||
marked_as_downloaded = True
|
||||
else:
|
||||
if (have_track_count / float(total_track_count)) >= (headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']])
|
||||
if (have_track_count / float(total_track_count)) >= (
|
||||
headphones.CONFIG.ALBUM_COMPLETION_PCT / 100.0):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?',
|
||||
['Downloaded', rg['id']])
|
||||
marked_as_downloaded = True
|
||||
|
||||
logger.info(u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
||||
logger.info(
|
||||
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
||||
cache.getThumb(AlbumID=rg['id'])
|
||||
|
||||
# Start a search for the album if it's new, hasn't been marked as
|
||||
@@ -487,7 +526,8 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
album_searches.append(rg['id'])
|
||||
else:
|
||||
if skip_log == 0:
|
||||
logger.info(u"[%s] No new releases, so no changes made to %s" % (artist['artist_name'], rg['title']))
|
||||
logger.info(u"[%s] No new releases, so no changes made to %s" % (
|
||||
artist['artist_name'], rg['title']))
|
||||
|
||||
time.sleep(3)
|
||||
finalize_update(artistid, artist['artist_name'], errors)
|
||||
@@ -499,7 +539,9 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
||||
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
|
||||
|
||||
if errors:
|
||||
logger.info("[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (artist['artist_name'], artist['artist_name']))
|
||||
logger.info(
|
||||
"[%s] Finished updating artist: %s but with errors, so not marking it as updated in the database" % (
|
||||
artist['artist_name'], artist['artist_name']))
|
||||
else:
|
||||
myDB.action('DELETE FROM newartists WHERE ArtistName = ?', [artist['artist_name']])
|
||||
logger.info(u"Updating complete for: %s" % artist['artist_name'])
|
||||
@@ -518,10 +560,18 @@ def finalize_update(artistid, artistname, errors=False):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
|
||||
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [artistid]))
|
||||
#havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artistname]))
|
||||
latestalbum = myDB.action(
|
||||
'SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC',
|
||||
[artistid]).fetchone()
|
||||
totaltracks = len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")',
|
||||
[artistid]))
|
||||
# havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [artistid])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['artist_name']]))
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[artistid])) + len(
|
||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[artistname]))
|
||||
|
||||
controlValueDict = {"ArtistID": artistid}
|
||||
|
||||
@@ -544,7 +594,6 @@ def finalize_update(artistid, artistname, errors=False):
|
||||
|
||||
|
||||
def addReleaseById(rid, rgid=None):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# Create minimum info upfront if added from searchresults
|
||||
@@ -563,14 +612,18 @@ def addReleaseById(rid, rgid=None):
|
||||
rgid = None
|
||||
artistid = None
|
||||
release_dict = None
|
||||
results = myDB.select("SELECT albums.ArtistID, releases.ReleaseGroupID from releases, albums WHERE releases.ReleaseID=? and releases.ReleaseGroupID=albums.AlbumID LIMIT 1", [rid])
|
||||
results = myDB.select(
|
||||
"SELECT albums.ArtistID, releases.ReleaseGroupID from releases, albums WHERE releases.ReleaseID=? and releases.ReleaseGroupID=albums.AlbumID LIMIT 1",
|
||||
[rid])
|
||||
for result in results:
|
||||
rgid = result['ReleaseGroupID']
|
||||
artistid = result['ArtistID']
|
||||
logger.debug("Found a cached releaseid : releasegroupid relationship: " + rid + " : " + rgid)
|
||||
logger.debug(
|
||||
"Found a cached releaseid : releasegroupid relationship: " + rid + " : " + rgid)
|
||||
if not rgid:
|
||||
#didn't find it in the cache, get the information from MB
|
||||
logger.debug("Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID")
|
||||
# didn't find it in the cache, get the information from MB
|
||||
logger.debug(
|
||||
"Didn't find releaseID " + rid + " in the cache. Looking up its ReleaseGroupID")
|
||||
try:
|
||||
release_dict = mb.getRelease(rid)
|
||||
except Exception as e:
|
||||
@@ -587,10 +640,10 @@ def addReleaseById(rid, rgid=None):
|
||||
rgid = release_dict['rgid']
|
||||
artistid = release_dict['artist_id']
|
||||
|
||||
#we don't want to make more calls to MB here unless we have to, could be happening quite a lot
|
||||
# we don't want to make more calls to MB here unless we have to, could be happening quite a lot
|
||||
rg_exists = myDB.select("SELECT * from albums WHERE AlbumID=?", [rgid])
|
||||
|
||||
#make sure the artist exists since I don't know what happens later if it doesn't
|
||||
# make sure the artist exists since I don't know what happens later if it doesn't
|
||||
artist_exists = myDB.select("SELECT * from artists WHERE ArtistID=?", [artistid])
|
||||
|
||||
if not artist_exists and release_dict:
|
||||
@@ -599,7 +652,8 @@ def addReleaseById(rid, rgid=None):
|
||||
else:
|
||||
sortname = release_dict['artist_name']
|
||||
|
||||
logger.info(u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
|
||||
logger.info(
|
||||
u"Now manually adding: " + release_dict['artist_name'] + " - with status Paused")
|
||||
controlValueDict = {"ArtistID": release_dict['artist_id']}
|
||||
newValueDict = {"ArtistName": release_dict['artist_name'],
|
||||
"ArtistSortName": sortname,
|
||||
@@ -624,13 +678,14 @@ def addReleaseById(rid, rgid=None):
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
elif not artist_exists and not release_dict:
|
||||
logger.error("Artist does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
logger.error(
|
||||
"Artist does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
if status == 'Loading':
|
||||
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
|
||||
return
|
||||
|
||||
if not rg_exists and release_dict or status == 'Loading' and release_dict: #it should never be the case that we have an rg and not the artist
|
||||
#but if it is this will fail
|
||||
if not rg_exists and release_dict or status == 'Loading' and release_dict: # it should never be the case that we have an rg and not the artist
|
||||
# but if it is this will fail
|
||||
logger.info(u"Now adding-by-id album (" + release_dict['title'] + ") from id: " + rgid)
|
||||
controlValueDict = {"AlbumID": rgid}
|
||||
if status != 'Loading':
|
||||
@@ -639,7 +694,8 @@ def addReleaseById(rid, rgid=None):
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ReleaseID": rgid,
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['title'] if 'title' in release_dict else release_dict['rg_title'],
|
||||
"AlbumTitle": release_dict['title'] if 'title' in release_dict else
|
||||
release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"ReleaseDate": release_dict['date'],
|
||||
"DateAdded": helpers.today(),
|
||||
@@ -650,41 +706,48 @@ def addReleaseById(rid, rgid=None):
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
#keep a local cache of these so that external programs that are adding releasesByID don't hammer MB
|
||||
# keep a local cache of these so that external programs that are adding releasesByID don't hammer MB
|
||||
myDB.action('INSERT INTO releases VALUES( ?, ?)', [rid, release_dict['rgid']])
|
||||
|
||||
for track in release_dict['tracks']:
|
||||
cleanname = helpers.cleanName(release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
|
||||
cleanname = helpers.cleanName(
|
||||
release_dict['artist_name'] + ' ' + release_dict['rg_title'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"AlbumID": rgid}
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['rg_title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format, Matched from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format, Matched from have WHERE CleanName=?',
|
||||
[cleanname]).fetchone()
|
||||
|
||||
if not match:
|
||||
match = myDB.action('SELECT Location, BitRate, Format, Matched from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release_dict['artist_name'], release_dict['rg_title'], track['title']]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format, Matched from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[release_dict['artist_name'], release_dict['rg_title'],
|
||||
track['title']]).fetchone()
|
||||
|
||||
#if not match:
|
||||
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
# if not match:
|
||||
# match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
|
||||
if match:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('DELETE from have WHERE Location=?', [match['Location']])
|
||||
# myDB.action('DELETE from have WHERE Location=?', [match['Location']])
|
||||
|
||||
# If the album has been scanned before adding the release it will be unmatched, update to matched
|
||||
if match['Matched'] == 'Failed':
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release_dict['rgid'], match['Location']))
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?',
|
||||
(release_dict['rgid'], match['Location']))
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
@@ -703,7 +766,8 @@ def addReleaseById(rid, rgid=None):
|
||||
searcher.searchforalbum(rgid, False)
|
||||
|
||||
elif not rg_exists and not release_dict:
|
||||
logger.error("ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
logger.error(
|
||||
"ReleaseGroup does not exist in the database and did not get a valid response from MB. Skipping release.")
|
||||
if status == 'Loading':
|
||||
myDB.action("DELETE FROM albums WHERE AlbumID=?", [rgid])
|
||||
return
|
||||
@@ -811,11 +875,13 @@ def getHybridRelease(fullreleaselist):
|
||||
|
||||
sortable_release_list.sort(key=lambda x: getSortableReleaseDate(x['releasedate']))
|
||||
|
||||
average_tracks = sum(x['trackscount'] for x in sortable_release_list) / float(len(sortable_release_list))
|
||||
average_tracks = sum(x['trackscount'] for x in sortable_release_list) / float(
|
||||
len(sortable_release_list))
|
||||
for item in sortable_release_list:
|
||||
item['trackscount_delta'] = abs(average_tracks - item['trackscount'])
|
||||
|
||||
a = helpers.multikeysort(sortable_release_list, ['-hasasin', 'country', 'format', 'trackscount_delta'])
|
||||
a = helpers.multikeysort(sortable_release_list,
|
||||
['-hasasin', 'country', 'format', 'trackscount_delta'])
|
||||
|
||||
release_dict = {'ReleaseDate': sortable_release_list[0]['releasedate'],
|
||||
'Tracks': a[0]['tracks'],
|
||||
|
||||
@@ -14,15 +14,14 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import random
|
||||
import headphones
|
||||
import headphones.lock
|
||||
|
||||
from headphones import db, logger, request
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
TIMEOUT = 60.0 # seconds
|
||||
REQUEST_LIMIT = 1.0 / 5 # seconds
|
||||
import headphones
|
||||
import headphones.lock
|
||||
from headphones import db, logger, request
|
||||
|
||||
TIMEOUT = 60.0 # seconds
|
||||
REQUEST_LIMIT = 1.0 / 5 # seconds
|
||||
ENTRY_POINT = "http://ws.audioscrobbler.com/2.0/"
|
||||
API_KEY = "395e6ec6bb557382fc41fde867bce66f"
|
||||
|
||||
|
||||
@@ -14,17 +14,16 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
import headphones
|
||||
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
|
||||
from headphones import db, logger, helpers, importer, lastfm
|
||||
|
||||
|
||||
# You can scan a single directory and append it to the current library by
|
||||
# specifying append=True, ArtistID and ArtistName.
|
||||
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
cron=False, artistScan=False):
|
||||
|
||||
cron=False, artistScan=False):
|
||||
if cron and not headphones.CONFIG.LIBRARYSCAN:
|
||||
return
|
||||
|
||||
@@ -40,7 +39,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
dir = dir.encode(headphones.SYS_ENCODING)
|
||||
|
||||
if not os.path.isdir(dir):
|
||||
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING,
|
||||
'replace'))
|
||||
return
|
||||
|
||||
myDB = db.DBConnection()
|
||||
@@ -50,13 +50,16 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
if not append:
|
||||
# Clean up bad filepaths
|
||||
tracks = myDB.select('SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
|
||||
tracks = myDB.select(
|
||||
'SELECT Location from alltracks WHERE Location IS NOT NULL UNION SELECT Location from tracks WHERE Location IS NOT NULL')
|
||||
|
||||
for track in tracks:
|
||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
||||
if not os.path.isfile(encoded_track_string):
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, track['Location']])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, track['Location']])
|
||||
|
||||
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
|
||||
|
||||
@@ -67,7 +70,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
# Make sure deleted files get accounted for when updating artist track counts
|
||||
new_artists.append(track['ArtistName'])
|
||||
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
||||
logger.info('File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
bitrates = []
|
||||
song_list = []
|
||||
@@ -89,9 +94,14 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
latest_subdirectory.append(subdirectory)
|
||||
|
||||
if file_count == 0 and r.replace(dir, '') != '':
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
elif latest_subdirectory[file_count] != latest_subdirectory[file_count - 1] and file_count != 0:
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (dir.decode(headphones.SYS_ENCODING, 'replace'), subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (
|
||||
dir.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
elif latest_subdirectory[file_count] != latest_subdirectory[
|
||||
file_count - 1] and file_count != 0:
|
||||
logger.info("[%s] Now scanning subdirectory %s" % (
|
||||
dir.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
subdirectory.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
|
||||
song = os.path.join(r, files)
|
||||
|
||||
@@ -102,10 +112,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
try:
|
||||
f = MediaFile(song)
|
||||
except (FileTypeError, UnreadableFileError):
|
||||
logger.warning("Cannot read media file '%s', skipping. It may be corrupted or not a media file.", unicode_song_path)
|
||||
logger.warning(
|
||||
"Cannot read media file '%s', skipping. It may be corrupted or not a media file.",
|
||||
unicode_song_path)
|
||||
continue
|
||||
except IOError:
|
||||
logger.warning("Cannnot read media file '%s', skipping. Does the file exists?", unicode_song_path)
|
||||
logger.warning("Cannnot read media file '%s', skipping. Does the file exists?",
|
||||
unicode_song_path)
|
||||
continue
|
||||
|
||||
# Grab the bitrates for the auto detect bit rate option
|
||||
@@ -131,45 +144,52 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
controlValueDict = {'Location': unicode_song_path}
|
||||
|
||||
newValueDict = {'TrackID': f.mb_trackid,
|
||||
#'ReleaseID' : f.mb_albumid,
|
||||
'ArtistName': f_artist,
|
||||
'AlbumTitle': f.album,
|
||||
'TrackNumber': f.track,
|
||||
'TrackLength': f.length,
|
||||
'Genre': f.genre,
|
||||
'Date': f.date,
|
||||
'TrackTitle': f.title,
|
||||
'BitRate': f.bitrate,
|
||||
'Format': f.format,
|
||||
'CleanName': CleanName
|
||||
}
|
||||
# 'ReleaseID' : f.mb_albumid,
|
||||
'ArtistName': f_artist,
|
||||
'AlbumTitle': f.album,
|
||||
'TrackNumber': f.track,
|
||||
'TrackLength': f.length,
|
||||
'Genre': f.genre,
|
||||
'Date': f.date,
|
||||
'TrackTitle': f.title,
|
||||
'BitRate': f.bitrate,
|
||||
'Format': f.format,
|
||||
'CleanName': CleanName
|
||||
}
|
||||
|
||||
#song_list.append(song_dict)
|
||||
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone()
|
||||
#Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
|
||||
# song_list.append(song_dict)
|
||||
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?",
|
||||
[unicode_song_path]).fetchone()
|
||||
# Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
|
||||
if not check_exist_song:
|
||||
#This is a new track
|
||||
# This is a new track
|
||||
if f_artist:
|
||||
new_artists.append(f_artist)
|
||||
myDB.upsert("have", newValueDict, controlValueDict)
|
||||
new_song_count += 1
|
||||
else:
|
||||
if check_exist_song['ArtistName'] != f_artist or check_exist_song['AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
|
||||
#Important track metadata has been modified, need to run matcher again
|
||||
if check_exist_song['ArtistName'] != f_artist or check_exist_song[
|
||||
'AlbumTitle'] != f.album or check_exist_song['TrackTitle'] != f.title:
|
||||
# Important track metadata has been modified, need to run matcher again
|
||||
if f_artist and f_artist != check_exist_song['ArtistName']:
|
||||
new_artists.append(f_artist)
|
||||
elif f_artist and f_artist == check_exist_song['ArtistName'] and check_exist_song['Matched'] != "Ignored":
|
||||
elif f_artist and f_artist == check_exist_song['ArtistName'] and \
|
||||
check_exist_song['Matched'] != "Ignored":
|
||||
new_artists.append(f_artist)
|
||||
else:
|
||||
continue
|
||||
|
||||
newValueDict['Matched'] = None
|
||||
myDB.upsert("have", newValueDict, controlValueDict)
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, unicode_song_path])
|
||||
myDB.action(
|
||||
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, unicode_song_path])
|
||||
myDB.action(
|
||||
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
||||
[None, None, None, unicode_song_path])
|
||||
new_song_count += 1
|
||||
else:
|
||||
#This track information hasn't changed
|
||||
# This track information hasn't changed
|
||||
if f_artist and check_exist_song['Matched'] != "Ignored":
|
||||
new_artists.append(f_artist)
|
||||
|
||||
@@ -177,9 +197,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
# Now we start track matching
|
||||
logger.info("%s new/modified songs found and added to the database" % new_song_count)
|
||||
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"])
|
||||
total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0]
|
||||
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
|
||||
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
||||
[dir.decode(headphones.SYS_ENCODING, 'replace') + "%"])
|
||||
total_number_of_songs = \
|
||||
myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?",
|
||||
[dir.decode(headphones.SYS_ENCODING, 'replace') + "%"]).fetchone()[0]
|
||||
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(
|
||||
headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
|
||||
|
||||
# Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid)
|
||||
# When we insert into the database, the tracks with the most specific information will overwrite the more general matches
|
||||
@@ -205,22 +229,24 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
if completion_percentage % 10 == 0:
|
||||
logger.info("Track matching is " + str(completion_percentage) + "% complete")
|
||||
|
||||
#THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT
|
||||
#EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION,
|
||||
#ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
|
||||
#(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
|
||||
# THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT
|
||||
# EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION,
|
||||
# ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
|
||||
# (IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
|
||||
|
||||
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
|
||||
|
||||
track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
track = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
have_updated = False
|
||||
if track:
|
||||
controlValueDict = {'ArtistName': track['ArtistName'],
|
||||
'AlbumTitle': track['AlbumTitle'],
|
||||
'TrackTitle': track['TrackTitle']}
|
||||
'AlbumTitle': track['AlbumTitle'],
|
||||
'TrackTitle': track['TrackTitle']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
@@ -228,12 +254,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
else:
|
||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
|
||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?',
|
||||
[song['CleanName']]).fetchone()
|
||||
if track:
|
||||
controlValueDict = {'CleanName': track['CleanName']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
@@ -246,26 +273,30 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
have_updated = True
|
||||
|
||||
alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
alltrack = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = {'ArtistName': alltrack['ArtistName'],
|
||||
'AlbumTitle': alltrack['AlbumTitle'],
|
||||
'TrackTitle': alltrack['TrackTitle']}
|
||||
'AlbumTitle': alltrack['AlbumTitle'],
|
||||
'TrackTitle': alltrack['TrackTitle']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
newValueDict2 = {'Matched': alltrack['AlbumID']}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
else:
|
||||
alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
|
||||
alltrack = myDB.action(
|
||||
'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?',
|
||||
[song['CleanName']]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = {'CleanName': alltrack['CleanName']}
|
||||
newValueDict = {'Location': song['Location'],
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
'BitRate': song['BitRate'],
|
||||
'Format': song['Format']}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = {'Location': song['Location']}
|
||||
@@ -283,9 +314,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
newValueDict2 = {'Matched': "Failed"}
|
||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||
|
||||
#######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
|
||||
#######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
|
||||
|
||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING,
|
||||
'replace'))
|
||||
|
||||
if not append or artistScan:
|
||||
logger.info('Updating scanned artist track counts')
|
||||
@@ -294,29 +326,32 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
unique_artists = {}.fromkeys(new_artists).keys()
|
||||
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
|
||||
|
||||
#There was a bug where artists with special characters (-,') would show up in new artists.
|
||||
# There was a bug where artists with special characters (-,') would show up in new artists.
|
||||
artist_list = [
|
||||
x for x in unique_artists
|
||||
if helpers.cleanName(x).lower() not in [
|
||||
helpers.cleanName(y[0]).lower()
|
||||
for y in current_artists
|
||||
]
|
||||
]
|
||||
]
|
||||
artists_checked = [
|
||||
x for x in unique_artists
|
||||
if helpers.cleanName(x).lower() in [
|
||||
helpers.cleanName(y[0]).lower()
|
||||
for y in current_artists
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
# Update track counts
|
||||
for artist in artists_checked:
|
||||
# Have tracks are selected from tracks table and not all tracks because of duplicates
|
||||
# We update the track count upon an album switch to compliment this
|
||||
havetracks = (
|
||||
len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist]))
|
||||
+ len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist]))
|
||||
len(myDB.select(
|
||||
'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
|
||||
[artist])) + len(myDB.select(
|
||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[artist]))
|
||||
)
|
||||
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
|
||||
# (can fix by getting rid of second len statement)
|
||||
@@ -330,7 +365,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
importer.artistlist_to_mbids(artist_list)
|
||||
else:
|
||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||
#myDB.action('DELETE from newartists')
|
||||
# myDB.action('DELETE from newartists')
|
||||
for artist in artist_list:
|
||||
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
|
||||
|
||||
@@ -341,7 +376,11 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
# If we're appending a new album to the database, update the artists total track counts
|
||||
logger.info('Updating artist track counts')
|
||||
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName]))
|
||||
havetracks = len(
|
||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
||||
[ArtistID])) + len(myDB.select(
|
||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
||||
[ArtistName]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||
|
||||
if not append:
|
||||
@@ -352,18 +391,21 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
||||
|
||||
logger.info('Library scan complete')
|
||||
|
||||
#ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
|
||||
|
||||
# ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
|
||||
|
||||
|
||||
def update_album_status(AlbumID=None):
|
||||
myDB = db.DBConnection()
|
||||
logger.info('Counting matched tracks to mark albums as skipped/downloaded')
|
||||
if AlbumID:
|
||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID])
|
||||
album_status_updater = myDB.action(
|
||||
'SELECT AlbumID, AlbumTitle, Status from albums WHERE AlbumID=?', [AlbumID])
|
||||
else:
|
||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums')
|
||||
for album in album_status_updater:
|
||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?', [album['AlbumID']])
|
||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?',
|
||||
[album['AlbumID']])
|
||||
total_tracks = 0
|
||||
have_tracks = 0
|
||||
for track in track_counter:
|
||||
@@ -383,7 +425,7 @@ def update_album_status(AlbumID=None):
|
||||
# I think we can only automatically change Skipped->Downloaded when updating
|
||||
# There was a bug report where this was causing infinite downloads if the album was
|
||||
# recent, but matched to less than 80%. It would go Downloaded->Skipped->Wanted->Downloaded->Skipped->Wanted->etc....
|
||||
#else:
|
||||
# else:
|
||||
# if album['Status'] == "Skipped" or album['Status'] == "Downloaded":
|
||||
# new_album_status = "Skipped"
|
||||
# else:
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
Locking-related classes
|
||||
"""
|
||||
|
||||
import headphones.logger
|
||||
import time
|
||||
import threading
|
||||
import Queue
|
||||
|
||||
import headphones.logger
|
||||
|
||||
|
||||
class TimedLock(object):
|
||||
"""
|
||||
|
||||
@@ -13,24 +13,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import helpers
|
||||
|
||||
from logutils.queue import QueueHandler, QueueListener
|
||||
from logging import handlers
|
||||
|
||||
import multiprocessing
|
||||
import contextlib
|
||||
import headphones
|
||||
import threading
|
||||
import traceback
|
||||
import logging
|
||||
import errno
|
||||
import sys
|
||||
|
||||
import os
|
||||
from headphones import helpers
|
||||
from logutils.queue import QueueHandler, QueueListener
|
||||
import headphones
|
||||
|
||||
|
||||
# These settings are for file logging only
|
||||
FILENAME = "headphones.log"
|
||||
MAX_SIZE = 1000000 # 1 MB
|
||||
MAX_SIZE = 1000000 # 1 MB
|
||||
MAX_FILES = 5
|
||||
|
||||
# Headphones logger
|
||||
@@ -39,6 +39,7 @@ logger = logging.getLogger("headphones")
|
||||
# Global queue for multiprocessing logging
|
||||
queue = None
|
||||
|
||||
|
||||
class LogListHandler(logging.Handler):
|
||||
"""
|
||||
Log handler for Web UI.
|
||||
@@ -71,8 +72,8 @@ def listener():
|
||||
# http://stackoverflow.com/questions/2009278 for more information.
|
||||
if e.errno == errno.EACCES:
|
||||
logger.warning("Multiprocess logging disabled, because "
|
||||
"current user cannot map shared memory. You won't see any" \
|
||||
"logging generated by the worker processed.")
|
||||
"current user cannot map shared memory. You won't see any" \
|
||||
"logging generated by the worker processed.")
|
||||
|
||||
# Multiprocess logging may be disabled.
|
||||
if not queue:
|
||||
@@ -149,8 +150,10 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||
if log_dir:
|
||||
filename = os.path.join(log_dir, FILENAME)
|
||||
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE, backupCount=MAX_FILES)
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(levelname)-7s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
file_handler = handlers.RotatingFileHandler(filename, maxBytes=MAX_SIZE,
|
||||
backupCount=MAX_FILES)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
@@ -158,7 +161,8 @@ def initLogger(console=False, log_dir=False, verbose=False):
|
||||
|
||||
# Setup console logger
|
||||
if console:
|
||||
console_formatter = logging.Formatter('%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(levelname)s :: %(threadName)s : %(message)s', '%d-%b-%Y %H:%M:%S')
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(console_formatter)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
@@ -212,11 +216,13 @@ def initHooks(global_exceptions=True, thread_exceptions=True, pass_original=True
|
||||
raise
|
||||
except:
|
||||
excepthook(*sys.exc_info())
|
||||
|
||||
self.run = new_run
|
||||
|
||||
# Monkey patch the run() by monkey patching the __init__ method
|
||||
threading.Thread.__init__ = new_init
|
||||
|
||||
|
||||
# Expose logger methods
|
||||
info = logger.info
|
||||
warn = logger.warn
|
||||
|
||||
@@ -13,18 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import htmlentitydefs
|
||||
|
||||
import re
|
||||
from headphones import logger, request
|
||||
|
||||
|
||||
def getLyrics(artist, song):
|
||||
|
||||
params = {"artist": artist.encode('utf-8'),
|
||||
"song": song.encode('utf-8'),
|
||||
"fmt": 'xml'
|
||||
}
|
||||
"song": song.encode('utf-8'),
|
||||
"fmt": 'xml'
|
||||
}
|
||||
|
||||
url = 'http://lyrics.wikia.com/api.php'
|
||||
data = request.request_minidom(url, params=params)
|
||||
@@ -46,10 +45,13 @@ def getLyrics(artist, song):
|
||||
logger.warn('Error fetching lyrics from: %s' % lyricsurl)
|
||||
return
|
||||
|
||||
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage)
|
||||
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(
|
||||
lyricspage)
|
||||
|
||||
if not m:
|
||||
m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage)
|
||||
m = re.compile(
|
||||
'''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(
|
||||
lyricspage)
|
||||
if m:
|
||||
return u'(Instrumental)'
|
||||
else:
|
||||
@@ -67,12 +69,12 @@ def convert_html_entities(s):
|
||||
if len(matches) > 0:
|
||||
hits = set(matches)
|
||||
for hit in hits:
|
||||
name = hit[2:-1]
|
||||
try:
|
||||
entnum = int(name)
|
||||
s = s.replace(hit, unichr(entnum))
|
||||
except ValueError:
|
||||
pass
|
||||
name = hit[2:-1]
|
||||
try:
|
||||
entnum = int(name)
|
||||
s = s.replace(hit, unichr(entnum))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
matches = re.findall("&\w+;", s)
|
||||
hits = set(matches)
|
||||
|
||||
373
headphones/mb.py
373
headphones/mb.py
@@ -31,12 +31,12 @@ except ImportError:
|
||||
|
||||
mb_lock = headphones.lock.TimedLock(0)
|
||||
|
||||
|
||||
# Quick fix to add mirror switching on the fly. Need to probably return the mbhost & mbport that's
|
||||
# being used, so we can send those values to the log
|
||||
|
||||
|
||||
def startmb():
|
||||
|
||||
mbuser = None
|
||||
mbpass = None
|
||||
|
||||
@@ -66,7 +66,7 @@ def startmb():
|
||||
if sleepytime == 0:
|
||||
musicbrainzngs.set_rate_limit(False)
|
||||
else:
|
||||
#calling it with an it ends up blocking all requests after the first
|
||||
# calling it with an it ends up blocking all requests after the first
|
||||
musicbrainzngs.set_rate_limit(limit_or_interval=float(sleepytime))
|
||||
mb_lock.minimum_delta = sleepytime
|
||||
|
||||
@@ -81,65 +81,72 @@ def startmb():
|
||||
if not headphones.CONFIG.CUSTOMAUTH and headphones.CONFIG.MIRROR == "custom":
|
||||
musicbrainzngs.disable_hpauth()
|
||||
|
||||
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
|
||||
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i',
|
||||
mbhost, mbport, sleepytime)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def findArtist(name, limit=1):
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
|
||||
criteria = {'artist': name.lower()}
|
||||
criteria = {'artist': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
|
||||
except ValueError as e:
|
||||
if "at least one query term is required" in e.message:
|
||||
logger.error("Tried to search without a term, or an empty one. Provided artist (probably emtpy): %s", name)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
with mb_lock:
|
||||
try:
|
||||
artistResults = musicbrainzngs.search_artists(limit=limit, **criteria)['artist-list']
|
||||
except ValueError as e:
|
||||
if "at least one query term is required" in e.message:
|
||||
logger.error(
|
||||
"Tried to search without a term, or an empty one. Provided artist (probably emtpy): %s",
|
||||
name)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not artistResults:
|
||||
return False
|
||||
for result in artistResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['sort-name'])
|
||||
if result['name'] != uniquename and limit == 1:
|
||||
logger.info(
|
||||
'Found an artist with a disambiguation: %s - doing an album based search' % name)
|
||||
artistdict = findArtistbyAlbum(name)
|
||||
if not artistdict:
|
||||
logger.info(
|
||||
'Cannot determine the best match from an artist/album search. Using top match instead')
|
||||
artistlist.append({
|
||||
# Just need the artist id if the limit is 1
|
||||
# 'name': unicode(result['sort-name']),
|
||||
# 'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
# 'score': int(result['ext:score'])
|
||||
})
|
||||
else:
|
||||
artistlist.append(artistdict)
|
||||
else:
|
||||
artistlist.append({
|
||||
'name': unicode(result['sort-name']),
|
||||
'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/artist/" + result['id']),
|
||||
# probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return artistlist
|
||||
|
||||
if not artistResults:
|
||||
return False
|
||||
for result in artistResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['sort-name'])
|
||||
if result['name'] != uniquename and limit == 1:
|
||||
logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name)
|
||||
artistdict = findArtistbyAlbum(name)
|
||||
if not artistdict:
|
||||
logger.info('Cannot determine the best match from an artist/album search. Using top match instead')
|
||||
artistlist.append({
|
||||
# Just need the artist id if the limit is 1
|
||||
# 'name': unicode(result['sort-name']),
|
||||
# 'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
# 'score': int(result['ext:score'])
|
||||
})
|
||||
else:
|
||||
artistlist.append(artistdict)
|
||||
else:
|
||||
artistlist.append({
|
||||
'name': unicode(result['sort-name']),
|
||||
'uniquename': uniquename,
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return artistlist
|
||||
|
||||
def findRelease(name, limit=1, artist=None):
|
||||
releaselist = []
|
||||
@@ -157,8 +164,9 @@ def findRelease(name, limit=1, artist=None):
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)['release-list']
|
||||
except musicbrainzngs.WebServiceError as e: #need to update exceptions
|
||||
releaseResults = musicbrainzngs.search_releases(query=name, limit=limit, artist=artist)[
|
||||
'release-list']
|
||||
except musicbrainzngs.WebServiceError as e: # need to update exceptions
|
||||
logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
@@ -202,55 +210,61 @@ def findRelease(name, limit=1, artist=None):
|
||||
rg_type = secondary_type
|
||||
|
||||
releaselist.append({
|
||||
'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
|
||||
'title': unicode(title),
|
||||
'id': unicode(result['artist-credit'][0]['artist']['id']),
|
||||
'albumid': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),#probably needs to be changed
|
||||
'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),#probably needs to be changed
|
||||
'score': int(result['ext:score']),
|
||||
'date': unicode(result['date']) if 'date' in result else '',
|
||||
'country': unicode(result['country']) if 'country' in result else '',
|
||||
'formats': unicode(formats),
|
||||
'tracks': unicode(tracks),
|
||||
'rgid': unicode(result['release-group']['id']),
|
||||
'rgtype': unicode(rg_type)
|
||||
})
|
||||
'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
|
||||
'title': unicode(title),
|
||||
'id': unicode(result['artist-credit'][0]['artist']['id']),
|
||||
'albumid': unicode(result['id']),
|
||||
'url': unicode(
|
||||
"http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),
|
||||
# probably needs to be changed
|
||||
'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),
|
||||
# probably needs to be changed
|
||||
'score': int(result['ext:score']),
|
||||
'date': unicode(result['date']) if 'date' in result else '',
|
||||
'country': unicode(result['country']) if 'country' in result else '',
|
||||
'formats': unicode(formats),
|
||||
'tracks': unicode(tracks),
|
||||
'rgid': unicode(result['release-group']['id']),
|
||||
'rgtype': unicode(rg_type)
|
||||
})
|
||||
return releaselist
|
||||
|
||||
|
||||
def findSeries(name, limit=1):
|
||||
serieslist = []
|
||||
seriesResults = None
|
||||
serieslist = []
|
||||
seriesResults = None
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"' + name + '"'
|
||||
|
||||
criteria = {'series': name.lower()}
|
||||
criteria = {'series': name.lower()}
|
||||
|
||||
with mb_lock:
|
||||
try:
|
||||
seriesResults = musicbrainzngs.search_series(limit=limit, **criteria)['series-list']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
with mb_lock:
|
||||
try:
|
||||
seriesResults = musicbrainzngs.search_series(limit=limit, **criteria)['series-list']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not seriesResults:
|
||||
return False
|
||||
for result in seriesResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['name'])
|
||||
serieslist.append({
|
||||
'uniquename': uniquename,
|
||||
'name': unicode(result['name']),
|
||||
'type': unicode(result['type']),
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/series/" + result['id']),
|
||||
# probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return serieslist
|
||||
|
||||
if not seriesResults:
|
||||
return False
|
||||
for result in seriesResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['name'] + " (" + result['disambiguation'] + ")")
|
||||
else:
|
||||
uniquename = unicode(result['name'])
|
||||
serieslist.append({
|
||||
'uniquename': uniquename,
|
||||
'name': unicode(result['name']),
|
||||
'type': unicode(result['type']),
|
||||
'id': unicode(result['id']),
|
||||
'url': unicode("http://musicbrainz.org/series/" + result['id']),#probably needs to be changed
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return serieslist
|
||||
|
||||
def getArtist(artistid, extrasonly=False):
|
||||
artist_dict = {}
|
||||
@@ -271,7 +285,9 @@ def getArtist(artistid, extrasonly=False):
|
||||
newRgs = newRgs['release-group-list']
|
||||
artist['release-group-list'] += newRgs
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (
|
||||
artistid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
except Exception as e:
|
||||
pass
|
||||
@@ -285,7 +301,7 @@ def getArtist(artistid, extrasonly=False):
|
||||
|
||||
if not extrasonly:
|
||||
for rg in artist['release-group-list']:
|
||||
if "secondary-type-list" in rg.keys(): #only add releases without a secondary type
|
||||
if "secondary-type-list" in rg.keys(): # only add releases without a secondary type
|
||||
continue
|
||||
releasegroups.append({
|
||||
'title': unicode(rg['title']),
|
||||
@@ -299,7 +315,8 @@ def getArtist(artistid, extrasonly=False):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
try:
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?', [artistid]).fetchone()
|
||||
db_artist = myDB.action('SELECT IncludeExtras, Extras from artists WHERE ArtistID=?',
|
||||
[artistid]).fetchone()
|
||||
includeExtras = db_artist['IncludeExtras']
|
||||
except IndexError:
|
||||
includeExtras = False
|
||||
@@ -335,7 +352,9 @@ def getArtist(artistid, extrasonly=False):
|
||||
newRgs = newRgs['release-group-list']
|
||||
mb_extras_list += newRgs
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (
|
||||
artistid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
for rg in mb_extras_list:
|
||||
@@ -354,14 +373,18 @@ def getArtist(artistid, extrasonly=False):
|
||||
artist_dict['releasegroups'] = releasegroups
|
||||
return artist_dict
|
||||
|
||||
|
||||
def getSeries(seriesid):
|
||||
series_dict = {}
|
||||
series = None
|
||||
try:
|
||||
with mb_lock:
|
||||
series = musicbrainzngs.get_series_by_id(seriesid,includes=['release-group-rels'])['series']
|
||||
series = musicbrainzngs.get_series_by_id(seriesid, includes=['release-group-rels'])[
|
||||
'series']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve series information from MusicBrainz failed for seriesid: %s (%s)' % (seriesid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve series information from MusicBrainz failed for seriesid: %s (%s)' % (
|
||||
seriesid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
except Exception as e:
|
||||
pass
|
||||
@@ -370,7 +393,8 @@ def getSeries(seriesid):
|
||||
return False
|
||||
|
||||
if 'disambiguation' in series:
|
||||
series_dict['artist_name'] = unicode(series['name'] + " (" + unicode(series['disambiguation']) + ")")
|
||||
series_dict['artist_name'] = unicode(
|
||||
series['name'] + " (" + unicode(series['disambiguation']) + ")")
|
||||
else:
|
||||
series_dict['artist_name'] = unicode(series['name'])
|
||||
|
||||
@@ -379,14 +403,15 @@ def getSeries(seriesid):
|
||||
for rg in series['release_group-relation-list']:
|
||||
releasegroup = rg['release-group']
|
||||
releasegroups.append({
|
||||
'title':releasegroup['title'],
|
||||
'date':releasegroup['first-release-date'],
|
||||
'id':releasegroup['id'],
|
||||
'type':rg['type']
|
||||
})
|
||||
'title': releasegroup['title'],
|
||||
'date': releasegroup['first-release-date'],
|
||||
'id': releasegroup['id'],
|
||||
'type': rg['type']
|
||||
})
|
||||
series_dict['releasegroups'] = releasegroups
|
||||
return series_dict
|
||||
|
||||
|
||||
def getReleaseGroup(rgid):
|
||||
"""
|
||||
Returns a list of releases in a release group
|
||||
@@ -398,7 +423,9 @@ def getReleaseGroup(rgid):
|
||||
rgid, ["artists", "releases", "media", "discids", ])
|
||||
releaseGroup = releaseGroup['release-group']
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||
rgid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not releaseGroup:
|
||||
@@ -417,11 +444,16 @@ def getRelease(releaseid, include_artist_info=True):
|
||||
try:
|
||||
with mb_lock:
|
||||
if include_artist_info:
|
||||
results = musicbrainzngs.get_release_by_id(releaseid, ["artists", "release-groups", "media", "recordings"]).get('release')
|
||||
results = musicbrainzngs.get_release_by_id(releaseid,
|
||||
["artists", "release-groups", "media",
|
||||
"recordings"]).get('release')
|
||||
else:
|
||||
results = musicbrainzngs.get_release_by_id(releaseid, ["media", "recordings"]).get('release')
|
||||
results = musicbrainzngs.get_release_by_id(releaseid, ["media", "recordings"]).get(
|
||||
'release')
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (
|
||||
releaseid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not results:
|
||||
@@ -449,7 +481,8 @@ def getRelease(releaseid, include_artist_info=True):
|
||||
try:
|
||||
release['rg_type'] = unicode(results['release-group']['type'])
|
||||
|
||||
if release['rg_type'] == 'Album' and 'secondary-type-list' in results['release-group']:
|
||||
if release['rg_type'] == 'Album' and 'secondary-type-list' in results[
|
||||
'release-group']:
|
||||
secondary_type = unicode(results['release-group']['secondary-type-list'][0])
|
||||
if secondary_type != release['rg_type']:
|
||||
release['rg_type'] = secondary_type
|
||||
@@ -469,7 +502,6 @@ def getRelease(releaseid, include_artist_info=True):
|
||||
|
||||
|
||||
def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
results = []
|
||||
|
||||
@@ -485,30 +517,33 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
newResults = musicbrainzngs.browse_releases(
|
||||
release_group=rgid,
|
||||
includes=['artist-credits', 'labels', 'recordings', 'release-groups', 'media'],
|
||||
release_status = release_status,
|
||||
release_status=release_status,
|
||||
limit=limit,
|
||||
offset=len(results))
|
||||
if 'release-list' not in newResults:
|
||||
break #may want to raise an exception here instead ?
|
||||
break # may want to raise an exception here instead ?
|
||||
newResults = newResults['release-list']
|
||||
results += newResults
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (
|
||||
rgid, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
return False
|
||||
|
||||
if not results or len(results) == 0:
|
||||
return False
|
||||
|
||||
#Clean all references to releases in dB that are no longer referenced in musicbrainz
|
||||
# Clean all references to releases in dB that are no longer referenced in musicbrainz
|
||||
release_list = []
|
||||
force_repackage1 = 0
|
||||
if len(results) != 0:
|
||||
for release_mark in results:
|
||||
release_list.append(unicode(release_mark['id']))
|
||||
release_title = release_mark['title']
|
||||
remove_missing_releases = myDB.action("SELECT ReleaseID FROM allalbums WHERE AlbumID=?", [rgid])
|
||||
remove_missing_releases = myDB.action("SELECT ReleaseID FROM allalbums WHERE AlbumID=?",
|
||||
[rgid])
|
||||
if remove_missing_releases:
|
||||
for items in remove_missing_releases:
|
||||
if items['ReleaseID'] not in release_list and items['ReleaseID'] != rgid:
|
||||
@@ -517,10 +552,13 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
myDB.action("DELETE FROM tracks WHERE ReleaseID=?", [items['ReleaseID']])
|
||||
myDB.action("DELETE FROM allalbums WHERE ReleaseID=?", [items['ReleaseID']])
|
||||
myDB.action("DELETE FROM alltracks WHERE ReleaseID=?", [items['ReleaseID']])
|
||||
logger.info("Removing all references to release %s to reflect MusicBrainz" % items['ReleaseID'])
|
||||
logger.info(
|
||||
"Removing all references to release %s to reflect MusicBrainz" % items[
|
||||
'ReleaseID'])
|
||||
force_repackage1 = 1
|
||||
else:
|
||||
logger.info("There was either an error pulling data from MusicBrainz or there might not be any releases for this category")
|
||||
logger.info(
|
||||
"There was either an error pulling data from MusicBrainz or there might not be any releases for this category")
|
||||
|
||||
num_new_releases = 0
|
||||
|
||||
@@ -528,9 +566,10 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
|
||||
release = {}
|
||||
rel_id_check = releasedata['id']
|
||||
album_checker = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [rel_id_check]).fetchone()
|
||||
album_checker = myDB.action('SELECT * from allalbums WHERE ReleaseID=?',
|
||||
[rel_id_check]).fetchone()
|
||||
if not album_checker or forcefull:
|
||||
#DELETE all references to this release since we're updating it anyway.
|
||||
# DELETE all references to this release since we're updating it anyway.
|
||||
myDB.action('DELETE from allalbums WHERE ReleaseID=?', [rel_id_check])
|
||||
myDB.action('DELETE from alltracks WHERE ReleaseID=?', [rel_id_check])
|
||||
release['AlbumTitle'] = unicode(releasedata['title'])
|
||||
@@ -539,7 +578,8 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None
|
||||
release['ReleaseID'] = releasedata['id']
|
||||
if 'release-group' not in releasedata:
|
||||
raise Exception('No release group associated with release id ' + releasedata['id'] + ' album id' + rgid)
|
||||
raise Exception('No release group associated with release id ' + releasedata[
|
||||
'id'] + ' album id' + rgid)
|
||||
release['Type'] = unicode(releasedata['release-group']['type'])
|
||||
|
||||
if release['Type'] == 'Album' and 'secondary-type-list' in releasedata['release-group']:
|
||||
@@ -547,7 +587,7 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
if secondary_type != release['Type']:
|
||||
release['Type'] = secondary_type
|
||||
|
||||
#making the assumption that the most important artist will be first in the list
|
||||
# making the assumption that the most important artist will be first in the list
|
||||
if 'artist-credit' in releasedata:
|
||||
release['ArtistID'] = unicode(releasedata['artist-credit'][0]['artist']['id'])
|
||||
release['ArtistName'] = unicode(releasedata['artist-credit-phrase'])
|
||||
@@ -555,8 +595,9 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
|
||||
return False
|
||||
|
||||
release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown'
|
||||
#assuming that the list will contain media and that the format will be consistent
|
||||
release['ReleaseCountry'] = unicode(
|
||||
releasedata['country']) if 'country' in releasedata else u'Unknown'
|
||||
# assuming that the list will contain media and that the format will be consistent
|
||||
try:
|
||||
additional_medium = ''
|
||||
for position in releasedata['medium-list']:
|
||||
@@ -568,16 +609,17 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
disc_number = ''
|
||||
else:
|
||||
disc_number = str(medium_count) + 'x'
|
||||
packaged_medium = disc_number + releasedata['medium-list'][0]['format'] + additional_medium
|
||||
packaged_medium = disc_number + releasedata['medium-list'][0][
|
||||
'format'] + additional_medium
|
||||
release['ReleaseFormat'] = unicode(packaged_medium)
|
||||
except:
|
||||
release['ReleaseFormat'] = u'Unknown'
|
||||
|
||||
release['Tracks'] = getTracksFromRelease(releasedata)
|
||||
|
||||
# What we're doing here now is first updating the allalbums & alltracks table to the most
|
||||
# current info, then moving the appropriate release into the album table and its associated
|
||||
# tracks into the tracks table
|
||||
# What we're doing here now is first updating the allalbums & alltracks table to the most
|
||||
# current info, then moving the appropriate release into the album table and its associated
|
||||
# tracks into the tracks table
|
||||
controlValueDict = {"ReleaseID": release['ReleaseID']}
|
||||
|
||||
newValueDict = {"ArtistID": release['ArtistID'],
|
||||
@@ -589,13 +631,14 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
"Type": release['Type'],
|
||||
"ReleaseCountry": release['ReleaseCountry'],
|
||||
"ReleaseFormat": release['ReleaseFormat']
|
||||
}
|
||||
}
|
||||
|
||||
myDB.upsert("allalbums", newValueDict, controlValueDict)
|
||||
|
||||
for track in release['Tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
|
||||
cleanname = helpers.cleanName(
|
||||
release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": release['ReleaseID']}
|
||||
@@ -609,30 +652,37 @@ def get_new_releases(rgid, includeExtras=False, forcefull=False):
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
}
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?',
|
||||
[cleanname]).fetchone()
|
||||
|
||||
if not match:
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone()
|
||||
#if not match:
|
||||
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
match = myDB.action(
|
||||
'SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
||||
[release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone()
|
||||
# if not match:
|
||||
# match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
|
||||
if match:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location']))
|
||||
# myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?',
|
||||
(release['AlbumID'], match['Location']))
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
num_new_releases = num_new_releases + 1
|
||||
if album_checker:
|
||||
logger.info('[%s] Existing release %s (%s) updated' % (release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
logger.info('[%s] Existing release %s (%s) updated' % (
|
||||
release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
else:
|
||||
logger.info('[%s] New release %s (%s) added' % (release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
logger.info('[%s] New release %s (%s) added' % (
|
||||
release['ArtistName'], release['AlbumTitle'], rel_id_check))
|
||||
if force_repackage1 == 1:
|
||||
num_new_releases = -1
|
||||
logger.info('[%s] Forcing repackage of %s, since dB releases have been removed' % (release['ArtistName'], release_title))
|
||||
logger.info('[%s] Forcing repackage of %s, since dB releases have been removed' % (
|
||||
release['ArtistName'], release_title))
|
||||
else:
|
||||
num_new_releases = num_new_releases
|
||||
|
||||
@@ -649,23 +699,25 @@ def getTracksFromRelease(release):
|
||||
except:
|
||||
track_title = unicode(track['recording']['title'])
|
||||
tracks.append({
|
||||
'number': totalTracks,
|
||||
'title': track_title,
|
||||
'id': unicode(track['recording']['id']),
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['length']) if 'length' in track else 0
|
||||
})
|
||||
'number': totalTracks,
|
||||
'title': track_title,
|
||||
'id': unicode(track['recording']['id']),
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['length']) if 'length' in track else 0
|
||||
})
|
||||
totalTracks += 1
|
||||
return tracks
|
||||
|
||||
|
||||
# Used when there is a disambiguation
|
||||
|
||||
|
||||
def findArtistbyAlbum(name):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone()
|
||||
artist = myDB.action(
|
||||
'SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()',
|
||||
[name]).fetchone()
|
||||
|
||||
if not artist:
|
||||
return False
|
||||
@@ -692,21 +744,20 @@ def findArtistbyAlbum(name):
|
||||
for releaseGroup in results:
|
||||
newArtist = releaseGroup['artist-credit'][0]['artist']
|
||||
# Only need the artist ID if we're doing an artist+album lookup
|
||||
#if 'disambiguation' in newArtist:
|
||||
# if 'disambiguation' in newArtist:
|
||||
# uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")")
|
||||
#else:
|
||||
# else:
|
||||
# uniquename = unicode(newArtist['sort-name'])
|
||||
#artist_dict['name'] = unicode(newArtist['sort-name'])
|
||||
#artist_dict['uniquename'] = uniquename
|
||||
# artist_dict['name'] = unicode(newArtist['sort-name'])
|
||||
# artist_dict['uniquename'] = uniquename
|
||||
artist_dict['id'] = unicode(newArtist['id'])
|
||||
#artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
|
||||
#artist_dict['score'] = int(releaseGroup['ext:score'])
|
||||
# artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
|
||||
# artist_dict['score'] = int(releaseGroup['ext:score'])
|
||||
|
||||
return artist_dict
|
||||
|
||||
|
||||
def findAlbumID(artist=None, album=None):
|
||||
|
||||
results = None
|
||||
chars = set('!?*-')
|
||||
|
||||
@@ -723,9 +774,11 @@ def findAlbumID(artist=None, album=None):
|
||||
album = '"' + album + '"'
|
||||
criteria = {'release': album.lower()}
|
||||
with mb_lock:
|
||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get('release-group-list')
|
||||
results = musicbrainzngs.search_release_groups(limit=1, **criteria).get(
|
||||
'release-group-list')
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e)))
|
||||
logger.warn(
|
||||
'Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e)))
|
||||
mb_lock.snooze(5)
|
||||
|
||||
if not results:
|
||||
|
||||
@@ -13,14 +13,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import json
|
||||
import headphones
|
||||
|
||||
from headphones import db, helpers, logger, request
|
||||
from headphones.common import USER_AGENT
|
||||
|
||||
def update(artistid, artist_name,release_groups):
|
||||
|
||||
def update(artistid, artist_name, release_groups):
|
||||
""" Pretty simple and crude function to find the artist page on metacritic,
|
||||
then parse that page to get critic & user scores for albums"""
|
||||
|
||||
@@ -28,12 +26,13 @@ def update(artistid, artist_name,release_groups):
|
||||
# We could just do a search, then take the top result, but at least this will
|
||||
# cut down on api calls. If it's ineffective then we'll switch to search
|
||||
|
||||
replacements = {" & " : " ", "." : ""}
|
||||
mc_artist_name = helpers.replace_all(artist_name.lower(),replacements)
|
||||
replacements = {" & ": " ", ".": ""}
|
||||
mc_artist_name = helpers.replace_all(artist_name.lower(), replacements)
|
||||
|
||||
mc_artist_name = mc_artist_name.replace(" ","-")
|
||||
mc_artist_name = mc_artist_name.replace(" ", "-")
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
|
||||
url = "http://www.metacritic.com/person/" + mc_artist_name + "?filter-options=music&sort_options=date&num_items=100"
|
||||
|
||||
@@ -67,12 +66,12 @@ def update(artistid, artist_name,release_groups):
|
||||
scores = row.find_all("span")
|
||||
critic_score = scores[0].string
|
||||
user_score = scores[1].string
|
||||
score_dict = {'title':title,'critic_score':critic_score,'user_score':user_score}
|
||||
score_dict = {'title': title, 'critic_score': critic_score, 'user_score': user_score}
|
||||
score_list.append(score_dict)
|
||||
|
||||
# Save scores to the database
|
||||
controlValueDict = {"ArtistID": artistid}
|
||||
newValueDict = {'MetaCritic':json.dumps(score_list)}
|
||||
newValueDict = {'MetaCritic': json.dumps(score_list)}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
for score in score_list:
|
||||
@@ -84,5 +83,5 @@ def update(artistid, artist_name,release_groups):
|
||||
critic_score = score['critic_score']
|
||||
user_score = score['user_score']
|
||||
controlValueDict = {"AlbumID": rg['id']}
|
||||
newValueDict = {'CriticScore':critic_score,'UserScore':user_score}
|
||||
newValueDict = {'CriticScore': critic_score, 'UserScore': user_score}
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import subprocess
|
||||
import headphones
|
||||
import multiprocessing
|
||||
|
||||
import os
|
||||
import headphones
|
||||
from headphones import logger
|
||||
from beets.mediafile import MediaFile
|
||||
|
||||
|
||||
# xld
|
||||
import getXldProfile
|
||||
|
||||
@@ -32,9 +33,11 @@ def encode(albumPath):
|
||||
|
||||
# Return if xld details not found
|
||||
if use_xld:
|
||||
(xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.CONFIG.XLDPROFILE)
|
||||
(xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(
|
||||
headphones.CONFIG.XLDPROFILE)
|
||||
if not xldFormat:
|
||||
logger.error('Details for xld profile \'%s\' not found, files will not be re-encoded', xldProfile)
|
||||
logger.error('Details for xld profile \'%s\' not found, files will not be re-encoded',
|
||||
xldProfile)
|
||||
return None
|
||||
else:
|
||||
xldProfile = None
|
||||
@@ -60,7 +63,8 @@ def encode(albumPath):
|
||||
for music in f:
|
||||
if any(music.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
if not use_xld:
|
||||
encoderFormat = headphones.CONFIG.ENCODEROUTPUTFORMAT.encode(headphones.SYS_ENCODING)
|
||||
encoderFormat = headphones.CONFIG.ENCODEROUTPUTFORMAT.encode(
|
||||
headphones.SYS_ENCODING)
|
||||
else:
|
||||
xldMusicFile = os.path.join(r, music)
|
||||
xldInfoMusic = MediaFile(xldMusicFile)
|
||||
@@ -68,9 +72,11 @@ def encode(albumPath):
|
||||
|
||||
if headphones.CONFIG.ENCODERLOSSLESS:
|
||||
ext = os.path.normpath(os.path.splitext(music)[1].lstrip(".")).lower()
|
||||
if not use_xld and ext == 'flac' or use_xld and (ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 400)):
|
||||
if not use_xld and ext == 'flac' or use_xld and (
|
||||
ext != xldFormat and (xldInfoMusic.bitrate / 1000 > 400)):
|
||||
musicFiles.append(os.path.join(r, music))
|
||||
musicTemp = os.path.normpath(os.path.splitext(music)[0] + '.' + encoderFormat)
|
||||
musicTemp = os.path.normpath(
|
||||
os.path.splitext(music)[0] + '.' + encoderFormat)
|
||||
musicTempFiles.append(os.path.join(tempDirEncode, musicTemp))
|
||||
else:
|
||||
logger.debug('%s is already encoded', music)
|
||||
@@ -86,7 +92,7 @@ def encode(albumPath):
|
||||
encoder = os.path.join('/Applications', 'xld')
|
||||
elif headphones.CONFIG.ENCODER == 'lame':
|
||||
if headphones.SYS_PLATFORM == "win32":
|
||||
## NEED THE DEFAULT LAME INSTALL ON WIN!
|
||||
# NEED THE DEFAULT LAME INSTALL ON WIN!
|
||||
encoder = "C:/Program Files/lame/lame.exe"
|
||||
else:
|
||||
encoder = "lame"
|
||||
@@ -111,26 +117,36 @@ def encode(albumPath):
|
||||
|
||||
if use_xld:
|
||||
if xldBitrate and (infoMusic.bitrate / 1000 <= xldBitrate):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music.decode(headphones.SYS_ENCODING, 'replace'), xldBitrate)
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded',
|
||||
music.decode(headphones.SYS_ENCODING, 'replace'), xldBitrate)
|
||||
else:
|
||||
encode = True
|
||||
elif headphones.CONFIG.ENCODER == 'lame':
|
||||
if not any(music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x in ["mp3", "wav"]):
|
||||
logger.warn('Lame cannot encode %s format for %s, use ffmpeg', os.path.splitext(music)[1], music)
|
||||
if not any(
|
||||
music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + x) for x
|
||||
in ["mp3", "wav"]):
|
||||
logger.warn('Lame cannot encode %s format for %s, use ffmpeg',
|
||||
os.path.splitext(music)[1], music)
|
||||
else:
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music, headphones.CONFIG.BITRATE)
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.mp3') and (
|
||||
int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music,
|
||||
headphones.CONFIG.BITRATE)
|
||||
else:
|
||||
encode = True
|
||||
else:
|
||||
if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'ogg':
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.ogg'):
|
||||
logger.warn('Cannot re-encode .ogg %s', music.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn('Cannot re-encode .ogg %s',
|
||||
music.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
else:
|
||||
encode = True
|
||||
elif headphones.CONFIG.ENCODEROUTPUTFORMAT == 'mp3' or headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a':
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith('.' + headphones.CONFIG.ENCODEROUTPUTFORMAT) and (int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music, headphones.CONFIG.BITRATE)
|
||||
if music.decode(headphones.SYS_ENCODING, 'replace').lower().endswith(
|
||||
'.' + headphones.CONFIG.ENCODEROUTPUTFORMAT) and (
|
||||
int(infoMusic.bitrate / 1000) <= headphones.CONFIG.BITRATE):
|
||||
logger.info('%s has bitrate <= %skb, will not be re-encoded', music,
|
||||
headphones.CONFIG.BITRATE)
|
||||
else:
|
||||
encode = True
|
||||
# encode
|
||||
@@ -155,7 +171,7 @@ def encode(albumPath):
|
||||
processes = headphones.CONFIG.ENCODER_MULTICORE_COUNT
|
||||
|
||||
logger.debug("Multi-core encoding enabled, spawning %d processes",
|
||||
processes)
|
||||
processes)
|
||||
|
||||
# Use multiprocessing only if it's worth the overhead. and if it is
|
||||
# enabled. If not, then use the old fashioned way.
|
||||
@@ -185,7 +201,8 @@ def encode(albumPath):
|
||||
for dest in musicTempFiles:
|
||||
if not os.path.exists(dest):
|
||||
encoder_failed = True
|
||||
logger.error("Encoded file '%s' does not exist in the destination temp directory", dest)
|
||||
logger.error("Encoded file '%s' does not exist in the destination temp directory",
|
||||
dest)
|
||||
|
||||
# No errors, move from temp to parent
|
||||
if not encoder_failed and musicTempFiles:
|
||||
@@ -211,7 +228,9 @@ def encode(albumPath):
|
||||
|
||||
# Return with error if any encoding errors
|
||||
if encoder_failed:
|
||||
logger.error("One or more files failed to encode. Ensure you have the latest version of %s installed.", headphones.CONFIG.ENCODER)
|
||||
logger.error(
|
||||
"One or more files failed to encode. Ensure you have the latest version of %s installed.",
|
||||
headphones.CONFIG.ENCODER)
|
||||
return None
|
||||
|
||||
time.sleep(1)
|
||||
@@ -269,7 +288,8 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
if not headphones.CONFIG.ADVANCEDENCODER:
|
||||
opts.extend(['-h'])
|
||||
if headphones.CONFIG.ENCODERVBRCBR == 'cbr':
|
||||
opts.extend(['--resample', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-b', str(headphones.CONFIG.BITRATE)])
|
||||
opts.extend(['--resample', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-b',
|
||||
str(headphones.CONFIG.BITRATE)])
|
||||
elif headphones.CONFIG.ENCODERVBRCBR == 'vbr':
|
||||
opts.extend(['-v', str(headphones.CONFIG.ENCODERQUALITY)])
|
||||
else:
|
||||
@@ -290,7 +310,8 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a':
|
||||
opts.extend(['-strict', 'experimental'])
|
||||
if headphones.CONFIG.ENCODERVBRCBR == 'cbr':
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab', str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab',
|
||||
str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
elif headphones.CONFIG.ENCODERVBRCBR == 'vbr':
|
||||
opts.extend(['-aq', str(headphones.CONFIG.ENCODERQUALITY)])
|
||||
opts.extend(['-y', '-ac', '2', '-vn'])
|
||||
@@ -311,7 +332,8 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
if headphones.CONFIG.ENCODEROUTPUTFORMAT == 'm4a':
|
||||
opts.extend(['-strict', 'experimental'])
|
||||
if headphones.CONFIG.ENCODERVBRCBR == 'cbr':
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab', str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
opts.extend(['-ar', str(headphones.CONFIG.SAMPLINGFREQUENCY), '-ab',
|
||||
str(headphones.CONFIG.BITRATE) + 'k'])
|
||||
elif headphones.CONFIG.ENCODERVBRCBR == 'vbr':
|
||||
opts.extend(['-aq', str(headphones.CONFIG.ENCODERQUALITY)])
|
||||
opts.extend(['-y', '-ac', '2', '-vn'])
|
||||
@@ -337,13 +359,14 @@ def command(encoder, musicSource, musicDest, albumPath, xldProfile):
|
||||
logger.debug(subprocess.list2cmdline(cmd))
|
||||
|
||||
process = subprocess.Popen(cmd, startupinfo=startupinfo,
|
||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdin=open(os.devnull, 'rb'), stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = process.communicate(headphones.CONFIG.ENCODER)
|
||||
|
||||
# Error if return code not zero
|
||||
if process.returncode:
|
||||
logger.error('Encoding failed for %s' % (musicSource.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
logger.error(
|
||||
'Encoding failed for %s' % (musicSource.decode(headphones.SYS_ENCODING, 'replace')))
|
||||
out = stdout if stdout else stderr
|
||||
out = out.decode(headphones.SYS_ENCODING, 'replace')
|
||||
outlast2lines = '\n'.join(out.splitlines()[-2:])
|
||||
|
||||
@@ -13,30 +13,25 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import logger, helpers, common, request
|
||||
|
||||
from xml.dom import minidom
|
||||
from httplib import HTTPSConnection
|
||||
from urlparse import parse_qsl
|
||||
from urllib import urlencode
|
||||
from pynma import pynma
|
||||
|
||||
import base64
|
||||
import cherrypy
|
||||
import urllib
|
||||
import urllib2
|
||||
import headphones
|
||||
import os.path
|
||||
import subprocess
|
||||
import gntp.notifier
|
||||
import json
|
||||
|
||||
import oauth2 as oauth
|
||||
import pythontwitter as twitter
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
import smtplib
|
||||
import email.utils
|
||||
from httplib import HTTPSConnection
|
||||
from urlparse import parse_qsl
|
||||
import urllib2
|
||||
|
||||
import os.path
|
||||
from headphones import logger, helpers, common, request
|
||||
from pynma import pynma
|
||||
import cherrypy
|
||||
import headphones
|
||||
import gntp.notifier
|
||||
import oauth2 as oauth
|
||||
import pythontwitter as twitter
|
||||
|
||||
|
||||
class GROWL(object):
|
||||
@@ -95,7 +90,7 @@ class GROWL(object):
|
||||
|
||||
# Send it, including an image
|
||||
image_file = os.path.join(str(headphones.PROG_DIR),
|
||||
"data/images/headphoneslogo.png")
|
||||
"data/images/headphoneslogo.png")
|
||||
|
||||
with open(image_file, 'rb') as f:
|
||||
image = f.read()
|
||||
@@ -114,7 +109,7 @@ class GROWL(object):
|
||||
logger.info(u"Growl notifications sent.")
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, host, password):
|
||||
@@ -151,24 +146,24 @@ class PROWL(object):
|
||||
'priority': headphones.CONFIG.PROWL_PRIORITY}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
"/publicapi/add",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Prowl notifications sent.")
|
||||
return True
|
||||
logger.info(u"Prowl notifications sent.")
|
||||
return True
|
||||
elif request_status == 401:
|
||||
logger.info(u"Prowl auth failed: %s" % response.reason)
|
||||
return False
|
||||
logger.info(u"Prowl auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Prowl notification failed.")
|
||||
return False
|
||||
logger.info(u"Prowl notification failed.")
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
@@ -185,7 +180,6 @@ class MPC(object):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
pass
|
||||
|
||||
def notify(self):
|
||||
@@ -218,9 +212,11 @@ class XBMC(object):
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
@@ -244,20 +240,24 @@ class XBMC(object):
|
||||
|
||||
header = "Headphones"
|
||||
message = "%s - %s added to your library" % (artist, album)
|
||||
time = "3000" # in ms
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to XMBC @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']
|
||||
version = \
|
||||
self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})[
|
||||
'version']['major']
|
||||
|
||||
if version < 12: #Eden
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: #Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time), 'image': albumartpath}
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
|
||||
if not request:
|
||||
@@ -335,9 +335,11 @@ class Plex(object):
|
||||
url = host + '/jsonrpc'
|
||||
|
||||
if self.password:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers, auth=(self.username, self.password))
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers, auth=(self.username, self.password))
|
||||
else:
|
||||
response = request.request_json(url, method="post", data=json.dumps(data), headers=headers)
|
||||
response = request.request_json(url, method="post", data=json.dumps(data),
|
||||
headers=headers)
|
||||
|
||||
if response:
|
||||
return response[0]['result']
|
||||
@@ -376,20 +378,24 @@ class Plex(object):
|
||||
|
||||
header = "Headphones"
|
||||
message = "%s - %s added to your library" % (artist, album)
|
||||
time = "3000" # in ms
|
||||
time = "3000" # in ms
|
||||
|
||||
for host in hosts:
|
||||
logger.info('Sending notification command to Plex client @ ' + host)
|
||||
try:
|
||||
version = self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})['version']['major']
|
||||
version = \
|
||||
self._sendjson(host, 'Application.GetProperties', {'properties': ['version']})[
|
||||
'version']['major']
|
||||
|
||||
if version < 12: #Eden
|
||||
if version < 12: # Eden
|
||||
notification = header + "," + message + "," + time + "," + albumartpath
|
||||
notifycommand = {'command': 'ExecBuiltIn', 'parameter': 'Notification(' + notification + ')'}
|
||||
notifycommand = {'command': 'ExecBuiltIn',
|
||||
'parameter': 'Notification(' + notification + ')'}
|
||||
request = self._sendhttp(host, notifycommand)
|
||||
|
||||
else: #Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time), 'image': albumartpath}
|
||||
else: # Frodo
|
||||
params = {'title': header, 'message': message, 'displaytime': int(time),
|
||||
'image': albumartpath}
|
||||
request = self._sendjson(host, 'GUI.ShowNotification', params)
|
||||
|
||||
if not request:
|
||||
@@ -438,7 +444,6 @@ class NMA(object):
|
||||
|
||||
|
||||
class PUSHBULLET(object):
|
||||
|
||||
def __init__(self):
|
||||
self.apikey = headphones.CONFIG.PUSHBULLET_APIKEY
|
||||
self.deviceid = headphones.CONFIG.PUSHBULLET_DEVICEID
|
||||
@@ -456,8 +461,8 @@ class PUSHBULLET(object):
|
||||
if self.deviceid:
|
||||
data['device_iden'] = self.deviceid
|
||||
|
||||
headers={'Content-type': "application/json",
|
||||
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
headers = {'Content-type': "application/json",
|
||||
'Authorization': 'Bearer ' + headphones.CONFIG.PUSHBULLET_APIKEY}
|
||||
|
||||
response = request.request_json(url, method="post", headers=headers, data=json.dumps(data))
|
||||
|
||||
@@ -468,8 +473,8 @@ class PUSHBULLET(object):
|
||||
logger.info(u"PushBullet notification failed.")
|
||||
return False
|
||||
|
||||
class PUSHALOT(object):
|
||||
|
||||
class PUSHALOT(object):
|
||||
def notify(self, message, event):
|
||||
if not headphones.CONFIG.PUSHALOT_ENABLED:
|
||||
return
|
||||
@@ -487,9 +492,9 @@ class PUSHALOT(object):
|
||||
'Body': message.encode("utf-8")}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
"/api/sendmessage",
|
||||
headers={'Content-type': "application/x-www-form-urlencoded"},
|
||||
body=urlencode(data))
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
@@ -498,14 +503,14 @@ class PUSHALOT(object):
|
||||
logger.debug(u"Pushalot response body: %r" % response.read())
|
||||
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushalot notifications sent.")
|
||||
return True
|
||||
logger.info(u"Pushalot notifications sent.")
|
||||
return True
|
||||
elif request_status == 410:
|
||||
logger.info(u"Pushalot auth failed: %s" % response.reason)
|
||||
return False
|
||||
logger.info(u"Pushalot auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
logger.info(u"Pushalot notification failed.")
|
||||
return False
|
||||
logger.info(u"Pushalot notification failed.")
|
||||
return False
|
||||
|
||||
|
||||
class Synoindex(object):
|
||||
@@ -519,7 +524,8 @@ class Synoindex(object):
|
||||
path = os.path.abspath(path)
|
||||
|
||||
if not self.util_exists():
|
||||
logger.warn("Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
logger.warn(
|
||||
"Error sending notification: synoindex utility not found at %s" % self.util_loc)
|
||||
return
|
||||
|
||||
if os.path.isfile(path):
|
||||
@@ -527,15 +533,17 @@ class Synoindex(object):
|
||||
elif os.path.isdir(path):
|
||||
cmd_arg = '-A'
|
||||
else:
|
||||
logger.warn("Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
logger.warn(
|
||||
"Error sending notification: Path passed to synoindex was not a file or folder.")
|
||||
return
|
||||
|
||||
cmd = [self.util_loc, cmd_arg, path]
|
||||
logger.info("Calling synoindex command: %s" % str(cmd))
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
cwd=headphones.PROG_DIR)
|
||||
out, error = p.communicate()
|
||||
#synoindex never returns any codes other than '0', highly irritating
|
||||
# synoindex never returns any codes other than '0', highly irritating
|
||||
except OSError, e:
|
||||
logger.warn("Error sending notification: %s" % str(e))
|
||||
|
||||
@@ -546,7 +554,6 @@ class Synoindex(object):
|
||||
|
||||
|
||||
class PUSHOVER(object):
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = headphones.CONFIG.PUSHOVER_ENABLED
|
||||
self.keys = headphones.CONFIG.PUSHOVER_KEYS
|
||||
@@ -584,7 +591,7 @@ class PUSHOVER(object):
|
||||
return False
|
||||
|
||||
def updateLibrary(self):
|
||||
#For uniformity reasons not removed
|
||||
# For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
@@ -596,7 +603,6 @@ class PUSHOVER(object):
|
||||
|
||||
|
||||
class TwitterNotifier(object):
|
||||
|
||||
REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token'
|
||||
ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token'
|
||||
AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize'
|
||||
@@ -608,14 +614,17 @@ class TwitterNotifier(object):
|
||||
|
||||
def notify_snatch(self, title):
|
||||
if headphones.CONFIG.TWITTER_ONSNATCH:
|
||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
self._notifyTwitter(
|
||||
common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now())
|
||||
|
||||
def notify_download(self, title):
|
||||
if headphones.CONFIG.TWITTER_ENABLED:
|
||||
self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
self._notifyTwitter(common.notifyStrings[
|
||||
common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now())
|
||||
|
||||
def test_notify(self):
|
||||
return self._notifyTwitter("This is a test notification from Headphones at " + helpers.now(), force=True)
|
||||
return self._notifyTwitter(
|
||||
"This is a test notification from Headphones at " + helpers.now(), force=True)
|
||||
|
||||
def _get_authorization(self):
|
||||
|
||||
@@ -652,7 +661,8 @@ class TwitterNotifier(object):
|
||||
logger.info('oauth_consumer: ' + str(oauth_consumer))
|
||||
oauth_client = oauth.Client(oauth_consumer, token)
|
||||
logger.info('oauth_client: ' + str(oauth_client))
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key)
|
||||
resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST',
|
||||
body='oauth_verifier=%s' % key)
|
||||
logger.info('resp, content: ' + str(resp) + ',' + str(content))
|
||||
|
||||
access_token = dict(parse_qsl(content))
|
||||
@@ -660,7 +670,8 @@ class TwitterNotifier(object):
|
||||
|
||||
logger.info('resp[status] = ' + str(resp['status']))
|
||||
if resp['status'] != '200':
|
||||
logger.info('The request for a token with did not succeed: ' + str(resp['status']), logger.ERROR)
|
||||
logger.info('The request for a token with did not succeed: ' + str(resp['status']),
|
||||
logger.ERROR)
|
||||
return False
|
||||
else:
|
||||
logger.info('Your Twitter Access Token key: %s' % access_token['oauth_token'])
|
||||
@@ -698,7 +709,6 @@ class TwitterNotifier(object):
|
||||
|
||||
|
||||
class OSX_NOTIFY(object):
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
self.objc = __import__("objc")
|
||||
@@ -751,7 +761,7 @@ class OSX_NOTIFY(object):
|
||||
if image:
|
||||
source_img = self.AppKit.NSImage.alloc().initByReferencingFile_(image)
|
||||
notification.setContentImage_(source_img)
|
||||
#notification.set_identityImage_(source_img)
|
||||
# notification.set_identityImage_(source_img)
|
||||
notification.setHasActionButton_(False)
|
||||
|
||||
notification_center = NSUserNotificationCenter.defaultUserNotificationCenter()
|
||||
@@ -769,7 +779,6 @@ class OSX_NOTIFY(object):
|
||||
|
||||
|
||||
class BOXCAR(object):
|
||||
|
||||
def __init__(self):
|
||||
self.url = 'https://new.boxcar.io/api/notifications'
|
||||
|
||||
@@ -783,7 +792,7 @@ class BOXCAR(object):
|
||||
'notification[title]': title.encode('utf-8'),
|
||||
'notification[long_message]': message.encode('utf-8'),
|
||||
'notification[sound]': "done"
|
||||
})
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
handle = urllib2.urlopen(req, data)
|
||||
@@ -796,7 +805,6 @@ class BOXCAR(object):
|
||||
|
||||
|
||||
class SubSonicNotifier(object):
|
||||
|
||||
def __init__(self):
|
||||
self.host = headphones.CONFIG.SUBSONIC_HOST
|
||||
self.username = headphones.CONFIG.SUBSONIC_USERNAME
|
||||
@@ -812,10 +820,10 @@ class SubSonicNotifier(object):
|
||||
|
||||
# Invoke request
|
||||
request.request_response(self.host + "musicFolderSettings.view?scanNow",
|
||||
auth=(self.username, self.password))
|
||||
auth=(self.username, self.password))
|
||||
|
||||
|
||||
class Email(object):
|
||||
|
||||
def notify(self, subject, message):
|
||||
|
||||
message = MIMEText(message, 'plain', "utf-8")
|
||||
@@ -824,20 +832,24 @@ class Email(object):
|
||||
message['To'] = headphones.CONFIG.EMAIL_TO
|
||||
|
||||
try:
|
||||
if (headphones.CONFIG.EMAIL_SSL):
|
||||
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
if headphones.CONFIG.EMAIL_SSL:
|
||||
mailserver = smtplib.SMTP_SSL(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
else:
|
||||
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER, headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
mailserver = smtplib.SMTP(headphones.CONFIG.EMAIL_SMTP_SERVER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PORT)
|
||||
|
||||
if (headphones.CONFIG.EMAIL_TLS):
|
||||
if headphones.CONFIG.EMAIL_TLS:
|
||||
mailserver.starttls()
|
||||
|
||||
mailserver.ehlo()
|
||||
|
||||
if headphones.CONFIG.EMAIL_SMTP_USER:
|
||||
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER, headphones.CONFIG.EMAIL_SMTP_PASSWORD)
|
||||
mailserver.login(headphones.CONFIG.EMAIL_SMTP_USER,
|
||||
headphones.CONFIG.EMAIL_SMTP_PASSWORD)
|
||||
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO, message.as_string())
|
||||
mailserver.sendmail(headphones.CONFIG.EMAIL_FROM, headphones.CONFIG.EMAIL_TO,
|
||||
message.as_string())
|
||||
mailserver.quit()
|
||||
return True
|
||||
|
||||
|
||||
@@ -19,18 +19,15 @@
|
||||
# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import httplib
|
||||
|
||||
import headphones
|
||||
|
||||
from base64 import standard_b64encode
|
||||
import httplib
|
||||
import xmlrpclib
|
||||
|
||||
import headphones
|
||||
from headphones import logger
|
||||
|
||||
|
||||
def sendNZB(nzb):
|
||||
|
||||
addToTop = False
|
||||
nzbgetXMLrpc = "%(protocol)s://%(username)s:%(password)s@%(host)s/xmlrpc"
|
||||
|
||||
@@ -45,17 +42,22 @@ def sendNZB(nzb):
|
||||
protocol = 'http'
|
||||
host = headphones.CONFIG.NZBGET_HOST.replace('http://', '', 1)
|
||||
|
||||
url = nzbgetXMLrpc % {"protocol": protocol, "host": host, "username": headphones.CONFIG.NZBGET_USERNAME, "password": headphones.CONFIG.NZBGET_PASSWORD}
|
||||
url = nzbgetXMLrpc % {"protocol": protocol, "host": host,
|
||||
"username": headphones.CONFIG.NZBGET_USERNAME,
|
||||
"password": headphones.CONFIG.NZBGET_PASSWORD}
|
||||
|
||||
nzbGetRPC = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if nzbGetRPC.writelog("INFO", "headphones connected to drop of %s any moment now." % (nzb.name + ".nzb")):
|
||||
if nzbGetRPC.writelog("INFO", "headphones connected to drop of %s any moment now." % (
|
||||
nzb.name + ".nzb")):
|
||||
logger.debug(u"Successfully connected to NZBget")
|
||||
else:
|
||||
logger.info(u"Successfully connected to NZBget, but unable to send a message" % (nzb.name + ".nzb"))
|
||||
logger.info(u"Successfully connected to NZBget, but unable to send a message" % (
|
||||
nzb.name + ".nzb"))
|
||||
|
||||
except httplib.socket.error:
|
||||
logger.error(u"Please check your NZBget host and port (if it is running). NZBget is not responding to this combination")
|
||||
logger.error(
|
||||
u"Please check your NZBget host and port (if it is running). NZBget is not responding to this combination")
|
||||
return False
|
||||
|
||||
except xmlrpclib.ProtocolError, e:
|
||||
@@ -82,7 +84,9 @@ def sendNZB(nzb):
|
||||
nzbget_version = int(nzbget_version_str[:nzbget_version_str.find(".")])
|
||||
if nzbget_version == 0:
|
||||
if nzbcontent64 is not None:
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, addToTop, nzbcontent64)
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY, addToTop,
|
||||
nzbcontent64)
|
||||
else:
|
||||
# from headphones.common.providers.generic import GenericProvider
|
||||
# if nzb.resultType == "nzb":
|
||||
@@ -95,24 +99,35 @@ def sendNZB(nzb):
|
||||
return False
|
||||
elif nzbget_version == 12:
|
||||
if nzbcontent64 is not None:
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbcontent64, False, dupekey, dupescore, "score")
|
||||
else:
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzb.url, False, dupekey, dupescore, "score")
|
||||
# v13+ has a new combined append method that accepts both (url and content)
|
||||
# also the return value has changed from boolean to integer
|
||||
# (Positive number representing NZBID of the queue item. 0 and negative numbers represent error codes.)
|
||||
elif nzbget_version >= 13:
|
||||
nzbget_result = True if nzbGetRPC.append(nzb.name + ".nzb", nzbcontent64 if nzbcontent64 is not None else nzb.url,
|
||||
headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False, False, dupekey, dupescore,
|
||||
nzbget_result = True if nzbGetRPC.append(nzb.name + ".nzb",
|
||||
nzbcontent64 if nzbcontent64 is not None else nzb.url,
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
False, dupekey, dupescore,
|
||||
"score") > 0 else False
|
||||
else:
|
||||
if nzbcontent64 is not None:
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.append(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbcontent64)
|
||||
else:
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb", headphones.CONFIG.NZBGET_CATEGORY, headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzbget_result = nzbGetRPC.appendurl(nzb.name + ".nzb",
|
||||
headphones.CONFIG.NZBGET_CATEGORY,
|
||||
headphones.CONFIG.NZBGET_PRIORITY, False,
|
||||
nzb.url)
|
||||
|
||||
if nzbget_result:
|
||||
@@ -122,5 +137,6 @@ def sendNZB(nzb):
|
||||
logger.error(u"NZBget could not add %s to the queue" % (nzb.name + ".nzb"))
|
||||
return False
|
||||
except:
|
||||
logger.error(u"Connect Error to NZBget: could not add %s to the queue" % (nzb.name + ".nzb"))
|
||||
logger.error(
|
||||
u"Connect Error to NZBget: could not add %s to the queue" % (nzb.name + ".nzb"))
|
||||
return False
|
||||
|
||||
@@ -13,20 +13,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
import beets
|
||||
import threading
|
||||
import itertools
|
||||
import headphones
|
||||
|
||||
import os
|
||||
import re
|
||||
import beets
|
||||
import headphones
|
||||
from beets import autotag
|
||||
from beets import config as beetsconfig
|
||||
from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError
|
||||
from beetsplug import lyrics as beetslyrics
|
||||
|
||||
from headphones import notifiers, utorrent, transmission
|
||||
from headphones import db, albumart, librarysync
|
||||
from headphones import logger, helpers, request, mb, music_encoder
|
||||
@@ -48,19 +47,21 @@ def checkFolder():
|
||||
else:
|
||||
download_dir = headphones.CONFIG.DOWNLOAD_TORRENT_DIR
|
||||
|
||||
album_path = os.path.join(download_dir, album['FolderName']).encode(headphones.SYS_ENCODING, 'replace')
|
||||
album_path = os.path.join(download_dir, album['FolderName']).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
logger.debug("Checking if %s exists" % album_path)
|
||||
|
||||
if os.path.exists(album_path):
|
||||
logger.info('Found "' + album['FolderName'] + '" in ' + album['Kind'] + ' download folder. Verifying....')
|
||||
logger.info('Found "' + album['FolderName'] + '" in ' + album[
|
||||
'Kind'] + ' download folder. Verifying....')
|
||||
verify(album['AlbumID'], album_path, album['Kind'])
|
||||
else:
|
||||
logger.info("No folder name found for " + album['Title'])
|
||||
|
||||
logger.debug("Checking download folder finished.")
|
||||
|
||||
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False):
|
||||
|
||||
def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=False):
|
||||
myDB = db.DBConnection()
|
||||
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
@@ -72,11 +73,14 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
try:
|
||||
release_list = mb.getReleaseGroup(albumid)
|
||||
except Exception as e:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s. Error: %s', albumid, e)
|
||||
logger.error(
|
||||
'Unable to get release information for manual album with rgid: %s. Error: %s',
|
||||
albumid, e)
|
||||
return
|
||||
|
||||
if not release_list:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s', albumid)
|
||||
logger.error('Unable to get release information for manual album with rgid: %s',
|
||||
albumid)
|
||||
return
|
||||
|
||||
# Since we're just using this to create the bare minimum information to
|
||||
@@ -85,7 +89,9 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
release_dict = mb.getRelease(releaseid)
|
||||
|
||||
if not release_dict:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s. Cannot continue', albumid)
|
||||
logger.error(
|
||||
'Unable to get release information for manual album with rgid: %s. Cannot continue',
|
||||
albumid)
|
||||
return
|
||||
|
||||
# Check if the artist is added to the database. In case the database is
|
||||
@@ -93,21 +99,26 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
# prevents new artists from appearing suddenly. In case forced is True,
|
||||
# this check is skipped, since it is assumed the user wants this.
|
||||
if headphones.CONFIG.FREEZE_DB and not forced:
|
||||
artist = myDB.select("SELECT ArtistName, ArtistID FROM artists WHERE ArtistId=? OR ArtistName=?", [release_dict['artist_id'], release_dict['artist_name']])
|
||||
artist = myDB.select(
|
||||
"SELECT ArtistName, ArtistID FROM artists WHERE ArtistId=? OR ArtistName=?",
|
||||
[release_dict['artist_id'], release_dict['artist_name']])
|
||||
|
||||
if not artist:
|
||||
logger.warn("Continuing would add new artist '%s' (ID %s), " \
|
||||
"but database is frozen. Will skip postprocessing for " \
|
||||
"album with rgid: %s", release_dict['artist_name'],
|
||||
release_dict['artist_id'], albumid)
|
||||
"but database is frozen. Will skip postprocessing for " \
|
||||
"album with rgid: %s", release_dict['artist_name'],
|
||||
release_dict['artist_id'], albumid)
|
||||
|
||||
myDB.action('UPDATE snatched SET status = "Frozen" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||
myDB.action(
|
||||
'UPDATE snatched SET status = "Frozen" WHERE status NOT LIKE "Seed%" and AlbumID=?',
|
||||
[albumid])
|
||||
frozen = re.search(r' \(Frozen\)(?:\[\d+\])?', albumpath)
|
||||
if not frozen:
|
||||
if headphones.CONFIG.RENAME_FROZEN:
|
||||
renameUnprocessedFolder(albumpath, tag="Frozen")
|
||||
else:
|
||||
logger.warn(u"Won't rename %s to mark as 'Frozen', because it is disabled.", albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn(u"Won't rename %s to mark as 'Frozen', because it is disabled.",
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
return
|
||||
|
||||
logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
|
||||
@@ -123,7 +134,8 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
"DateAdded": helpers.today(),
|
||||
"Status": "Paused"}
|
||||
|
||||
logger.info("ArtistID: " + release_dict['artist_id'] + " , ArtistName: " + release_dict['artist_name'])
|
||||
logger.info("ArtistID: " + release_dict['artist_id'] + " , ArtistName: " + release_dict[
|
||||
'artist_name'])
|
||||
|
||||
if headphones.CONFIG.INCLUDE_EXTRAS:
|
||||
newValueDict['IncludeExtras'] = 1
|
||||
@@ -150,18 +162,17 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
# Delete existing tracks associated with this AlbumID since we're going to replace them and don't want any extras
|
||||
myDB.action('DELETE from tracks WHERE AlbumID=?', [albumid])
|
||||
for track in release_dict['tracks']:
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"AlbumID": albumid}
|
||||
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number']
|
||||
}
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number']
|
||||
}
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
@@ -169,7 +180,8 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
newValueDict = {"Status": "Paused"}
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
logger.info(u"Addition complete for: " + release_dict['title'] + " - " + release_dict['artist_name'])
|
||||
logger.info(u"Addition complete for: " + release_dict['title'] + " - " + release_dict[
|
||||
'artist_name'])
|
||||
|
||||
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
@@ -185,11 +197,14 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
downloaded_cuecount += 1
|
||||
# if any of the files end in *.part, we know the torrent isn't done yet. Process if forced, though
|
||||
elif files.lower().endswith(('.part', '.utpart')) and not forced:
|
||||
logger.info("Looks like " + os.path.basename(albumpath).decode(headphones.SYS_ENCODING, 'replace') + " isn't complete yet. Will try again on the next run")
|
||||
logger.info(
|
||||
"Looks like " + os.path.basename(albumpath).decode(headphones.SYS_ENCODING,
|
||||
'replace') + " isn't complete yet. Will try again on the next run")
|
||||
return
|
||||
|
||||
# Split cue
|
||||
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list):
|
||||
if headphones.CONFIG.CUE_SPLIT and downloaded_cuecount and downloaded_cuecount >= len(
|
||||
downloaded_track_list):
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent":
|
||||
albumpath = helpers.preserve_torrent_directory(albumpath)
|
||||
if albumpath and helpers.cue_split(albumpath):
|
||||
@@ -202,7 +217,10 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except Exception as e:
|
||||
logger.info(u"Exception from MediaFile for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + u" : " + unicode(e))
|
||||
logger.info(
|
||||
u"Exception from MediaFile for: " + downloaded_track.decode(headphones.SYS_ENCODING,
|
||||
'replace') + u" : " + unicode(
|
||||
e))
|
||||
continue
|
||||
|
||||
if not f.artist:
|
||||
@@ -219,7 +237,8 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
logger.debug('Matching metadata album: %s with album name: %s' % (metaalbum, dbalbum))
|
||||
|
||||
if metaartist == dbartist and metaalbum == dbalbum:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
return
|
||||
|
||||
# test #2: filenames
|
||||
@@ -237,7 +256,8 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
logger.debug('Checking if track title: %s is in file name: %s' % (dbtrack, filetrack))
|
||||
|
||||
if dbtrack in filetrack:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
return
|
||||
|
||||
# test #3: number of songs and duration
|
||||
@@ -269,32 +289,42 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
||||
logger.debug('Database track duration: %i' % db_track_duration)
|
||||
delta = abs(downloaded_track_duration - db_track_duration)
|
||||
if delta < 240:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind, keep_original_folder)
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind,
|
||||
keep_original_folder)
|
||||
return
|
||||
|
||||
logger.warn(u'Could not identify album: %s. It may not be the intended album.', albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||
logger.warn(u'Could not identify album: %s. It may not be the intended album.',
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
myDB.action(
|
||||
'UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?',
|
||||
[albumid])
|
||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||
if not processed:
|
||||
if headphones.CONFIG.RENAME_UNPROCESSED:
|
||||
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
||||
else:
|
||||
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled.", albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn(u"Won't rename %s to mark as 'Unprocessed', because it is disabled.",
|
||||
albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None, keep_original_folder=False):
|
||||
|
||||
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
|
||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None,
|
||||
keep_original_folder=False):
|
||||
logger.info(
|
||||
'Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
|
||||
# Check to see if we're preserving the torrent dir
|
||||
if (headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
|
||||
new_folder = os.path.join(albumpath, 'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
if (
|
||||
headphones.CONFIG.KEEP_TORRENT_FILES and Kind == "torrent" and 'headphones-modified' not in albumpath) or headphones.CONFIG.KEEP_ORIGINAL_FOLDER or keep_original_folder:
|
||||
new_folder = os.path.join(albumpath,
|
||||
'headphones-modified'.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info(
|
||||
"Copying files to 'headphones-modified' subfolder to preserve downloaded files for seeding")
|
||||
try:
|
||||
shutil.copytree(albumpath, new_folder)
|
||||
# Update the album path with the new location
|
||||
albumpath = new_folder
|
||||
except Exception as e:
|
||||
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
|
||||
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(
|
||||
headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
|
||||
return
|
||||
|
||||
# Need to update the downloaded track list with the new location.
|
||||
@@ -317,8 +347,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
return
|
||||
except (FileTypeError, UnreadableFileError):
|
||||
logger.error("Track file is not a valid media file: %s. Not " \
|
||||
"continuing.", downloaded_track.decode(
|
||||
headphones.SYS_ENCODING, "replace"))
|
||||
"continuing.", downloaded_track.decode(
|
||||
headphones.SYS_ENCODING, "replace"))
|
||||
return
|
||||
except IOError:
|
||||
logger.error("Unable to find media file: %s. Not continuing.")
|
||||
@@ -328,9 +358,9 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
# files, which requires write permissions. This step just check this, so
|
||||
# it will not try and fail lateron, with strange exceptions.
|
||||
if headphones.CONFIG.EMBED_ALBUM_ART or headphones.CONFIG.CLEANUP_FILES or \
|
||||
headphones.CONFIG.ADD_ALBUM_ART or headphones.CONFIG.CORRECT_METADATA or \
|
||||
headphones.CONFIG.EMBED_LYRICS or headphones.CONFIG.RENAME_FILES or \
|
||||
headphones.CONFIG.MOVE_FILES:
|
||||
headphones.CONFIG.ADD_ALBUM_ART or headphones.CONFIG.CORRECT_METADATA or \
|
||||
headphones.CONFIG.EMBED_LYRICS or headphones.CONFIG.RENAME_FILES or \
|
||||
headphones.CONFIG.MOVE_FILES:
|
||||
|
||||
try:
|
||||
with open(downloaded_track, "a+b") as fp:
|
||||
@@ -338,11 +368,11 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
except IOError as e:
|
||||
logger.debug("Write check exact error: %s", e)
|
||||
logger.error("Track file is not writable. This is required " \
|
||||
"for some post processing steps: %s. Not continuing.",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
|
||||
"for some post processing steps: %s. Not continuing.",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, "replace"))
|
||||
return
|
||||
|
||||
#start encoding
|
||||
# start encoding
|
||||
if headphones.CONFIG.MUSIC_ENCODER:
|
||||
downloaded_track_list = music_encoder.encode(albumpath)
|
||||
|
||||
@@ -389,7 +419,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
renameFiles(albumpath, downloaded_track_list, release)
|
||||
|
||||
if headphones.CONFIG.MOVE_FILES and not headphones.CONFIG.DESTINATION_DIR:
|
||||
logger.error('No DESTINATION_DIR has been set. Set "Destination Directory" to the parent directory you want to move the files to')
|
||||
logger.error(
|
||||
'No DESTINATION_DIR has been set. Set "Destination Directory" to the parent directory you want to move the files to')
|
||||
albumpaths = [albumpath]
|
||||
elif headphones.CONFIG.MOVE_FILES and headphones.CONFIG.DESTINATION_DIR:
|
||||
albumpaths = moveFiles(albumpath, release, tracks)
|
||||
@@ -400,15 +431,20 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Downloaded" WHERE AlbumID=?', [albumid])
|
||||
myDB.action('UPDATE snatched SET status = "Processed" WHERE Status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||
myDB.action(
|
||||
'UPDATE snatched SET status = "Processed" WHERE Status NOT LIKE "Seed%" and AlbumID=?',
|
||||
[albumid])
|
||||
|
||||
# Check if torrent has finished seeding
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1 or headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
seed_snatched = myDB.action('SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?', [albumid]).fetchone()
|
||||
seed_snatched = myDB.action(
|
||||
'SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?',
|
||||
[albumid]).fetchone()
|
||||
if seed_snatched:
|
||||
hash = seed_snatched['FolderName']
|
||||
torrent_removed = False
|
||||
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (release['ArtistName'], release['AlbumTitle']))
|
||||
logger.info(u'%s - %s. Checking if torrent has finished seeding and can be removed' % (
|
||||
release['ArtistName'], release['AlbumTitle']))
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1:
|
||||
torrent_removed = transmission.removeTorrent(hash, True)
|
||||
else:
|
||||
@@ -416,15 +452,20 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
|
||||
# Torrent removed, delete the snatched record, else update Status for scheduled job to check
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Snatched" and AlbumID=?', [albumid])
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Snatched" and AlbumID=?',
|
||||
[albumid])
|
||||
else:
|
||||
myDB.action('UPDATE snatched SET status = "Seed_Processed" WHERE status = "Seed_Snatched" and AlbumID=?', [albumid])
|
||||
myDB.action(
|
||||
'UPDATE snatched SET status = "Seed_Processed" WHERE status = "Seed_Snatched" and AlbumID=?',
|
||||
[albumid])
|
||||
|
||||
# Update the have tracks for all created dirs:
|
||||
for albumpath in albumpaths:
|
||||
librarysync.libraryScan(dir=albumpath, append=True, ArtistID=release['ArtistID'], ArtistName=release['ArtistName'])
|
||||
librarysync.libraryScan(dir=albumpath, append=True, ArtistID=release['ArtistID'],
|
||||
ArtistName=release['ArtistName'])
|
||||
|
||||
logger.info(u'Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle']))
|
||||
logger.info(
|
||||
u'Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle']))
|
||||
|
||||
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
|
||||
statusmessage = "Download and Postprocessing completed"
|
||||
@@ -530,7 +571,8 @@ def embedAlbumArt(artwork, downloaded_track_list):
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except:
|
||||
logger.error(u'Could not read %s. Not adding album art' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.error(u'Could not read %s. Not adding album art' % downloaded_track.decode(
|
||||
headphones.SYS_ENCODING, 'replace'))
|
||||
continue
|
||||
|
||||
logger.debug('Adding album art to: %s' % downloaded_track)
|
||||
@@ -539,7 +581,8 @@ def embedAlbumArt(artwork, downloaded_track_list):
|
||||
f.art = artwork
|
||||
f.save()
|
||||
except Exception as e:
|
||||
logger.error(u'Error embedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
|
||||
logger.error(u'Error embedding album art to: %s. Error: %s' % (
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
|
||||
continue
|
||||
|
||||
|
||||
@@ -552,16 +595,18 @@ def addAlbumArt(artwork, albumpath, release):
|
||||
year = ''
|
||||
|
||||
values = {'$Artist': release['ArtistName'],
|
||||
'$Album': release['AlbumTitle'],
|
||||
'$Year': year,
|
||||
'$artist': release['ArtistName'].lower(),
|
||||
'$album': release['AlbumTitle'].lower(),
|
||||
'$year': year
|
||||
}
|
||||
'$Album': release['AlbumTitle'],
|
||||
'$Year': year,
|
||||
'$artist': release['ArtistName'].lower(),
|
||||
'$album': release['AlbumTitle'].lower(),
|
||||
'$year': year
|
||||
}
|
||||
|
||||
album_art_name = helpers.replace_all(headphones.CONFIG.ALBUM_ART_FORMAT.strip(), values) + ".jpg"
|
||||
album_art_name = helpers.replace_all(headphones.CONFIG.ALBUM_ART_FORMAT.strip(),
|
||||
values) + ".jpg"
|
||||
|
||||
album_art_name = helpers.replace_illegal_chars(album_art_name).encode(headphones.SYS_ENCODING, 'replace')
|
||||
album_art_name = helpers.replace_illegal_chars(album_art_name).encode(headphones.SYS_ENCODING,
|
||||
'replace')
|
||||
|
||||
if headphones.CONFIG.FILE_UNDERSCORES:
|
||||
album_art_name = album_art_name.replace(' ', '_')
|
||||
@@ -587,7 +632,8 @@ def cleanupFiles(albumpath):
|
||||
try:
|
||||
os.remove(os.path.join(r, files))
|
||||
except Exception as e:
|
||||
logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
logger.error(u'Could not remove file: %s. Error: %s' % (
|
||||
files.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
|
||||
|
||||
def renameNFO(albumpath):
|
||||
@@ -596,12 +642,16 @@ def renameNFO(albumpath):
|
||||
for r, d, f in os.walk(albumpath):
|
||||
for file in f:
|
||||
if file.lower().endswith('.nfo'):
|
||||
logger.debug('Renaming: "%s" to "%s"' % (file.decode(headphones.SYS_ENCODING, 'replace'), file.decode(headphones.SYS_ENCODING, 'replace') + '-orig'))
|
||||
logger.debug('Renaming: "%s" to "%s"' % (
|
||||
file.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
file.decode(headphones.SYS_ENCODING, 'replace') + '-orig'))
|
||||
try:
|
||||
new_file_name = os.path.join(r, file)[:-3] + 'orig.nfo'
|
||||
os.rename(os.path.join(r, file), new_file_name)
|
||||
except Exception as e:
|
||||
logger.error(u'Could not rename file: %s. Error: %s' % (os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
logger.error(u'Could not rename file: %s. Error: %s' % (
|
||||
os.path.join(r, file).decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
|
||||
|
||||
def moveFiles(albumpath, release, tracks):
|
||||
logger.info("Moving files: %s" % albumpath)
|
||||
@@ -630,25 +680,26 @@ def moveFiles(albumpath, release, tracks):
|
||||
|
||||
for r, d, f in os.walk(albumpath):
|
||||
try:
|
||||
origfolder = os.path.basename(os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
|
||||
origfolder = os.path.basename(
|
||||
os.path.normpath(r).decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except:
|
||||
origfolder = u''
|
||||
|
||||
values = {'$Artist': artist,
|
||||
'$SortArtist': sortname,
|
||||
'$Album': album,
|
||||
'$Year': year,
|
||||
'$Type': releasetype,
|
||||
'$OriginalFolder': origfolder,
|
||||
'$First': firstchar.upper(),
|
||||
'$artist': artist.lower(),
|
||||
'$sortartist': sortname.lower(),
|
||||
'$album': album.lower(),
|
||||
'$year': year,
|
||||
'$type': releasetype.lower(),
|
||||
'$first': firstchar.lower(),
|
||||
'$originalfolder': origfolder.lower()
|
||||
}
|
||||
'$SortArtist': sortname,
|
||||
'$Album': album,
|
||||
'$Year': year,
|
||||
'$Type': releasetype,
|
||||
'$OriginalFolder': origfolder,
|
||||
'$First': firstchar.upper(),
|
||||
'$artist': artist.lower(),
|
||||
'$sortartist': sortname.lower(),
|
||||
'$album': album.lower(),
|
||||
'$year': year,
|
||||
'$type': releasetype.lower(),
|
||||
'$first': firstchar.lower(),
|
||||
'$originalfolder': origfolder.lower()
|
||||
}
|
||||
|
||||
folder = helpers.replace_all(headphones.CONFIG.FOLDER_FORMAT.strip(), values, normalize=True)
|
||||
|
||||
@@ -672,15 +723,20 @@ def moveFiles(albumpath, release, tracks):
|
||||
files_to_move.append(os.path.join(r, files))
|
||||
if any(files.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
|
||||
lossy_media = True
|
||||
if any(files.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
if any(files.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
lossless_media = True
|
||||
|
||||
# Do some sanity checking to see what directories we need to create:
|
||||
make_lossy_folder = False
|
||||
make_lossless_folder = False
|
||||
|
||||
lossy_destination_path = os.path.normpath(os.path.join(headphones.CONFIG.DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING, 'replace')
|
||||
lossless_destination_path = os.path.normpath(os.path.join(headphones.CONFIG.LOSSLESS_DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING, 'replace')
|
||||
lossy_destination_path = os.path.normpath(
|
||||
os.path.join(headphones.CONFIG.DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING,
|
||||
'replace')
|
||||
lossless_destination_path = os.path.normpath(
|
||||
os.path.join(headphones.CONFIG.LOSSLESS_DESTINATION_DIR, folder)).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
|
||||
# If they set a destination dir for lossless media, only create the lossy folder if there is lossy media
|
||||
if headphones.CONFIG.LOSSLESS_DESTINATION_DIR:
|
||||
@@ -704,7 +760,9 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
shutil.rmtree(lossless_destination_path)
|
||||
except Exception as e:
|
||||
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
logger.error(
|
||||
"Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (
|
||||
lossless_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
create_duplicate_folder = True
|
||||
|
||||
if not headphones.CONFIG.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
|
||||
@@ -713,7 +771,9 @@ def moveFiles(albumpath, release, tracks):
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = temp_folder + '[%i]' % i
|
||||
lossless_destination_path = os.path.normpath(os.path.join(headphones.CONFIG.LOSSLESS_DESTINATION_DIR, newfolder)).encode(headphones.SYS_ENCODING, 'replace')
|
||||
lossless_destination_path = os.path.normpath(
|
||||
os.path.join(headphones.CONFIG.LOSSLESS_DESTINATION_DIR, newfolder)).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
if os.path.exists(lossless_destination_path):
|
||||
i += 1
|
||||
else:
|
||||
@@ -724,7 +784,8 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
os.makedirs(lossless_destination_path)
|
||||
except Exception as e:
|
||||
logger.error('Could not create lossless folder for %s. (Error: %s)' % (release['AlbumTitle'], e))
|
||||
logger.error('Could not create lossless folder for %s. (Error: %s)' % (
|
||||
release['AlbumTitle'], e))
|
||||
if not make_lossy_folder:
|
||||
return [albumpath]
|
||||
|
||||
@@ -737,7 +798,9 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
shutil.rmtree(lossy_destination_path)
|
||||
except Exception as e:
|
||||
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
logger.error(
|
||||
"Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (
|
||||
lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
create_duplicate_folder = True
|
||||
|
||||
if not headphones.CONFIG.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
|
||||
@@ -746,7 +809,9 @@ def moveFiles(albumpath, release, tracks):
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = temp_folder + '[%i]' % i
|
||||
lossy_destination_path = os.path.normpath(os.path.join(headphones.CONFIG.DESTINATION_DIR, newfolder)).encode(headphones.SYS_ENCODING, 'replace')
|
||||
lossy_destination_path = os.path.normpath(
|
||||
os.path.join(headphones.CONFIG.DESTINATION_DIR, newfolder)).encode(
|
||||
headphones.SYS_ENCODING, 'replace')
|
||||
if os.path.exists(lossy_destination_path):
|
||||
i += 1
|
||||
else:
|
||||
@@ -757,7 +822,8 @@ def moveFiles(albumpath, release, tracks):
|
||||
try:
|
||||
os.makedirs(lossy_destination_path)
|
||||
except Exception as e:
|
||||
logger.error('Could not create folder for %s. Not moving: %s' % (release['AlbumTitle'], e))
|
||||
logger.error(
|
||||
'Could not create folder for %s. Not moving: %s' % (release['AlbumTitle'], e))
|
||||
return [albumpath]
|
||||
|
||||
logger.info('Checking which files we need to move.....')
|
||||
@@ -768,26 +834,34 @@ def moveFiles(albumpath, release, tracks):
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
|
||||
if any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
|
||||
if any(file_to_move.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSY_MEDIA_FORMATS):
|
||||
helpers.smartMove(file_to_move, lossy_destination_path)
|
||||
|
||||
elif any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
elif any(file_to_move.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
helpers.smartMove(file_to_move, lossless_destination_path)
|
||||
|
||||
# If it's a non-music file, move it to both dirs
|
||||
# TODO: Move specific-to-lossless files to the lossless dir only
|
||||
else:
|
||||
|
||||
moved_to_lossy_folder = helpers.smartMove(file_to_move, lossy_destination_path, delete=False)
|
||||
moved_to_lossless_folder = helpers.smartMove(file_to_move, lossless_destination_path, delete=False)
|
||||
moved_to_lossy_folder = helpers.smartMove(file_to_move, lossy_destination_path,
|
||||
delete=False)
|
||||
moved_to_lossless_folder = helpers.smartMove(file_to_move,
|
||||
lossless_destination_path,
|
||||
delete=False)
|
||||
|
||||
if moved_to_lossy_folder or moved_to_lossless_folder:
|
||||
try:
|
||||
os.remove(file_to_move)
|
||||
except Exception as e:
|
||||
logger.error("Error deleting file '" + file_to_move.decode(headphones.SYS_ENCODING, 'replace') + "' from source directory")
|
||||
logger.error(
|
||||
"Error deleting file '" + file_to_move.decode(headphones.SYS_ENCODING,
|
||||
'replace') + "' from source directory")
|
||||
else:
|
||||
logger.error("Error copying '" + file_to_move.decode(headphones.SYS_ENCODING, 'replace') + "'. Not deleting from download directory")
|
||||
logger.error("Error copying '" + file_to_move.decode(headphones.SYS_ENCODING,
|
||||
'replace') + "'. Not deleting from download directory")
|
||||
|
||||
elif make_lossless_folder and not make_lossy_folder:
|
||||
|
||||
@@ -817,11 +891,14 @@ def moveFiles(albumpath, release, tracks):
|
||||
|
||||
if headphones.CONFIG.FOLDER_PERMISSIONS_ENABLED:
|
||||
try:
|
||||
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'),
|
||||
int(headphones.CONFIG.FOLDER_PERMISSIONS, 8))
|
||||
except Exception as e:
|
||||
logger.error("Error trying to change permissions on folder: %s. %s", temp_f.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
logger.error("Error trying to change permissions on folder: %s. %s",
|
||||
temp_f.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
else:
|
||||
logger.debug("Not changing folder permissions, since it is disabled: %s", temp_f.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.debug("Not changing folder permissions, since it is disabled: %s",
|
||||
temp_f.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
# If we failed to move all the files out of the directory, this will fail too
|
||||
try:
|
||||
@@ -840,7 +917,6 @@ def moveFiles(albumpath, release, tracks):
|
||||
|
||||
|
||||
def correctMetadata(albumid, release, downloaded_track_list):
|
||||
|
||||
logger.info('Preparing to write metadata to tracks....')
|
||||
lossy_items = []
|
||||
lossless_items = []
|
||||
@@ -850,14 +926,18 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
|
||||
try:
|
||||
|
||||
if any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
if any(downloaded_track.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
lossless_items.append(beets.library.Item.from_path(downloaded_track))
|
||||
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
|
||||
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSY_MEDIA_FORMATS):
|
||||
lossy_items.append(beets.library.Item.from_path(downloaded_track))
|
||||
else:
|
||||
logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn("Skipping: %s because it is not a mutagen friendly file format",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except Exception as e:
|
||||
logger.error("Beets couldn't create an Item from: %s - not a media file? %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
logger.error("Beets couldn't create an Item from: %s - not a media file? %s",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
|
||||
for items in [lossy_items, lossless_items]:
|
||||
|
||||
@@ -865,18 +945,24 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
continue
|
||||
|
||||
try:
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle']))
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items,
|
||||
search_artist=helpers.latinToAscii(
|
||||
release['ArtistName']),
|
||||
search_album=helpers.latinToAscii(
|
||||
release['AlbumTitle']))
|
||||
except Exception as e:
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata', e)
|
||||
return False
|
||||
if str(rec) == 'Recommendation.none':
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
return False
|
||||
|
||||
if candidates:
|
||||
dist, info, mapping, extra_items, extra_tracks = candidates[0]
|
||||
else:
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
return False
|
||||
|
||||
logger.info('Beets recommendation for tagging items: %s' % rec)
|
||||
@@ -896,13 +982,16 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
for item in items:
|
||||
try:
|
||||
item.write()
|
||||
logger.info("Successfully applied metadata to: %s", item.path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("Successfully applied metadata to: %s",
|
||||
item.path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except Exception as e:
|
||||
logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
logger.warn("Error writing metadata to '%s': %s",
|
||||
item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def embedLyrics(downloaded_track_list):
|
||||
logger.info('Adding lyrics')
|
||||
|
||||
@@ -916,14 +1005,18 @@ def embedLyrics(downloaded_track_list):
|
||||
for downloaded_track in downloaded_track_list:
|
||||
|
||||
try:
|
||||
if any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
if any(downloaded_track.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
lossless_items.append(beets.library.Item.from_path(downloaded_track))
|
||||
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
|
||||
elif any(downloaded_track.lower().endswith('.' + x.lower()) for x in
|
||||
headphones.LOSSY_MEDIA_FORMATS):
|
||||
lossy_items.append(beets.library.Item.from_path(downloaded_track))
|
||||
else:
|
||||
logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.warn("Skipping: %s because it is not a mutagen friendly file format",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except Exception as e:
|
||||
logger.error("Beets couldn't create an Item from: %s - not a media file? %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
logger.error("Beets couldn't create an Item from: %s - not a media file? %s",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
|
||||
for items in [lossy_items, lossless_items]:
|
||||
|
||||
@@ -963,7 +1056,8 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except:
|
||||
logger.info("MediaFile couldn't parse: %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.info("MediaFile couldn't parse: %s",
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
continue
|
||||
|
||||
if not f.disc:
|
||||
@@ -998,26 +1092,28 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
sortname = artistname
|
||||
|
||||
values = {'$Disc': discnumber,
|
||||
'$Track': tracknumber,
|
||||
'$Title': title,
|
||||
'$Artist': artistname,
|
||||
'$SortArtist': sortname,
|
||||
'$Album': release['AlbumTitle'],
|
||||
'$Year': year,
|
||||
'$disc': discnumber,
|
||||
'$track': tracknumber,
|
||||
'$title': title.lower(),
|
||||
'$artist': artistname.lower(),
|
||||
'$sortartist': sortname.lower(),
|
||||
'$album': release['AlbumTitle'].lower(),
|
||||
'$year': year
|
||||
}
|
||||
'$Track': tracknumber,
|
||||
'$Title': title,
|
||||
'$Artist': artistname,
|
||||
'$SortArtist': sortname,
|
||||
'$Album': release['AlbumTitle'],
|
||||
'$Year': year,
|
||||
'$disc': discnumber,
|
||||
'$track': tracknumber,
|
||||
'$title': title.lower(),
|
||||
'$artist': artistname.lower(),
|
||||
'$sortartist': sortname.lower(),
|
||||
'$album': release['AlbumTitle'].lower(),
|
||||
'$year': year
|
||||
}
|
||||
|
||||
ext = os.path.splitext(downloaded_track)[1]
|
||||
|
||||
new_file_name = helpers.replace_all(headphones.CONFIG.FILE_FORMAT.strip(), values).replace('/', '_') + ext
|
||||
new_file_name = helpers.replace_all(headphones.CONFIG.FILE_FORMAT.strip(),
|
||||
values).replace('/', '_') + ext
|
||||
|
||||
new_file_name = helpers.replace_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING, 'replace')
|
||||
new_file_name = helpers.replace_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING,
|
||||
'replace')
|
||||
|
||||
if headphones.CONFIG.FILE_UNDERSCORES:
|
||||
new_file_name = new_file_name.replace(' ', '_')
|
||||
@@ -1028,19 +1124,22 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
new_file = os.path.join(albumpath, new_file_name)
|
||||
|
||||
if downloaded_track == new_file_name:
|
||||
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary")
|
||||
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING,
|
||||
'replace') + " is not neccessary")
|
||||
continue
|
||||
|
||||
logger.debug('Renaming %s ---> %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), new_file_name.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.debug('Renaming %s ---> %s',
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'),
|
||||
new_file_name.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
try:
|
||||
os.rename(downloaded_track, new_file)
|
||||
except Exception as e:
|
||||
logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
logger.error('Error renaming file: %s. Error: %s',
|
||||
downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
continue
|
||||
|
||||
|
||||
def updateFilePermissions(albumpaths):
|
||||
|
||||
for folder in albumpaths:
|
||||
logger.info("Updating file permissions in %s", folder)
|
||||
for r, d, f in os.walk(folder):
|
||||
@@ -1053,7 +1152,8 @@ def updateFilePermissions(albumpaths):
|
||||
logger.error("Could not change permissions for file: %s", full_path)
|
||||
continue
|
||||
else:
|
||||
logger.debug("Not changing file permissions, since it is disabled: %s", full_path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
logger.debug("Not changing file permissions, since it is disabled: %s",
|
||||
full_path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
def renameUnprocessedFolder(path, tag):
|
||||
@@ -1076,7 +1176,6 @@ def renameUnprocessedFolder(path, tag):
|
||||
|
||||
|
||||
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_original_folder=False):
|
||||
|
||||
logger.info('Force checking download folder for completed downloads')
|
||||
|
||||
ignored = 0
|
||||
@@ -1089,9 +1188,11 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
if dir:
|
||||
download_dirs.append(dir.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
if headphones.CONFIG.DOWNLOAD_DIR and not dir:
|
||||
download_dirs.append(headphones.CONFIG.DOWNLOAD_DIR.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
download_dirs.append(
|
||||
headphones.CONFIG.DOWNLOAD_DIR.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
if headphones.CONFIG.DOWNLOAD_TORRENT_DIR and not dir:
|
||||
download_dirs.append(headphones.CONFIG.DOWNLOAD_TORRENT_DIR.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
download_dirs.append(
|
||||
headphones.CONFIG.DOWNLOAD_TORRENT_DIR.encode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
# If DOWNLOAD_DIR and DOWNLOAD_TORRENT_DIR are the same, remove the duplicate to prevent us from trying to process the same folder twice.
|
||||
download_dirs = list(set(download_dirs))
|
||||
@@ -1108,7 +1209,8 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
# Scan for subfolders
|
||||
subfolders = os.listdir(download_dir)
|
||||
ignored += helpers.path_filter_patterns(subfolders,
|
||||
headphones.CONFIG.IGNORED_FOLDERS, root=download_dir)
|
||||
headphones.CONFIG.IGNORED_FOLDERS,
|
||||
root=download_dir)
|
||||
|
||||
for folder in subfolders:
|
||||
path_to_folder = os.path.join(download_dir, folder)
|
||||
@@ -1125,7 +1227,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
if folders:
|
||||
logger.debug('Expanded post processing folders: %s', folders)
|
||||
logger.info('Found %d folders to process (%d ignored).',
|
||||
len(folders), ignored)
|
||||
len(folders), ignored)
|
||||
else:
|
||||
logger.info('Found no folders to process. Aborting.')
|
||||
return
|
||||
@@ -1143,15 +1245,23 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
# underscores -> dots (this might be hit or miss since it assumes all
|
||||
# spaces/underscores came from sab replacing values
|
||||
logger.debug('Attempting to find album in the snatched table')
|
||||
snatched = myDB.action('SELECT AlbumID, Title, Kind, Status from snatched WHERE FolderName LIKE ?', [folder_basename]).fetchone()
|
||||
snatched = myDB.action(
|
||||
'SELECT AlbumID, Title, Kind, Status from snatched WHERE FolderName LIKE ?',
|
||||
[folder_basename]).fetchone()
|
||||
|
||||
if snatched:
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES and snatched['Kind'] == 'torrent' and snatched['Status'] == 'Processed':
|
||||
logger.info('%s is a torrent folder being preserved for seeding and has already been processed. Skipping.', folder_basename)
|
||||
if headphones.CONFIG.KEEP_TORRENT_FILES and snatched['Kind'] == 'torrent' and snatched[
|
||||
'Status'] == 'Processed':
|
||||
logger.info(
|
||||
'%s is a torrent folder being preserved for seeding and has already been processed. Skipping.',
|
||||
folder_basename)
|
||||
continue
|
||||
else:
|
||||
logger.info('Found a match in the database: %s. Verifying to make sure it is the correct album', snatched['Title'])
|
||||
verify(snatched['AlbumID'], folder, snatched['Kind'], keep_original_folder=keep_original_folder)
|
||||
logger.info(
|
||||
'Found a match in the database: %s. Verifying to make sure it is the correct album',
|
||||
snatched['Title'])
|
||||
verify(snatched['AlbumID'], folder, snatched['Kind'],
|
||||
keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
|
||||
# Attempt 2: strip release group id from filename
|
||||
@@ -1165,13 +1275,19 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
|
||||
if rgid:
|
||||
rgid = possible_rgid
|
||||
release = myDB.action('SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?', [rgid]).fetchone()
|
||||
release = myDB.action(
|
||||
'SELECT ArtistName, AlbumTitle, AlbumID from albums WHERE AlbumID=?',
|
||||
[rgid]).fetchone()
|
||||
if release:
|
||||
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, forced=True, keep_original_folder=keep_original_folder)
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, forced=True,
|
||||
keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Found a (possibly) valid Musicbrainz release group id in album folder name.')
|
||||
logger.info(
|
||||
'Found a (possibly) valid Musicbrainz release group id in album folder name.')
|
||||
verify(rgid, folder, forced=True)
|
||||
continue
|
||||
|
||||
@@ -1184,13 +1300,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
name = album = year = None
|
||||
|
||||
if name and album:
|
||||
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone()
|
||||
release = myDB.action(
|
||||
'SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?',
|
||||
[name, album]).fetchone()
|
||||
if release:
|
||||
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
||||
album)
|
||||
try:
|
||||
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
|
||||
except:
|
||||
@@ -1219,13 +1340,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
name = album = None
|
||||
|
||||
if name and album:
|
||||
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?', [name, album]).fetchone()
|
||||
release = myDB.action(
|
||||
'SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE ArtistName LIKE ? and AlbumTitle LIKE ?',
|
||||
[name, album]).fetchone()
|
||||
if release:
|
||||
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name, album)
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s - %s', name,
|
||||
album)
|
||||
try:
|
||||
rgid = mb.findAlbumID(helpers.latinToAscii(name), helpers.latinToAscii(album))
|
||||
except:
|
||||
@@ -1243,13 +1369,18 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
logger.debug('Attempt to extract album name by assuming it is the folder name')
|
||||
|
||||
if '-' not in folder_basename:
|
||||
release = myDB.action('SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?', [folder_basename]).fetchone()
|
||||
release = myDB.action(
|
||||
'SELECT AlbumID, ArtistName, AlbumTitle from albums WHERE AlbumTitle LIKE ?',
|
||||
[folder_basename]).fetchone()
|
||||
if release:
|
||||
logger.info('Found a match in the database: %s - %s. Verifying to make sure it is the correct album', release['ArtistName'], release['AlbumTitle'])
|
||||
logger.info(
|
||||
'Found a match in the database: %s - %s. Verifying to make sure it is the correct album',
|
||||
release['ArtistName'], release['AlbumTitle'])
|
||||
verify(release['AlbumID'], folder, keep_original_folder=keep_original_folder)
|
||||
continue
|
||||
else:
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s', folder_basename)
|
||||
logger.info('Querying MusicBrainz for the release group id for: %s',
|
||||
folder_basename)
|
||||
try:
|
||||
rgid = mb.findAlbumID(album=helpers.latinToAscii(folder_basename))
|
||||
except:
|
||||
@@ -1264,6 +1395,6 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
|
||||
|
||||
# Fail here
|
||||
logger.info("Couldn't parse '%s' into any valid format. If adding " \
|
||||
"albums from another source, they must be in an 'Artist - Album " \
|
||||
"[Year]' format, or end with the musicbrainz release group id.",
|
||||
folder_basename)
|
||||
"albums from another source, they must be in an 'Artist - Album " \
|
||||
"[Year]' format, or end with the musicbrainz release group id.",
|
||||
folder_basename)
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import logger
|
||||
|
||||
from xml.dom import minidom
|
||||
from bs4 import BeautifulSoup
|
||||
import collections
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
from headphones import logger
|
||||
import feedparser
|
||||
import headphones
|
||||
import headphones.lock
|
||||
import collections
|
||||
|
||||
|
||||
# Disable SSL certificate warnings. We have our own handling
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
@@ -211,7 +211,7 @@ def server_message(response):
|
||||
|
||||
# First attempt is to 'read' the response as HTML
|
||||
if response.headers.get("content-type") and \
|
||||
"text/html" in response.headers.get("content-type"):
|
||||
"text/html" in response.headers.get("content-type"):
|
||||
try:
|
||||
soup = BeautifulSoup(response.content, "html5lib")
|
||||
except Exception:
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import urllib
|
||||
import requests as requests
|
||||
from urlparse import urlparse
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
from urlparse import urlparse
|
||||
|
||||
import re
|
||||
import requests as requests
|
||||
from bs4 import BeautifulSoup
|
||||
import headphones
|
||||
from headphones import logger
|
||||
|
||||
class Rutracker(object):
|
||||
|
||||
class Rutracker(object):
|
||||
def __init__(self):
|
||||
self.session = requests.session()
|
||||
self.timeout = 60
|
||||
@@ -58,7 +56,8 @@ class Rutracker(object):
|
||||
self.loggedin = True
|
||||
logger.info("Successfully logged in to rutracker")
|
||||
else:
|
||||
logger.error("Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
logger.error(
|
||||
"Could not login to rutracker, credentials maybe incorrect, site is down or too many attempts. Try again later")
|
||||
self.loggedin = False
|
||||
return self.loggedin
|
||||
except Exception as e:
|
||||
@@ -111,7 +110,7 @@ class Rutracker(object):
|
||||
soup = BeautifulSoup(r.content, 'html5lib')
|
||||
|
||||
# Debug
|
||||
#logger.debug (soup.prettify())
|
||||
# logger.debug (soup.prettify())
|
||||
|
||||
# Check if still logged in
|
||||
if not self.still_logged_in(soup):
|
||||
@@ -130,7 +129,8 @@ class Rutracker(object):
|
||||
return None
|
||||
minimumseeders = int(headphones.CONFIG.NUMBEROFSEEDERS) - 1
|
||||
|
||||
for item in zip(i.find_all(class_='hl-tags'),i.find_all(class_='dl-stub'),i.find_all(class_='seedmed')):
|
||||
for item in zip(i.find_all(class_='hl-tags'), i.find_all(class_='dl-stub'),
|
||||
i.find_all(class_='seedmed')):
|
||||
title = item[0].get_text()
|
||||
url = item[1].get('href')
|
||||
size_formatted = item[1].get_text()[:-2]
|
||||
@@ -149,12 +149,15 @@ class Rutracker(object):
|
||||
|
||||
if size < self.maxsize and minimumseeders < int(seeds):
|
||||
logger.info('Found %s. Size: %s' % (title, size_formatted))
|
||||
#Torrent topic page
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])['t']
|
||||
# Torrent topic page
|
||||
torrent_id = dict([part.split('=') for part in urlparse(url)[4].split('&')])[
|
||||
't']
|
||||
topicurl = 'http://rutracker.org/forum/viewtopic.php?t=' + torrent_id
|
||||
rulist.append((title, size, topicurl, 'rutracker.org', 'torrent', True))
|
||||
else:
|
||||
logger.info("%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)" % (title, size, int(seeds)))
|
||||
logger.info(
|
||||
"%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)" % (
|
||||
title, size, int(seeds)))
|
||||
|
||||
if not rulist:
|
||||
logger.info("No valid results found from rutracker")
|
||||
@@ -165,7 +168,6 @@ class Rutracker(object):
|
||||
logger.error("An unknown error occurred in the rutracker parser: %s" % e)
|
||||
return None
|
||||
|
||||
|
||||
def get_torrent_data(self, url):
|
||||
"""
|
||||
return the .torrent data
|
||||
@@ -176,14 +178,14 @@ class Rutracker(object):
|
||||
cookie = {'bb_dl': torrent_id}
|
||||
try:
|
||||
headers = {'Referer': url}
|
||||
r = self.session.post(url=downloadurl, cookies=cookie, headers=headers, timeout=self.timeout)
|
||||
r = self.session.post(url=downloadurl, cookies=cookie, headers=headers,
|
||||
timeout=self.timeout)
|
||||
return r.content
|
||||
except Exception as e:
|
||||
logger.error('Error getting torrent: %s', e)
|
||||
return False
|
||||
|
||||
|
||||
#TODO get this working in utorrent.py
|
||||
# TODO get this working in utorrent.py
|
||||
def utorrent_add_file(self, data):
|
||||
|
||||
host = headphones.CONFIG.UTORRENT_HOST
|
||||
@@ -197,7 +199,8 @@ class Rutracker(object):
|
||||
base_url = host
|
||||
|
||||
url = base_url + '/gui/'
|
||||
self.session.auth = (headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD)
|
||||
self.session.auth = (
|
||||
headphones.CONFIG.UTORRENT_USERNAME, headphones.CONFIG.UTORRENT_PASSWORD)
|
||||
|
||||
try:
|
||||
r = self.session.get(url + 'token.html')
|
||||
@@ -221,4 +224,3 @@ class Rutracker(object):
|
||||
self.session.post(url, params={'action': 'add-file'}, files=files)
|
||||
except Exception as e:
|
||||
logger.exception('Error adding file to utorrent %s', e)
|
||||
|
||||
|
||||
@@ -13,26 +13,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#####################################
|
||||
## Stolen from Sick-Beard's sab.py ##
|
||||
#####################################
|
||||
###################################
|
||||
# Stolen from Sick-Beard's sab.py #
|
||||
###################################
|
||||
|
||||
import MultipartPostHandler
|
||||
import headphones
|
||||
import cookielib
|
||||
import httplib
|
||||
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger, helpers, request
|
||||
|
||||
|
||||
def sab_api_call(request_type=None, params={}, **kwargs):
|
||||
|
||||
if not headphones.CONFIG.SAB_HOST.startswith('http'):
|
||||
headphones.CONFIG.SAB_HOST = 'http://' + headphones.CONFIG.SAB_HOST
|
||||
|
||||
if headphones.CONFIG.SAB_HOST.endswith('/'):
|
||||
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[0:len(headphones.CONFIG.SAB_HOST) - 1]
|
||||
headphones.CONFIG.SAB_HOST = headphones.CONFIG.SAB_HOST[
|
||||
0:len(headphones.CONFIG.SAB_HOST) - 1]
|
||||
|
||||
url = headphones.CONFIG.SAB_HOST + "/" + "api?"
|
||||
|
||||
@@ -42,11 +40,11 @@ def sab_api_call(request_type=None, params={}, **kwargs):
|
||||
params['ma_password'] = headphones.CONFIG.SAB_PASSWORD
|
||||
if headphones.CONFIG.SAB_APIKEY:
|
||||
params['apikey'] = headphones.CONFIG.SAB_APIKEY
|
||||
|
||||
if request_type=='send_nzb' and headphones.CONFIG.SAB_CATEGORY:
|
||||
|
||||
if request_type == 'send_nzb' and headphones.CONFIG.SAB_CATEGORY:
|
||||
params['cat'] = headphones.CONFIG.SAB_CATEGORY
|
||||
|
||||
params['output']='json'
|
||||
params['output'] = 'json'
|
||||
|
||||
response = request.request_json(url, params=params, **kwargs)
|
||||
|
||||
@@ -57,8 +55,8 @@ def sab_api_call(request_type=None, params={}, **kwargs):
|
||||
logger.debug("Successfully connected to SABnzbd on url: %s" % headphones.CONFIG.SAB_HOST)
|
||||
return response
|
||||
|
||||
def sendNZB(nzb):
|
||||
|
||||
def sendNZB(nzb):
|
||||
params = {}
|
||||
# if it's a normal result we just pass SAB the URL
|
||||
if nzb.resultType == "nzb":
|
||||
@@ -87,7 +85,8 @@ def sendNZB(nzb):
|
||||
response = sab_api_call('send_nzb', params=params)
|
||||
elif nzb.resultType == "nzbdata":
|
||||
cookies = cookielib.CookieJar()
|
||||
response = sab_api_call('send_nzb', params=params, method="post", files=files, cookies=cookies, headers=headers)
|
||||
response = sab_api_call('send_nzb', params=params, method="post", files=files,
|
||||
cookies=cookies, headers=headers)
|
||||
|
||||
if not response:
|
||||
logger.info(u"No data returned from SABnzbd, NZB not sent")
|
||||
@@ -102,15 +101,15 @@ def sendNZB(nzb):
|
||||
|
||||
|
||||
def checkConfig():
|
||||
|
||||
params = {'mode': 'get_config',
|
||||
'section': 'misc',
|
||||
}
|
||||
'section': 'misc',
|
||||
}
|
||||
|
||||
config_options = sab_api_call(params=params)
|
||||
|
||||
|
||||
if not config_options:
|
||||
logger.warn("Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
|
||||
logger.warn(
|
||||
"Unable to read SABnzbd config file - cannot determine renaming options (might affect auto & forced post processing)")
|
||||
return (0, 0)
|
||||
|
||||
replace_spaces = config_options['config']['misc']['replace_spaces']
|
||||
|
||||
@@ -15,35 +15,32 @@
|
||||
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||
|
||||
import urllib
|
||||
import urlparse
|
||||
from pygazelle import api as gazelleapi
|
||||
from pygazelle import encoding as gazelleencoding
|
||||
from pygazelle import format as gazelleformat
|
||||
from base64 import b16encode, b32decode
|
||||
from hashlib import sha1
|
||||
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
import shutil
|
||||
import random
|
||||
import urllib
|
||||
import datetime
|
||||
import headphones
|
||||
import subprocess
|
||||
import unicodedata
|
||||
import urlparse
|
||||
|
||||
import os
|
||||
import re
|
||||
from pygazelle import api as gazelleapi
|
||||
from pygazelle import encoding as gazelleencoding
|
||||
from pygazelle import format as gazelleformat
|
||||
import headphones
|
||||
from headphones.common import USER_AGENT
|
||||
from headphones import logger, db, helpers, classes, sab, nzbget, request
|
||||
from headphones import utorrent, transmission, notifiers, rutracker
|
||||
|
||||
from bencode import bencode, bdecode
|
||||
|
||||
|
||||
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
||||
TORRENT_TO_MAGNET_SERVICES = [
|
||||
#'https://zoink.it/torrent/%s.torrent',
|
||||
#'http://torrage.com/torrent/%s.torrent',
|
||||
# 'https://zoink.it/torrent/%s.torrent',
|
||||
# 'http://torrage.com/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
@@ -123,10 +120,10 @@ def read_torrent_name(torrent_file, default_name=None):
|
||||
except KeyError:
|
||||
if default_name:
|
||||
logger.warning("Couldn't get name from torrent file: %s. " \
|
||||
"Defaulting to '%s'", e, default_name)
|
||||
"Defaulting to '%s'", e, default_name)
|
||||
else:
|
||||
logger.warning("Couldn't get name from torrent file: %s. No " \
|
||||
"default given", e)
|
||||
"default given", e)
|
||||
|
||||
# Return default
|
||||
return default_name
|
||||
@@ -147,7 +144,7 @@ def calculate_torrent_hash(link, data=None):
|
||||
torrent_hash = sha1(bencode(info)).hexdigest()
|
||||
else:
|
||||
raise ValueError("Cannot calculate torrent hash without magnet link " \
|
||||
"or data")
|
||||
"or data")
|
||||
|
||||
return torrent_hash.upper()
|
||||
|
||||
@@ -173,7 +170,7 @@ def get_seed_ratio(provider):
|
||||
elif provider == 'Mininova':
|
||||
seed_ratio = headphones.CONFIG.MININOVA_RATIO
|
||||
elif provider == 'Strike':
|
||||
seed_ratio = headphones.CONFIG.STRIKE_RATIO
|
||||
seed_ratio = headphones.CONFIG.STRIKE_RATIO
|
||||
else:
|
||||
seed_ratio = None
|
||||
|
||||
@@ -187,13 +184,13 @@ def get_seed_ratio(provider):
|
||||
|
||||
|
||||
def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
choose_specific_download=False):
|
||||
|
||||
choose_specific_download=False):
|
||||
logger.info('Searching for wanted albums')
|
||||
myDB = db.DBConnection()
|
||||
|
||||
if not albumid:
|
||||
results = myDB.select('SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"')
|
||||
results = myDB.select(
|
||||
'SELECT * from albums WHERE Status="Wanted" OR Status="Wanted Lossless"')
|
||||
|
||||
for album in results:
|
||||
|
||||
@@ -205,11 +202,13 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
try:
|
||||
release_date = datetime.datetime.strptime(album['ReleaseDate'], "%Y-%m-%d")
|
||||
except:
|
||||
logger.warn("No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
|
||||
logger.warn(
|
||||
"No valid date for: %s. Skipping automatic search" % album['AlbumTitle'])
|
||||
continue
|
||||
|
||||
if release_date > datetime.datetime.today():
|
||||
logger.info("Skipping: %s. Waiting for release date of: %s" % (album['AlbumTitle'], album['ReleaseDate']))
|
||||
logger.info("Skipping: %s. Waiting for release date of: %s" % (
|
||||
album['AlbumTitle'], album['ReleaseDate']))
|
||||
continue
|
||||
|
||||
new = True
|
||||
@@ -217,7 +216,8 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
if album['Status'] == "Wanted Lossless":
|
||||
losslessOnly = True
|
||||
|
||||
logger.info('Searching for "%s - %s" since it is marked as wanted' % (album['ArtistName'], album['AlbumTitle']))
|
||||
logger.info('Searching for "%s - %s" since it is marked as wanted' % (
|
||||
album['ArtistName'], album['AlbumTitle']))
|
||||
do_sorted_search(album, new, losslessOnly)
|
||||
|
||||
elif albumid and choose_specific_download:
|
||||
@@ -228,21 +228,25 @@ def searchforalbum(albumid=None, new=False, losslessOnly=False,
|
||||
|
||||
else:
|
||||
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||
logger.info('Searching for "%s - %s" since it was marked as wanted' % (album['ArtistName'], album['AlbumTitle']))
|
||||
logger.info('Searching for "%s - %s" since it was marked as wanted' % (
|
||||
album['ArtistName'], album['AlbumTitle']))
|
||||
do_sorted_search(album, new, losslessOnly)
|
||||
|
||||
logger.info('Search for wanted albums complete')
|
||||
|
||||
|
||||
def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
|
||||
NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or headphones.CONFIG.NEWZNAB or headphones.CONFIG.NZBSORG or headphones.CONFIG.OMGWTFNZBS)
|
||||
NZB_DOWNLOADERS = (headphones.CONFIG.SAB_HOST or headphones.CONFIG.BLACKHOLE_DIR or headphones.CONFIG.NZBGET_HOST)
|
||||
TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or headphones.CONFIG.KAT or headphones.CONFIG.PIRATEBAY or headphones.CONFIG.OLDPIRATEBAY or headphones.CONFIG.MININOVA or headphones.CONFIG.WAFFLES or headphones.CONFIG.RUTRACKER or headphones.CONFIG.WHATCD or headphones.CONFIG.STRIKE)
|
||||
NZB_PROVIDERS = (
|
||||
headphones.CONFIG.HEADPHONES_INDEXER or headphones.CONFIG.NEWZNAB or headphones.CONFIG.NZBSORG or headphones.CONFIG.OMGWTFNZBS)
|
||||
NZB_DOWNLOADERS = (
|
||||
headphones.CONFIG.SAB_HOST or headphones.CONFIG.BLACKHOLE_DIR or headphones.CONFIG.NZBGET_HOST)
|
||||
TORRENT_PROVIDERS = (
|
||||
headphones.CONFIG.TORZNAB or headphones.CONFIG.KAT or headphones.CONFIG.PIRATEBAY or headphones.CONFIG.OLDPIRATEBAY or headphones.CONFIG.MININOVA or headphones.CONFIG.WAFFLES or headphones.CONFIG.RUTRACKER or headphones.CONFIG.WHATCD or headphones.CONFIG.STRIKE)
|
||||
|
||||
results = []
|
||||
myDB = db.DBConnection()
|
||||
albumlength = myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0]
|
||||
albumlength = \
|
||||
myDB.select('SELECT sum(TrackDuration) from tracks WHERE AlbumID=?', [album['AlbumID']])[0][0]
|
||||
|
||||
if headphones.CONFIG.PREFER_TORRENTS == 0 and not choose_specific_download:
|
||||
|
||||
@@ -269,7 +273,8 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
nzb_results = searchNZB(album, new, losslessOnly, albumlength, choose_specific_download)
|
||||
|
||||
if TORRENT_PROVIDERS:
|
||||
torrent_results = searchTorrent(album, new, losslessOnly, albumlength, choose_specific_download)
|
||||
torrent_results = searchTorrent(album, new, losslessOnly, albumlength,
|
||||
choose_specific_download)
|
||||
|
||||
if not nzb_results:
|
||||
nzb_results = []
|
||||
@@ -283,7 +288,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
||||
return results
|
||||
|
||||
# Filter all results that do not comply
|
||||
results = [ result for result in results if result[5] ]
|
||||
results = [result for result in results if result[5]]
|
||||
|
||||
# Sort the remaining results
|
||||
sorted_search_results = sort_search_results(results, album, new, albumlength)
|
||||
@@ -305,14 +310,14 @@ def removeDisallowedFilenameChars(filename):
|
||||
|
||||
|
||||
def more_filtering(results, album, albumlength, new):
|
||||
|
||||
low_size_limit = None
|
||||
high_size_limit = None
|
||||
allow_lossless = False
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# Lossless - ignore results if target size outside bitrate range
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 and albumlength and (headphones.CONFIG.LOSSLESS_BITRATE_FROM or headphones.CONFIG.LOSSLESS_BITRATE_TO):
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 and albumlength and (
|
||||
headphones.CONFIG.LOSSLESS_BITRATE_FROM or headphones.CONFIG.LOSSLESS_BITRATE_TO):
|
||||
if headphones.CONFIG.LOSSLESS_BITRATE_FROM:
|
||||
low_size_limit = albumlength / 1000 * int(headphones.CONFIG.LOSSLESS_BITRATE_FROM) * 128
|
||||
if headphones.CONFIG.LOSSLESS_BITRATE_TO:
|
||||
@@ -325,9 +330,11 @@ def more_filtering(results, album, albumlength, new):
|
||||
targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128
|
||||
logger.info('Target size: %s' % helpers.bytes_to_mb(targetsize))
|
||||
if headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER:
|
||||
low_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100
|
||||
low_size_limit = targetsize * int(
|
||||
headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER) / 100
|
||||
if headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER:
|
||||
high_size_limit = targetsize * int(headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100
|
||||
high_size_limit = targetsize * int(
|
||||
headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER) / 100
|
||||
if headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS:
|
||||
allow_lossless = True
|
||||
|
||||
@@ -335,22 +342,18 @@ def more_filtering(results, album, albumlength, new):
|
||||
|
||||
for result in results:
|
||||
|
||||
normalizedAlbumArtist = removeDisallowedFilenameChars(album['ArtistName'])
|
||||
normalizedAlbumTitle = removeDisallowedFilenameChars(album['AlbumTitle'])
|
||||
normalizedResultTitle = removeDisallowedFilenameChars(result[0])
|
||||
artistTitleCount = normalizedResultTitle.count(normalizedAlbumArtist)
|
||||
|
||||
# WHAT DOES THIS DO?
|
||||
#if normalizedAlbumArtist in normalizedAlbumTitle and artistTitleCount < 2:
|
||||
# logger.info("Removing %s from %s" % (result[0], result[3]))
|
||||
# continue
|
||||
|
||||
if low_size_limit and (int(result[1]) < low_size_limit):
|
||||
logger.info("%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(low_size_limit))
|
||||
logger.info(
|
||||
"%s from %s is too small for this album - not considering it. (Size: %s, Minsize: %s)",
|
||||
result[0], result[3], helpers.bytes_to_mb(result[1]),
|
||||
helpers.bytes_to_mb(low_size_limit))
|
||||
continue
|
||||
|
||||
if high_size_limit and (int(result[1]) > high_size_limit):
|
||||
logger.info("%s from %s is too large for this album - not considering it. (Size: %s, Maxsize: %s)", result[0], result[3], helpers.bytes_to_mb(result[1]), helpers.bytes_to_mb(high_size_limit))
|
||||
logger.info(
|
||||
"%s from %s is too large for this album - not considering it. (Size: %s, Maxsize: %s)",
|
||||
result[0], result[3], helpers.bytes_to_mb(result[1]),
|
||||
helpers.bytes_to_mb(high_size_limit))
|
||||
|
||||
# Keep lossless results if there are no good lossy matches
|
||||
if not (allow_lossless and 'flac' in result[0].lower()):
|
||||
@@ -360,7 +363,8 @@ def more_filtering(results, album, albumlength, new):
|
||||
alreadydownloaded = myDB.select('SELECT * from snatched WHERE URL=?', [result[2]])
|
||||
|
||||
if len(alreadydownloaded):
|
||||
logger.info('%s has already been downloaded from %s. Skipping.' % (result[0], result[3]))
|
||||
logger.info(
|
||||
'%s has already been downloaded from %s. Skipping.' % (result[0], result[3]))
|
||||
continue
|
||||
|
||||
newlist.append(result)
|
||||
@@ -371,9 +375,9 @@ def more_filtering(results, album, albumlength, new):
|
||||
|
||||
|
||||
def sort_search_results(resultlist, album, new, albumlength):
|
||||
|
||||
if new and not len(resultlist):
|
||||
logger.info('No more results found for: %s - %s' % (album['ArtistName'], album['AlbumTitle']))
|
||||
logger.info(
|
||||
'No more results found for: %s - %s' % (album['ArtistName'], album['AlbumTitle']))
|
||||
return None
|
||||
|
||||
# Add a priority if it has any of the preferred words
|
||||
@@ -387,7 +391,8 @@ def sort_search_results(resultlist, album, new, albumlength):
|
||||
if any(word.lower() in result[0].lower() for word in preferred_words):
|
||||
priority = 1
|
||||
# add a search provider priority (weighted based on position)
|
||||
i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()), None)
|
||||
i = next((i for i, word in enumerate(preferred_words) if word in result[3].lower()),
|
||||
None)
|
||||
if i is not None:
|
||||
priority += round((len(preferred_words) - i) / float(len(preferred_words)), 2)
|
||||
|
||||
@@ -401,8 +406,10 @@ def sort_search_results(resultlist, album, new, albumlength):
|
||||
targetsize = albumlength / 1000 * int(headphones.CONFIG.PREFERRED_BITRATE) * 128
|
||||
|
||||
if not targetsize:
|
||||
logger.info('No track information for %s - %s. Defaulting to highest quality' % (album['ArtistName'], album['AlbumTitle']))
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
logger.info('No track information for %s - %s. Defaulting to highest quality' % (
|
||||
album['ArtistName'], album['AlbumTitle']))
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])),
|
||||
reverse=True)
|
||||
|
||||
else:
|
||||
newlist = []
|
||||
@@ -412,36 +419,43 @@ def sort_search_results(resultlist, album, new, albumlength):
|
||||
|
||||
# Add lossless results to the "flac list" which we can use if there are no good lossy matches
|
||||
if 'flac' in result[0].lower():
|
||||
flac_list.append((result[0], result[1], result[2], result[3], result[4], result[5]))
|
||||
flac_list.append(
|
||||
(result[0], result[1], result[2], result[3], result[4], result[5]))
|
||||
continue
|
||||
|
||||
delta = abs(targetsize - int(result[1]))
|
||||
newlist.append((result[0], result[1], result[2], result[3], result[4], result[5], delta))
|
||||
newlist.append(
|
||||
(result[0], result[1], result[2], result[3], result[4], result[5], delta))
|
||||
|
||||
finallist = sorted(newlist, key=lambda title: (-title[5], title[6]))
|
||||
|
||||
if not len(finallist) and len(flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS:
|
||||
logger.info("Since there were no appropriate lossy matches (and at least one lossless match), going to use lossless instead")
|
||||
finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
if not len(finallist) and len(
|
||||
flac_list) and headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS:
|
||||
logger.info(
|
||||
"Since there were no appropriate lossy matches (and at least one lossless match), going to use lossless instead")
|
||||
finallist = sorted(flac_list, key=lambda title: (title[5], int(title[1])),
|
||||
reverse=True)
|
||||
except Exception:
|
||||
logger.exception('Unhandled exception')
|
||||
logger.info('No track information for %s - %s. Defaulting to highest quality', album['ArtistName'], album['AlbumTitle'])
|
||||
logger.info('No track information for %s - %s. Defaulting to highest quality',
|
||||
album['ArtistName'], album['AlbumTitle'])
|
||||
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])),
|
||||
reverse=True)
|
||||
|
||||
else:
|
||||
|
||||
finallist = sorted(resultlist, key=lambda title: (title[5], int(title[1])), reverse=True)
|
||||
|
||||
if not len(finallist):
|
||||
logger.info('No appropriate matches found for %s - %s', album['ArtistName'], album['AlbumTitle'])
|
||||
logger.info('No appropriate matches found for %s - %s', album['ArtistName'],
|
||||
album['AlbumTitle'])
|
||||
return None
|
||||
|
||||
return finallist
|
||||
|
||||
|
||||
def get_year_from_release_date(release_date):
|
||||
|
||||
try:
|
||||
year = release_date[:4]
|
||||
except TypeError:
|
||||
@@ -450,11 +464,13 @@ def get_year_from_release_date(release_date):
|
||||
return year
|
||||
|
||||
|
||||
def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False):
|
||||
def searchNZB(album, new=False, losslessOnly=False, albumlength=None,
|
||||
choose_specific_download=False):
|
||||
reldate = album['ReleaseDate']
|
||||
year = get_year_from_release_date(reldate)
|
||||
|
||||
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': '', '*': '', '.': '', ':': ''}
|
||||
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': '',
|
||||
'*': '', '.': '', ':': ''}
|
||||
|
||||
cleanalbum = helpers.latinToAscii(helpers.replace_all(album['AlbumTitle'], dic)).strip()
|
||||
cleanartist = helpers.latinToAscii(helpers.replace_all(album['ArtistName'], dic)).strip()
|
||||
@@ -468,7 +484,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
# FLAC usually doesn't have a year for some reason so leave it out.
|
||||
# Various Artist albums might be listed as VA, so I'll leave that out too
|
||||
# Only use the year if the term could return a bunch of different albums, i.e. self-titled albums
|
||||
if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len(album['AlbumTitle']) < 4:
|
||||
if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len(
|
||||
album['AlbumTitle']) < 4:
|
||||
term = cleanartist + ' ' + cleanalbum + ' ' + year
|
||||
elif album['ArtistName'] == 'Various Artists':
|
||||
term = cleanalbum + ' ' + year
|
||||
@@ -542,7 +559,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
newznab_hosts = []
|
||||
|
||||
if headphones.CONFIG.NEWZNAB_HOST and headphones.CONFIG.NEWZNAB_ENABLED:
|
||||
newznab_hosts.append((headphones.CONFIG.NEWZNAB_HOST, headphones.CONFIG.NEWZNAB_APIKEY, headphones.CONFIG.NEWZNAB_ENABLED))
|
||||
newznab_hosts.append((headphones.CONFIG.NEWZNAB_HOST, headphones.CONFIG.NEWZNAB_APIKEY,
|
||||
headphones.CONFIG.NEWZNAB_ENABLED))
|
||||
|
||||
for newznab_host in headphones.CONFIG.get_extra_newznabs():
|
||||
if newznab_host[2] == '1' or newznab_host[2] == 1:
|
||||
@@ -575,7 +593,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
categories = categories + ",4050"
|
||||
|
||||
# Request results
|
||||
logger.info('Parsing results from %s using search term: %s' % (newznab_host[0],term))
|
||||
logger.info('Parsing results from %s using search term: %s' % (newznab_host[0], term))
|
||||
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
params = {
|
||||
@@ -602,13 +620,15 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
title = item.title
|
||||
size = int(item.links[1]['length'])
|
||||
if all(word.lower() in title.lower() for word in term.split()):
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
logger.info(
|
||||
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
resultlist.append((title, size, url, provider, 'nzb', True))
|
||||
else:
|
||||
logger.info('Skipping %s, not all search term words found' % title)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("An unknown error occurred trying to parse the feed: %s" % e)
|
||||
logger.exception(
|
||||
"An unknown error occurred trying to parse the feed: %s" % e)
|
||||
|
||||
if headphones.CONFIG.NZBSORG:
|
||||
provider = "nzbsorg"
|
||||
@@ -663,7 +683,7 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
categories = "22,7"
|
||||
else:
|
||||
categories = "7"
|
||||
categories = "7"
|
||||
|
||||
if album['Type'] == 'Other':
|
||||
categories = "29"
|
||||
@@ -708,7 +728,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
#
|
||||
# Also will filter flac & remix albums if not specifically looking for it
|
||||
# This code also checks the ignored words and required words
|
||||
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
|
||||
results = [result for result in resultlist if
|
||||
verifyresult(result[0], artistterm, term, losslessOnly)]
|
||||
|
||||
# Additional filtering for size etc
|
||||
if results and not choose_specific_download:
|
||||
@@ -718,8 +739,8 @@ def searchNZB(album, new=False, losslessOnly=False, albumlength=None, choose_spe
|
||||
|
||||
|
||||
def send_to_downloader(data, bestqual, album):
|
||||
|
||||
logger.info(u'Found best result from %s: <a href="%s">%s</a> - %s', bestqual[3], bestqual[2], bestqual[0], helpers.bytes_to_mb(bestqual[1]))
|
||||
logger.info(u'Found best result from %s: <a href="%s">%s</a> - %s', bestqual[3], bestqual[2],
|
||||
bestqual[0], helpers.bytes_to_mb(bestqual[1]))
|
||||
# Get rid of any dodgy chars here so we can prevent sab from renaming our downloads
|
||||
kind = bestqual[4]
|
||||
seed_ratio = None
|
||||
@@ -768,7 +789,10 @@ def send_to_downloader(data, bestqual, album):
|
||||
logger.error('Couldn\'t write NZB file: %s', e)
|
||||
return
|
||||
else:
|
||||
folder_name = '%s - %s [%s]' % (helpers.latinToAscii(album['ArtistName']).encode('UTF-8').replace('/', '_'), helpers.latinToAscii(album['AlbumTitle']).encode('UTF-8').replace('/', '_'), get_year_from_release_date(album['ReleaseDate']))
|
||||
folder_name = '%s - %s [%s]' % (
|
||||
helpers.latinToAscii(album['ArtistName']).encode('UTF-8').replace('/', '_'),
|
||||
helpers.latinToAscii(album['AlbumTitle']).encode('UTF-8').replace('/', '_'),
|
||||
get_year_from_release_date(album['ReleaseDate']))
|
||||
|
||||
# Blackhole
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 0:
|
||||
@@ -783,9 +807,11 @@ def send_to_downloader(data, bestqual, album):
|
||||
if headphones.SYS_PLATFORM == 'win32':
|
||||
os.startfile(bestqual[2])
|
||||
elif headphones.SYS_PLATFORM == 'darwin':
|
||||
subprocess.Popen(["open", bestqual[2]], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.Popen(["open", bestqual[2]], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", bestqual[2]], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.Popen(["xdg-open", bestqual[2]], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
# Gonna just take a guess at this..... Is there a better way to find this out?
|
||||
folder_name = bestqual[0]
|
||||
@@ -810,20 +836,20 @@ def send_to_downloader(data, bestqual, album):
|
||||
return
|
||||
# Extract folder name from torrent
|
||||
folder_name = read_torrent_name(download_path,
|
||||
bestqual[0])
|
||||
bestqual[0])
|
||||
|
||||
# Break for loop
|
||||
break
|
||||
else:
|
||||
# No service succeeded
|
||||
logger.warning("Unable to convert magnet with hash " \
|
||||
"'%s' into a torrent file.", torrent_hash)
|
||||
"'%s' into a torrent file.", torrent_hash)
|
||||
return
|
||||
else:
|
||||
logger.error("Cannot save magnet link in blackhole. " \
|
||||
"Please switch your torrent downloader to " \
|
||||
"Transmission or uTorrent, or allow Headphones " \
|
||||
"to open or convert magnet links")
|
||||
"Please switch your torrent downloader to " \
|
||||
"Transmission or uTorrent, or allow Headphones " \
|
||||
"to open or convert magnet links")
|
||||
return
|
||||
else:
|
||||
|
||||
@@ -860,7 +886,7 @@ def send_to_downloader(data, bestqual, album):
|
||||
if seed_ratio is not None:
|
||||
transmission.setSeedRatio(torrentid, seed_ratio)
|
||||
|
||||
else:# if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
else: # if headphones.CONFIG.TORRENT_DOWNLOADER == 2:
|
||||
logger.info("Sending torrent to uTorrent")
|
||||
|
||||
# Add torrent
|
||||
@@ -894,11 +920,16 @@ def send_to_downloader(data, bestqual, album):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, kind])
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name,
|
||||
kind])
|
||||
|
||||
# Store the torrent id so we can check later if it's finished seeding and can be removed
|
||||
if seed_ratio is not None and seed_ratio != 0 and torrentid:
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid, kind])
|
||||
myDB.action(
|
||||
'INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)',
|
||||
[album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid,
|
||||
kind])
|
||||
|
||||
# notify
|
||||
artist = album[1]
|
||||
@@ -959,11 +990,11 @@ def send_to_downloader(data, bestqual, album):
|
||||
message = 'Snatched from ' + provider + '. ' + name
|
||||
email.notify(title, message)
|
||||
|
||||
def verifyresult(title, artistterm, term, lossless):
|
||||
|
||||
def verifyresult(title, artistterm, term, lossless):
|
||||
title = re.sub('[\.\-\/\_]', ' ', title)
|
||||
|
||||
#if artistterm != 'Various Artists':
|
||||
# if artistterm != 'Various Artists':
|
||||
#
|
||||
# if not re.search('^' + re.escape(artistterm), title, re.IGNORECASE):
|
||||
# #logger.info("Removed from results: " + title + " (artist not at string start).")
|
||||
@@ -975,22 +1006,28 @@ def verifyresult(title, artistterm, term, lossless):
|
||||
# logger.info("Removed from results: " + title + " (pre substring result).")
|
||||
# return False
|
||||
|
||||
#another attempt to weed out substrings. We don't want "Vol III" when we were looking for "Vol II"
|
||||
# another attempt to weed out substrings. We don't want "Vol III" when we were looking for "Vol II"
|
||||
|
||||
# Filter out remix search results (if we're not looking for it)
|
||||
if 'remix' not in term.lower() and 'remix' in title.lower():
|
||||
logger.info("Removed %s from results because it's a remix album and we're not looking for a remix album right now.", title)
|
||||
logger.info(
|
||||
"Removed %s from results because it's a remix album and we're not looking for a remix album right now.",
|
||||
title)
|
||||
return False
|
||||
|
||||
# Filter out FLAC if we're not specifically looking for it
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == (0 or '0') and 'flac' in title.lower() and not lossless:
|
||||
logger.info("Removed %s from results because it's a lossless album and we're not looking for a lossless album right now.", title)
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == (
|
||||
0 or '0') and 'flac' in title.lower() and not lossless:
|
||||
logger.info(
|
||||
"Removed %s from results because it's a lossless album and we're not looking for a lossless album right now.",
|
||||
title)
|
||||
return False
|
||||
|
||||
if headphones.CONFIG.IGNORED_WORDS:
|
||||
for each_word in helpers.split_string(headphones.CONFIG.IGNORED_WORDS):
|
||||
if each_word.lower() in title.lower():
|
||||
logger.info("Removed '%s' from results because it contains ignored word: '%s'", title, each_word)
|
||||
logger.info("Removed '%s' from results because it contains ignored word: '%s'",
|
||||
title, each_word)
|
||||
return False
|
||||
|
||||
if headphones.CONFIG.REQUIRED_WORDS:
|
||||
@@ -1000,17 +1037,22 @@ def verifyresult(title, artistterm, term, lossless):
|
||||
if any(word.lower() in title.lower() for word in or_words):
|
||||
continue
|
||||
else:
|
||||
logger.info("Removed '%s' from results because it doesn't contain any of the required words in: '%s'", title, str(or_words))
|
||||
logger.info(
|
||||
"Removed '%s' from results because it doesn't contain any of the required words in: '%s'",
|
||||
title, str(or_words))
|
||||
return False
|
||||
if each_word.lower() not in title.lower():
|
||||
logger.info("Removed '%s' from results because it doesn't contain required word: '%s'", title, each_word)
|
||||
logger.info(
|
||||
"Removed '%s' from results because it doesn't contain required word: '%s'",
|
||||
title, each_word)
|
||||
return False
|
||||
|
||||
if headphones.CONFIG.IGNORE_CLEAN_RELEASES:
|
||||
for each_word in ['clean','edited','censored']:
|
||||
for each_word in ['clean', 'edited', 'censored']:
|
||||
logger.debug("Checking if '%s' is in search result: '%s'", each_word, title)
|
||||
if each_word.lower() in title.lower() and each_word.lower() not in term.lower():
|
||||
logger.info("Removed '%s' from results because it contains clean album word: '%s'", title, each_word)
|
||||
logger.info("Removed '%s' from results because it contains clean album word: '%s'",
|
||||
title, each_word)
|
||||
return False
|
||||
|
||||
tokens = re.split('\W', term, re.IGNORECASE | re.UNICODE)
|
||||
@@ -1022,27 +1064,31 @@ def verifyresult(title, artistterm, term, lossless):
|
||||
continue
|
||||
if not re.search('(?:\W|^)+' + token + '(?:\W|$)+', title, re.IGNORECASE | re.UNICODE):
|
||||
cleantoken = ''.join(c for c in token if c not in string.punctuation)
|
||||
if not not re.search('(?:\W|^)+' + cleantoken + '(?:\W|$)+', title, re.IGNORECASE | re.UNICODE):
|
||||
if not not re.search('(?:\W|^)+' + cleantoken + '(?:\W|$)+', title,
|
||||
re.IGNORECASE | re.UNICODE):
|
||||
dic = {'!': 'i', '$': 's'}
|
||||
dumbtoken = helpers.replace_all(token, dic)
|
||||
if not not re.search('(?:\W|^)+' + dumbtoken + '(?:\W|$)+', title, re.IGNORECASE | re.UNICODE):
|
||||
logger.info("Removed from results: %s (missing tokens: %s and %s)", title, token, cleantoken)
|
||||
if not not re.search('(?:\W|^)+' + dumbtoken + '(?:\W|$)+', title,
|
||||
re.IGNORECASE | re.UNICODE):
|
||||
logger.info("Removed from results: %s (missing tokens: %s and %s)", title,
|
||||
token, cleantoken)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose_specific_download=False):
|
||||
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||
choose_specific_download=False):
|
||||
global gazelle # persistent what.cd api object to reduce number of login attempts
|
||||
global ruobj # and rutracker
|
||||
global ruobj # and rutracker
|
||||
|
||||
albumid = album['AlbumID']
|
||||
reldate = album['ReleaseDate']
|
||||
|
||||
year = get_year_from_release_date(reldate)
|
||||
|
||||
# MERGE THIS WITH THE TERM CLEANUP FROM searchNZB
|
||||
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': ' ', '*': ''}
|
||||
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ', '"': '', ',': ' ',
|
||||
'*': ''}
|
||||
|
||||
semi_cleanalbum = helpers.replace_all(album['AlbumTitle'], dic)
|
||||
cleanalbum = helpers.latinToAscii(semi_cleanalbum)
|
||||
@@ -1059,7 +1105,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
# FLAC usually doesn't have a year for some reason so I'll leave it out
|
||||
# Various Artist albums might be listed as VA, so I'll leave that out too
|
||||
# Only use the year if the term could return a bunch of different albums, i.e. self-titled albums
|
||||
if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len(album['AlbumTitle']) < 4:
|
||||
if album['ArtistName'] in album['AlbumTitle'] or len(album['ArtistName']) < 4 or len(
|
||||
album['AlbumTitle']) < 4:
|
||||
term = cleanartist + ' ' + cleanalbum + ' ' + year
|
||||
elif album['ArtistName'] == 'Various Artists':
|
||||
term = cleanalbum + ' ' + year
|
||||
@@ -1103,7 +1150,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
torznab_hosts = []
|
||||
|
||||
if headphones.CONFIG.TORZNAB_HOST and headphones.CONFIG.TORZNAB_ENABLED:
|
||||
torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY, headphones.CONFIG.TORZNAB_ENABLED))
|
||||
torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY,
|
||||
headphones.CONFIG.TORZNAB_ENABLED))
|
||||
|
||||
for torznab_host in headphones.CONFIG.get_extra_torznabs():
|
||||
if torznab_host[2] == '1' or torznab_host[2] == 1:
|
||||
@@ -1125,7 +1173,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
provider = torznab_host[0]
|
||||
|
||||
# Request results
|
||||
logger.info('Parsing results from %s using search term: %s' % (torznab_host[0],term))
|
||||
logger.info('Parsing results from %s using search term: %s' % (torznab_host[0], term))
|
||||
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
params = {
|
||||
@@ -1152,13 +1200,15 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
title = item.title
|
||||
size = int(item.links[1]['length'])
|
||||
if all(word.lower() in title.lower() for word in term.split()):
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
logger.info(
|
||||
'Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
else:
|
||||
logger.info('Skipping %s, not all search term words found' % title)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("An unknown error occurred trying to parse the feed: %s" % e)
|
||||
logger.exception(
|
||||
"An unknown error occurred trying to parse the feed: %s" % e)
|
||||
|
||||
if headphones.CONFIG.KAT:
|
||||
provider = "Kick Ass Torrents"
|
||||
@@ -1185,7 +1235,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
providerurl += " category:music/"
|
||||
|
||||
# Requesting content
|
||||
logger.info("Searching %s using term: %s" % (provider,ka_term))
|
||||
logger.info("Searching %s using term: %s" % (provider, ka_term))
|
||||
|
||||
params = {
|
||||
"field": "seeders",
|
||||
@@ -1194,7 +1244,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
}
|
||||
|
||||
data = request.request_feed(url=providerurl, params=params,
|
||||
whitelist_status_code=[404])
|
||||
whitelist_status_code=[404])
|
||||
|
||||
# Process feed
|
||||
if data:
|
||||
@@ -1214,7 +1264,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %d)', title, size, int(seeders))
|
||||
logger.info(
|
||||
'%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %d)',
|
||||
title, size, int(seeders))
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception in the KAT parser")
|
||||
|
||||
@@ -1243,7 +1295,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
|
||||
query_items.extend(['format:(%s)' % format,
|
||||
'size:[0 TO %d]' % maxsize,
|
||||
'-seeders:0']) # cut out dead torrents
|
||||
'-seeders:0']) # cut out dead torrents
|
||||
|
||||
if bitrate:
|
||||
query_items.append('bitrate:"%s"' % bitrate)
|
||||
@@ -1256,8 +1308,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
"passkey": headphones.CONFIG.WAFFLES_PASSKEY,
|
||||
"rss": "1",
|
||||
"c0": "1",
|
||||
"s": "seeders", # sort by
|
||||
"d": "desc", # direction
|
||||
"s": "seeders", # sort by
|
||||
"d": "desc", # direction
|
||||
"q": " ".join(query_items)
|
||||
}
|
||||
|
||||
@@ -1281,7 +1333,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
logger.info('Found %s. Size: %s', title, helpers.bytes_to_mb(size))
|
||||
except Exception as e:
|
||||
logger.error(u"An error occurred while trying to parse the response from Waffles.fm: %s", e)
|
||||
logger.error(
|
||||
u"An error occurred while trying to parse the response from Waffles.fm: %s",
|
||||
e)
|
||||
|
||||
# rutracker.org
|
||||
if headphones.CONFIG.RUTRACKER:
|
||||
@@ -1341,7 +1395,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
if re.search(bitrate, encoding_string, flags=re.I):
|
||||
bitrate_string = encoding_string
|
||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||
logger.info(u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.")
|
||||
logger.info(
|
||||
u"Your preferred bitrate is not one of the available What.cd filters, so not using it as a search parameter.")
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||
@@ -1353,28 +1408,35 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
if not gazelle or not gazelle.logged_in():
|
||||
try:
|
||||
logger.info(u"Attempting to log in to What.cd...")
|
||||
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME, headphones.CONFIG.WHATCD_PASSWORD)
|
||||
gazelle = gazelleapi.GazelleAPI(headphones.CONFIG.WHATCD_USERNAME,
|
||||
headphones.CONFIG.WHATCD_PASSWORD)
|
||||
gazelle._login()
|
||||
except Exception as e:
|
||||
gazelle = None
|
||||
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (e.__class__.__name__, str(e)))
|
||||
logger.error(u"What.cd credentials incorrect or site is down. Error: %s %s" % (
|
||||
e.__class__.__name__, str(e)))
|
||||
|
||||
if gazelle and gazelle.logged_in():
|
||||
logger.info(u"Searching %s..." % provider)
|
||||
all_torrents = []
|
||||
for search_format in search_formats:
|
||||
if usersearchterm:
|
||||
all_torrents.extend(gazelle.search_torrents(searchstr=usersearchterm, format=search_format, encoding=bitrate_string)['results'])
|
||||
all_torrents.extend(
|
||||
gazelle.search_torrents(searchstr=usersearchterm, format=search_format,
|
||||
encoding=bitrate_string)['results'])
|
||||
else:
|
||||
all_torrents.extend(gazelle.search_torrents(artistname=semi_clean_artist_term,
|
||||
groupname=semi_clean_album_term,
|
||||
format=search_format, encoding=bitrate_string)['results'])
|
||||
groupname=semi_clean_album_term,
|
||||
format=search_format,
|
||||
encoding=bitrate_string)['results'])
|
||||
|
||||
# filter on format, size, and num seeders
|
||||
logger.info(u"Filtering torrents by format, maximum size, and minimum seeders...")
|
||||
match_torrents = [t for t in all_torrents if t.size <= maxsize and t.seeders >= minimumseeders]
|
||||
match_torrents = [t for t in all_torrents if
|
||||
t.size <= maxsize and t.seeders >= minimumseeders]
|
||||
|
||||
logger.info(u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
logger.info(
|
||||
u"Remaining torrents: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
|
||||
# sort by times d/l'd
|
||||
if not len(match_torrents):
|
||||
@@ -1382,21 +1444,25 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
elif len(match_torrents) > 1:
|
||||
logger.info(u"Found %d matching releases from %s for %s - %s after filtering" %
|
||||
(len(match_torrents), provider, artistterm, albumterm))
|
||||
logger.info("Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string)
|
||||
logger.info(
|
||||
"Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string)
|
||||
match_torrents.sort(key=lambda x: int(x.snatched), reverse=True)
|
||||
if gazelleformat.MP3 in search_formats:
|
||||
# sort by size after rounding to nearest 10MB...hacky, but will favor highest quality
|
||||
match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)), reverse=True)
|
||||
match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)),
|
||||
reverse=True)
|
||||
if search_formats and None not in search_formats:
|
||||
match_torrents.sort(key=lambda x: int(search_formats.index(x.format))) # prefer lossless
|
||||
# if bitrate:
|
||||
# match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True)
|
||||
# match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True)
|
||||
logger.info(u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
match_torrents.sort(
|
||||
key=lambda x: int(search_formats.index(x.format))) # prefer lossless
|
||||
# if bitrate:
|
||||
# match_torrents.sort(key=lambda x: re.match("mp3", x.getTorrentDetails(), flags=re.I), reverse=True)
|
||||
# match_torrents.sort(key=lambda x: str(bitrate) in x.getTorrentFolderName(), reverse=True)
|
||||
logger.info(
|
||||
u"New order: %s" % ", ".join(repr(torrent) for torrent in match_torrents))
|
||||
|
||||
for torrent in match_torrents:
|
||||
if not torrent.file_path:
|
||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||
resultlist.append((torrent.file_path,
|
||||
torrent.size,
|
||||
gazelle.generate_torrent_link(torrent.id),
|
||||
@@ -1415,23 +1481,24 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
providerurl = fix_url("https://thepiratebay.se")
|
||||
|
||||
# Build URL
|
||||
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
|
||||
providerurl = providerurl + "/search/" + tpb_term + "/0/7/" # 7 is sort by seeders
|
||||
|
||||
# Pick category for torrents
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
category = '104' # FLAC
|
||||
category = '104' # FLAC
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
category = '100' # General audio category
|
||||
category = '100' # General audio category
|
||||
maxsize = 10000000000
|
||||
else:
|
||||
category = '101' # MP3 only
|
||||
category = '101' # MP3 only
|
||||
maxsize = 300000000
|
||||
|
||||
# Request content
|
||||
logger.info("Searching The Pirate Bay using term: %s", tpb_term)
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
data = request.request_soup(url=providerurl + category, headers=headers)
|
||||
|
||||
# Process content
|
||||
@@ -1463,7 +1530,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
if url.lower().startswith("//"):
|
||||
url = "http:" + url
|
||||
|
||||
formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace(u'\xa0', ' ')
|
||||
formatted_size = re.search('Size (.*),', unicode(item)).group(1).replace(
|
||||
u'\xa0', ' ')
|
||||
size = helpers.piratesize(formatted_size)
|
||||
|
||||
if size < maxsize and minimumseeders < seeds and url is not None:
|
||||
@@ -1471,7 +1539,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
logger.info('Found %s. Size: %s' % (title, formatted_size))
|
||||
else:
|
||||
match = False
|
||||
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (title, size, int(seeds)))
|
||||
logger.info(
|
||||
'%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (
|
||||
title, size, int(seeds)))
|
||||
|
||||
resultlist.append((title, size, url, provider, "torrent", match))
|
||||
except Exception as e:
|
||||
@@ -1493,9 +1563,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
# Requesting content
|
||||
logger.info("Parsing results from Old Pirate Bay")
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'}
|
||||
provider_url = fix_url(headphones.CONFIG.OLDPIRATEBAY_URL) + \
|
||||
"/search.php?" + urllib.urlencode({"q": tpb_term, "iht": 6})
|
||||
"/search.php?" + urllib.urlencode({"q": tpb_term, "iht": 6})
|
||||
|
||||
data = request.request_soup(url=provider_url, headers=headers)
|
||||
|
||||
@@ -1513,7 +1584,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
rightformat = True
|
||||
title = links[1].text
|
||||
seeds = int(item.select("td.seeders-row")[0].text)
|
||||
url = links[0]["href"] # Magnet link. The actual download link is not based on the URL
|
||||
url = links[0][
|
||||
"href"] # Magnet link. The actual download link is not based on the URL
|
||||
|
||||
formatted_size = item.select("td.size-row")[0].text
|
||||
size = helpers.piratesize(formatted_size)
|
||||
@@ -1523,11 +1595,14 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
logger.info('Found %s. Size: %s' % (title, formatted_size))
|
||||
else:
|
||||
match = False
|
||||
logger.info('%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (title, size, int(seeds)))
|
||||
logger.info(
|
||||
'%s is larger than the maxsize or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i)' % (
|
||||
title, size, int(seeds)))
|
||||
|
||||
resultlist.append((title, size, url, provider, "torrent", match))
|
||||
except Exception as e:
|
||||
logger.error(u"An unknown error occurred in the Old Pirate Bay parser: %s" % e)
|
||||
logger.error(
|
||||
u"An unknown error occurred in the Old Pirate Bay parser: %s" % e)
|
||||
|
||||
# Strike
|
||||
if headphones.CONFIG.STRIKE:
|
||||
@@ -1547,7 +1622,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
|
||||
logger.info("Searching %s using term: %s" % (provider, s_term))
|
||||
data = request.request_json(url=providerurl,
|
||||
whitelist_status_code=[404])
|
||||
whitelist_status_code=[404])
|
||||
|
||||
if not data or not data.get('torrents'):
|
||||
logger.info("No results found on %s using search term: %s" % (provider, s_term))
|
||||
@@ -1558,7 +1633,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
seeders = item['seeds']
|
||||
url = item['magnet_uri']
|
||||
size = int(item['size'])
|
||||
subcategory = item['sub_category']
|
||||
|
||||
if size < maxsize and minimumseeders < int(seeders):
|
||||
resultlist.append((title, size, url, provider, 'torrent', True))
|
||||
@@ -1577,15 +1651,15 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
|
||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
# categories = "7" #music
|
||||
format = "2" #flac
|
||||
format = "2" # flac
|
||||
maxsize = 10000000000
|
||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless:
|
||||
# categories = "7" #music
|
||||
format = "10" #mp3+flac
|
||||
format = "10" # mp3+flac
|
||||
maxsize = 10000000000
|
||||
else:
|
||||
# categories = "7" #music
|
||||
format = "8" #mp3
|
||||
format = "8" # mp3
|
||||
maxsize = 300000000
|
||||
|
||||
# Requesting content
|
||||
@@ -1614,7 +1688,8 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
size = int(item.links[1]['length'])
|
||||
if format == "2":
|
||||
torrent = request.request_content(url)
|
||||
if not torrent or (int(torrent.find(".mp3")) > 0 and int(torrent.find(".flac")) < 1):
|
||||
if not torrent or (int(torrent.find(".mp3")) > 0 and int(
|
||||
torrent.find(".flac")) < 1):
|
||||
rightformat = False
|
||||
|
||||
if rightformat and size < maxsize and minimumseeders < seeds:
|
||||
@@ -1622,16 +1697,19 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
match = False
|
||||
logger.info('%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (title, size, int(seeds), rightformat))
|
||||
logger.info(
|
||||
'%s is larger than the maxsize, the wrong format or has too little seeders for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (
|
||||
title, size, int(seeds), rightformat))
|
||||
|
||||
resultlist.append((title, size, url, provider, 'torrent', match))
|
||||
except Exception as e:
|
||||
logger.exception("Unhandled exception in Mininova Parser")
|
||||
|
||||
#attempt to verify that this isn't a substring result
|
||||
#when looking for "Foo - Foo" we don't want "Foobar"
|
||||
#this should be less of an issue when it isn't a self-titled album so we'll only check vs artist
|
||||
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
|
||||
# attempt to verify that this isn't a substring result
|
||||
# when looking for "Foo - Foo" we don't want "Foobar"
|
||||
# this should be less of an issue when it isn't a self-titled album so we'll only check vs artist
|
||||
results = [result for result in resultlist if
|
||||
verifyresult(result[0], artistterm, term, losslessOnly)]
|
||||
|
||||
# Additional filtering for size etc
|
||||
if results and not choose_specific_download:
|
||||
@@ -1639,11 +1717,11 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None, choose
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# THIS IS KIND OF A MESS AND PROBABLY NEEDS TO BE CLEANED UP
|
||||
|
||||
|
||||
def preprocess(resultlist):
|
||||
|
||||
for result in resultlist:
|
||||
if result[4] == 'torrent':
|
||||
|
||||
@@ -1651,7 +1729,7 @@ def preprocess(resultlist):
|
||||
if result[3] == 'rutracker.org':
|
||||
return ruobj.get_torrent_data(result[2]), result
|
||||
|
||||
#Get out of here if we're using Transmission
|
||||
# Get out of here if we're using Transmission
|
||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1: ## if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
|
||||
return True, result
|
||||
# Get out of here if it's a magnet link
|
||||
@@ -1667,13 +1745,15 @@ def preprocess(resultlist):
|
||||
elif result[3] == 'What.cd':
|
||||
headers['User-Agent'] = 'Headphones'
|
||||
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
||||
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||
headers[
|
||||
'User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||
return request.request_content(url=result[2], headers=headers), result
|
||||
|
||||
else:
|
||||
headers = {'User-Agent': USER_AGENT}
|
||||
|
||||
if result[3] == 'headphones':
|
||||
return request.request_content(url=result[2], headers=headers, auth=(headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)), result
|
||||
return request.request_content(url=result[2], headers=headers, auth=(
|
||||
headphones.CONFIG.HPUSER, headphones.CONFIG.HPPASS)), result
|
||||
else:
|
||||
return request.request_content(url=result[2], headers=headers), result
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import db, utorrent, transmission, logger
|
||||
|
||||
import threading
|
||||
|
||||
from headphones import db, utorrent, transmission, logger
|
||||
import headphones
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
|
||||
|
||||
def checkTorrentFinished():
|
||||
"""
|
||||
Remove Torrent + data if Post Processed and finished Seeding
|
||||
@@ -41,6 +42,7 @@ def checkTorrentFinished():
|
||||
torrent_removed = utorrent.removeTorrent(hash, True)
|
||||
|
||||
if torrent_removed:
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?', [albumid])
|
||||
myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?',
|
||||
[albumid])
|
||||
|
||||
logger.info("Checking finished torrents completed")
|
||||
|
||||
@@ -13,14 +13,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from headphones import logger, request
|
||||
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import urlparse
|
||||
|
||||
from headphones import logger, request
|
||||
import headphones
|
||||
|
||||
|
||||
# This is just a simple script to send torrents to transmission. The
|
||||
# intention is to turn this into a class where we can check the state
|
||||
# of the download, set the download dir, etc.
|
||||
@@ -96,7 +97,6 @@ def setSeedRatio(torrentid, ratio):
|
||||
|
||||
|
||||
def removeTorrent(torrentid, remove_data=False):
|
||||
|
||||
method = 'torrent-get'
|
||||
arguments = {'ids': torrentid, 'fields': ['isFinished', 'name']}
|
||||
|
||||
@@ -118,7 +118,8 @@ def removeTorrent(torrentid, remove_data=False):
|
||||
response = torrentAction(method, arguments)
|
||||
return True
|
||||
else:
|
||||
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % name)
|
||||
logger.info(
|
||||
'%s has not finished seeding yet, torrent will not be removed, will try again on next run' % name)
|
||||
except:
|
||||
return False
|
||||
|
||||
@@ -126,7 +127,6 @@ def removeTorrent(torrentid, remove_data=False):
|
||||
|
||||
|
||||
def torrentAction(method, arguments):
|
||||
|
||||
host = headphones.CONFIG.TRANSMISSION_HOST
|
||||
username = headphones.CONFIG.TRANSMISSION_USERNAME
|
||||
password = headphones.CONFIG.TRANSMISSION_PASSWORD
|
||||
@@ -152,7 +152,7 @@ def torrentAction(method, arguments):
|
||||
# Retrieve session id
|
||||
auth = (username, password) if username and password else None
|
||||
response = request.request_response(host, auth=auth,
|
||||
whitelist_status_code=[401, 409])
|
||||
whitelist_status_code=[401, 409])
|
||||
|
||||
if response is None:
|
||||
logger.error("Error gettings Transmission session ID")
|
||||
@@ -162,7 +162,7 @@ def torrentAction(method, arguments):
|
||||
if response.status_code == 401:
|
||||
if auth:
|
||||
logger.error("Username and/or password not accepted by " \
|
||||
"Transmission")
|
||||
"Transmission")
|
||||
else:
|
||||
logger.error("Transmission authorization required")
|
||||
|
||||
@@ -179,7 +179,7 @@ def torrentAction(method, arguments):
|
||||
data = {'method': method, 'arguments': arguments}
|
||||
|
||||
response = request.request_json(host, method="POST", data=json.dumps(data),
|
||||
headers=headers, auth=auth)
|
||||
headers=headers, auth=auth)
|
||||
|
||||
print response
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ from headphones import logger, db, importer
|
||||
|
||||
|
||||
def dbUpdate(forcefull=False):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
active_artists = myDB.select('SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC')
|
||||
active_artists = myDB.select(
|
||||
'SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC')
|
||||
logger.info('Starting update for %i active artists', len(active_artists))
|
||||
|
||||
for artist in active_artists:
|
||||
|
||||
@@ -14,26 +14,24 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import urllib
|
||||
import json
|
||||
import time
|
||||
from collections import namedtuple
|
||||
import urllib2
|
||||
import urlparse
|
||||
import cookielib
|
||||
import json
|
||||
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class utorrentclient(object):
|
||||
|
||||
TOKEN_REGEX = "<div id='token' style='display:none;'>([^<>]+)</div>"
|
||||
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
|
||||
|
||||
def __init__(self, base_url=None, username=None, password=None,):
|
||||
def __init__(self, base_url=None, username=None, password=None, ):
|
||||
|
||||
host = headphones.CONFIG.UTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
@@ -50,7 +48,7 @@ class utorrentclient(object):
|
||||
self.password = headphones.CONFIG.UTORRENT_PASSWORD
|
||||
self.opener = self._make_opener('uTorrent', self.base_url, self.username, self.password)
|
||||
self.token = self._get_token()
|
||||
#TODO refresh token, when necessary
|
||||
# TODO refresh token, when necessary
|
||||
|
||||
def _make_opener(self, realm, base_url, username, password):
|
||||
"""uTorrent API need HTTP Basic Auth and cookie support for token verify."""
|
||||
@@ -83,7 +81,7 @@ class utorrentclient(object):
|
||||
return self._action(params)
|
||||
|
||||
def add_url(self, url):
|
||||
#can receive magnet or normal .torrent link
|
||||
# can receive magnet or normal .torrent link
|
||||
params = [('action', 'add-url'), ('s', url)]
|
||||
return self._action(params)
|
||||
|
||||
@@ -187,7 +185,9 @@ def removeTorrent(hash, remove_data=False):
|
||||
uTorrentClient.remove(hash, remove_data)
|
||||
return True
|
||||
else:
|
||||
logger.info('%s has not finished seeding yet, torrent will not be removed, will try again on next run' % torrent[2])
|
||||
logger.info(
|
||||
'%s has not finished seeding yet, torrent will not be removed, will try again on next run' %
|
||||
torrent[2])
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -203,7 +203,6 @@ def setSeedRatio(hash, ratio):
|
||||
|
||||
|
||||
def dirTorrent(hash, cacheid=None, return_name=None):
|
||||
|
||||
uTorrentClient = utorrentclient()
|
||||
|
||||
if not cacheid:
|
||||
@@ -228,19 +227,20 @@ def dirTorrent(hash, cacheid=None, return_name=None):
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def addTorrent(link):
|
||||
uTorrentClient = utorrentclient()
|
||||
uTorrentClient.add_url(link)
|
||||
|
||||
|
||||
def getFolder(hash):
|
||||
uTorrentClient = utorrentclient()
|
||||
|
||||
# Get Active Directory from settings
|
||||
active_dir, completed_dir = getSettingsDirectories()
|
||||
|
||||
if not active_dir:
|
||||
logger.error('Could not get "Put new downloads in:" directory from uTorrent settings, please ensure it is set')
|
||||
logger.error(
|
||||
'Could not get "Put new downloads in:" directory from uTorrent settings, please ensure it is set')
|
||||
return None
|
||||
|
||||
# Get Torrent Folder Name
|
||||
|
||||
@@ -13,18 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import os
|
||||
import tarfile
|
||||
import platform
|
||||
import headphones
|
||||
import subprocess
|
||||
|
||||
import re
|
||||
import os
|
||||
import headphones
|
||||
from headphones import logger, version, request
|
||||
|
||||
|
||||
def runGit(args):
|
||||
|
||||
if headphones.CONFIG.GIT_PATH:
|
||||
git_locations = ['"' + headphones.CONFIG.GIT_PATH + '"']
|
||||
else:
|
||||
@@ -40,7 +39,8 @@ def runGit(args):
|
||||
|
||||
try:
|
||||
logger.debug('Trying to execute: "' + cmd + '" with shell in ' + headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=headphones.PROG_DIR)
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True,
|
||||
cwd=headphones.PROG_DIR)
|
||||
output, err = p.communicate()
|
||||
output = output.strip()
|
||||
|
||||
@@ -62,7 +62,6 @@ def runGit(args):
|
||||
|
||||
|
||||
def getVersion():
|
||||
|
||||
if version.HEADPHONES_VERSION.startswith('win32build'):
|
||||
headphones.INSTALL_TYPE = 'win'
|
||||
|
||||
@@ -92,7 +91,8 @@ def getVersion():
|
||||
branch_name = branch_name
|
||||
|
||||
if not branch_name and headphones.CONFIG.GIT_BRANCH:
|
||||
logger.error('Could not retrieve branch name from git. Falling back to %s' % headphones.CONFIG.GIT_BRANCH)
|
||||
logger.error(
|
||||
'Could not retrieve branch name from git. Falling back to %s' % headphones.CONFIG.GIT_BRANCH)
|
||||
branch_name = headphones.CONFIG.GIT_BRANCH
|
||||
if not branch_name:
|
||||
logger.error('Could not retrieve branch name from git. Defaulting to master')
|
||||
@@ -123,11 +123,13 @@ def checkGithub():
|
||||
|
||||
# Get the latest version available from github
|
||||
logger.info('Retrieving latest version information from GitHub')
|
||||
url = 'https://api.github.com/repos/%s/headphones/commits/%s' % (headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
url = 'https://api.github.com/repos/%s/headphones/commits/%s' % (
|
||||
headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict)
|
||||
|
||||
if version is None:
|
||||
logger.warn('Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
logger.warn(
|
||||
'Could not get the latest version from GitHub. Are you running a local development version?')
|
||||
return headphones.CURRENT_VERSION
|
||||
|
||||
headphones.LATEST_VERSION = version['sha']
|
||||
@@ -135,7 +137,8 @@ def checkGithub():
|
||||
|
||||
# See how many commits behind we are
|
||||
if not headphones.CURRENT_VERSION:
|
||||
logger.info('You are running an unknown version of Headphones. Run the updater to identify your version')
|
||||
logger.info(
|
||||
'You are running an unknown version of Headphones. Run the updater to identify your version')
|
||||
return headphones.LATEST_VERSION
|
||||
|
||||
if headphones.LATEST_VERSION == headphones.CURRENT_VERSION:
|
||||
@@ -143,8 +146,10 @@ def checkGithub():
|
||||
return headphones.LATEST_VERSION
|
||||
|
||||
logger.info('Comparing currently installed version with latest GitHub version')
|
||||
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||
url = 'https://api.github.com/repos/%s/headphones/compare/%s...%s' % (
|
||||
headphones.CONFIG.GIT_USER, headphones.LATEST_VERSION, headphones.CURRENT_VERSION)
|
||||
commits = request.request_json(url, timeout=20, whitelist_status_code=404,
|
||||
validator=lambda x: type(x) == dict)
|
||||
|
||||
if commits is None:
|
||||
logger.warn('Could not get commits behind from GitHub.')
|
||||
@@ -158,7 +163,8 @@ def checkGithub():
|
||||
headphones.COMMITS_BEHIND = 0
|
||||
|
||||
if headphones.COMMITS_BEHIND > 0:
|
||||
logger.info('New version is available. You are %s commits behind' % headphones.COMMITS_BEHIND)
|
||||
logger.info(
|
||||
'New version is available. You are %s commits behind' % headphones.COMMITS_BEHIND)
|
||||
elif headphones.COMMITS_BEHIND == 0:
|
||||
logger.info('Headphones is up to date')
|
||||
|
||||
@@ -185,7 +191,8 @@ def update():
|
||||
logger.info('Output: ' + str(output))
|
||||
|
||||
else:
|
||||
tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % (headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
tar_download_url = 'https://github.com/%s/headphones/tarball/%s' % (
|
||||
headphones.CONFIG.GIT_USER, headphones.CONFIG.GIT_BRANCH)
|
||||
update_dir = os.path.join(headphones.PROG_DIR, 'update')
|
||||
version_path = os.path.join(headphones.PROG_DIR, 'version.txt')
|
||||
|
||||
@@ -214,7 +221,8 @@ def update():
|
||||
os.remove(tar_download_path)
|
||||
|
||||
# Find update dir name
|
||||
update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))]
|
||||
update_dir_contents = [x for x in os.listdir(update_dir) if
|
||||
os.path.isdir(os.path.join(update_dir, x))]
|
||||
if len(update_dir_contents) != 1:
|
||||
logger.error("Invalid update data, update failed: " + str(update_dir_contents))
|
||||
return
|
||||
|
||||
@@ -15,18 +15,8 @@
|
||||
|
||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||
|
||||
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers
|
||||
from headphones.helpers import checked, radio, today, cleanName
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
import headphones
|
||||
import threading
|
||||
import cherrypy
|
||||
import urllib2
|
||||
import hashlib
|
||||
import random
|
||||
import urllib
|
||||
@@ -34,8 +24,16 @@ import json
|
||||
import time
|
||||
import cgi
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
import os
|
||||
import re
|
||||
from headphones import logger, searcher, db, importer, mb, lastfm, librarysync, helpers, notifiers
|
||||
from headphones.helpers import checked, radio, today, cleanName
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
import headphones
|
||||
import cherrypy
|
||||
|
||||
try:
|
||||
# pylint:disable=E0611
|
||||
@@ -48,7 +46,6 @@ except ImportError:
|
||||
|
||||
|
||||
def serve_template(templatename, **kwargs):
|
||||
|
||||
interface_dir = os.path.join(str(headphones.PROG_DIR), 'data/interfaces/')
|
||||
template_dir = os.path.join(str(interface_dir), headphones.CONFIG.INTERFACE)
|
||||
|
||||
@@ -62,7 +59,6 @@ def serve_template(templatename, **kwargs):
|
||||
|
||||
|
||||
class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
@@ -90,7 +86,8 @@ class WebInterface(object):
|
||||
if not artist:
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
albums = myDB.select('SELECT * from albums WHERE ArtistID=? order by ReleaseDate DESC', [ArtistID])
|
||||
albums = myDB.select('SELECT * from albums WHERE ArtistID=? order by ReleaseDate DESC',
|
||||
[ArtistID])
|
||||
|
||||
# Serve the extras up as a dict to make things easier for new templates (append new extras to the end)
|
||||
extras_list = headphones.POSSIBLE_EXTRAS
|
||||
@@ -109,7 +106,8 @@ class WebInterface(object):
|
||||
extras_dict[extra] = ""
|
||||
i += 1
|
||||
|
||||
return serve_template(templatename="artist.html", title=artist['ArtistName'], artist=artist, albums=albums, extras=extras_dict)
|
||||
return serve_template(templatename="artist.html", title=artist['ArtistName'], artist=artist,
|
||||
albums=albums, extras=extras_dict)
|
||||
|
||||
@cherrypy.expose
|
||||
def albumPage(self, AlbumID):
|
||||
@@ -128,8 +126,10 @@ class WebInterface(object):
|
||||
if not album:
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=? ORDER BY CAST(TrackNumber AS INTEGER)', [AlbumID])
|
||||
description = myDB.action('SELECT * from descriptions WHERE ReleaseGroupID=?', [AlbumID]).fetchone()
|
||||
tracks = myDB.select(
|
||||
'SELECT * from tracks WHERE AlbumID=? ORDER BY CAST(TrackNumber AS INTEGER)', [AlbumID])
|
||||
description = myDB.action('SELECT * from descriptions WHERE ReleaseGroupID=?',
|
||||
[AlbumID]).fetchone()
|
||||
|
||||
if not album['ArtistName']:
|
||||
title = ' - '
|
||||
@@ -139,7 +139,8 @@ class WebInterface(object):
|
||||
title = title + ""
|
||||
else:
|
||||
title = title + album['AlbumTitle']
|
||||
return serve_template(templatename="album.html", title=title, album=album, tracks=tracks, description=description)
|
||||
return serve_template(templatename="album.html", title=title, album=album, tracks=tracks,
|
||||
description=description)
|
||||
|
||||
@cherrypy.expose
|
||||
def search(self, name, type):
|
||||
@@ -151,7 +152,9 @@ class WebInterface(object):
|
||||
searchresults = mb.findRelease(name, limit=100)
|
||||
else:
|
||||
searchresults = mb.findSeries(name, limit=100)
|
||||
return serve_template(templatename="searchresults.html", title='Search Results for: "' + cgi.escape(name) + '"', searchresults=searchresults, name=cgi.escape(name), type=type)
|
||||
return serve_template(templatename="searchresults.html",
|
||||
title='Search Results for: "' + cgi.escape(name) + '"',
|
||||
searchresults=searchresults, name=cgi.escape(name), type=type)
|
||||
|
||||
@cherrypy.expose
|
||||
def addArtist(self, artistid):
|
||||
@@ -162,7 +165,8 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def addSeries(self, seriesid):
|
||||
thread = threading.Thread(target=importer.addArtisttoDB, args=[seriesid, False, False, "series"])
|
||||
thread = threading.Thread(target=importer.addArtisttoDB,
|
||||
args=[seriesid, False, False, "series"])
|
||||
thread.start()
|
||||
thread.join(1)
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % seriesid)
|
||||
@@ -200,12 +204,18 @@ class WebInterface(object):
|
||||
controlValueDict = {'ArtistID': ArtistID}
|
||||
newValueDict = {'IncludeExtras': 0}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
extraalbums = myDB.select('SELECT AlbumID from albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"', [ArtistID])
|
||||
extraalbums = myDB.select(
|
||||
'SELECT AlbumID from albums WHERE ArtistID=? AND Status="Skipped" AND Type!="Album"',
|
||||
[ArtistID])
|
||||
for album in extraalbums:
|
||||
myDB.action('DELETE from tracks WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from albums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from allalbums WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from alltracks WHERE ArtistID=? AND AlbumID=?', [ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from tracks WHERE ArtistID=? AND AlbumID=?',
|
||||
[ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from albums WHERE ArtistID=? AND AlbumID=?',
|
||||
[ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from allalbums WHERE ArtistID=? AND AlbumID=?',
|
||||
[ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from alltracks WHERE ArtistID=? AND AlbumID=?',
|
||||
[ArtistID, album['AlbumID']])
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [album['AlbumID']])
|
||||
from headphones import cache
|
||||
c = cache.Cache()
|
||||
@@ -242,7 +252,9 @@ class WebInterface(object):
|
||||
from headphones import cache
|
||||
c = cache.Cache()
|
||||
|
||||
rgids = myDB.select('SELECT AlbumID FROM albums WHERE ArtistID=? UNION SELECT AlbumID FROM allalbums WHERE ArtistID=?', [ArtistID, ArtistID])
|
||||
rgids = myDB.select(
|
||||
'SELECT AlbumID FROM albums WHERE ArtistID=? UNION SELECT AlbumID FROM allalbums WHERE ArtistID=?',
|
||||
[ArtistID, ArtistID])
|
||||
for rgid in rgids:
|
||||
albumid = rgid['AlbumID']
|
||||
myDB.action('DELETE from releases WHERE ReleaseGroupID=?', [albumid])
|
||||
@@ -269,17 +281,19 @@ class WebInterface(object):
|
||||
def scanArtist(self, ArtistID):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
artist_name = myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0]
|
||||
artist_name = \
|
||||
myDB.select('SELECT DISTINCT ArtistName FROM artists WHERE ArtistID=?', [ArtistID])[0][0]
|
||||
|
||||
logger.info(u"Scanning artist: %s", artist_name)
|
||||
|
||||
full_folder_format = headphones.CONFIG.FOLDER_FORMAT
|
||||
folder_format = re.findall(r'(.*?[Aa]rtist?)\.*', full_folder_format)[0]
|
||||
|
||||
acceptable_formats = ["$artist","$sortartist","$first/$artist","$first/$sortartist"]
|
||||
acceptable_formats = ["$artist", "$sortartist", "$first/$artist", "$first/$sortartist"]
|
||||
|
||||
if not folder_format.lower() in acceptable_formats:
|
||||
logger.info("Can't determine the artist folder from the configured folder_format. Not scanning")
|
||||
logger.info(
|
||||
"Can't determine the artist folder from the configured folder_format. Not scanning")
|
||||
return
|
||||
|
||||
# Format the folder to match the settings
|
||||
@@ -299,12 +313,12 @@ class WebInterface(object):
|
||||
firstchar = sortname[0]
|
||||
|
||||
values = {'$Artist': artist,
|
||||
'$SortArtist': sortname,
|
||||
'$First': firstchar.upper(),
|
||||
'$artist': artist.lower(),
|
||||
'$sortartist': sortname.lower(),
|
||||
'$first': firstchar.lower(),
|
||||
}
|
||||
'$SortArtist': sortname,
|
||||
'$First': firstchar.upper(),
|
||||
'$artist': artist.lower(),
|
||||
'$sortartist': sortname.lower(),
|
||||
'$first': firstchar.lower(),
|
||||
}
|
||||
|
||||
folder = helpers.replace_all(folder_format.strip(), values, normalize=True)
|
||||
|
||||
@@ -332,14 +346,17 @@ class WebInterface(object):
|
||||
if not os.path.isdir(artistfolder):
|
||||
logger.debug("Cannot find directory: " + artistfolder)
|
||||
continue
|
||||
threading.Thread(target=librarysync.libraryScan, kwargs={"dir":artistfolder, "artistScan":True, "ArtistID":ArtistID, "ArtistName":artist_name}).start()
|
||||
threading.Thread(target=librarysync.libraryScan,
|
||||
kwargs={"dir": artistfolder, "artistScan": True, "ArtistID": ArtistID,
|
||||
"ArtistName": artist_name}).start()
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||
|
||||
@cherrypy.expose
|
||||
def deleteEmptyArtists(self):
|
||||
logger.info(u"Deleting all empty artists")
|
||||
myDB = db.DBConnection()
|
||||
emptyArtistIDs = [row['ArtistID'] for row in myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum IS NULL")]
|
||||
emptyArtistIDs = [row['ArtistID'] for row in
|
||||
myDB.select("SELECT ArtistID FROM artists WHERE LatestAlbum IS NULL")]
|
||||
for ArtistID in emptyArtistIDs:
|
||||
self.removeArtist(ArtistID)
|
||||
|
||||
@@ -371,8 +388,11 @@ class WebInterface(object):
|
||||
if ArtistID:
|
||||
ArtistIDT = ArtistID
|
||||
else:
|
||||
ArtistIDT = myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0]
|
||||
myDB.action('UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?', [ArtistIDT, ArtistIDT])
|
||||
ArtistIDT = \
|
||||
myDB.action('SELECT ArtistID FROM albums WHERE AlbumID=?', [mbid]).fetchone()[0]
|
||||
myDB.action(
|
||||
'UPDATE artists SET TotalTracks=(SELECT COUNT(*) FROM tracks WHERE ArtistID = ? AND AlbumTitle IN (SELECT AlbumTitle FROM albums WHERE Status != "Ignored")) WHERE ArtistID = ?',
|
||||
[ArtistIDT, ArtistIDT])
|
||||
if ArtistID:
|
||||
raise cherrypy.HTTPRedirect("artistPage?ArtistID=%s" % ArtistID)
|
||||
else:
|
||||
@@ -385,8 +405,10 @@ class WebInterface(object):
|
||||
if action == "ignore":
|
||||
myDB = db.DBConnection()
|
||||
for artist in args:
|
||||
myDB.action('DELETE FROM newartists WHERE ArtistName=?', [artist.decode(headphones.SYS_ENCODING, 'replace')])
|
||||
myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=?', [artist.decode(headphones.SYS_ENCODING, 'replace')])
|
||||
myDB.action('DELETE FROM newartists WHERE ArtistName=?',
|
||||
[artist.decode(headphones.SYS_ENCODING, 'replace')])
|
||||
myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=?',
|
||||
[artist.decode(headphones.SYS_ENCODING, 'replace')])
|
||||
logger.info("Artist %s removed from new artist list and set to ignored" % artist)
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
@@ -440,12 +462,12 @@ class WebInterface(object):
|
||||
(data, bestqual) = searcher.preprocess(result)
|
||||
|
||||
if data and bestqual:
|
||||
myDB = db.DBConnection()
|
||||
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
|
||||
searcher.send_to_downloader(data, bestqual, album)
|
||||
return json.dumps({'result':'success'})
|
||||
myDB = db.DBConnection()
|
||||
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
|
||||
searcher.send_to_downloader(data, bestqual, album)
|
||||
return json.dumps({'result': 'success'})
|
||||
else:
|
||||
return json.dumps({'result':'failure'})
|
||||
return json.dumps({'result': 'failure'})
|
||||
|
||||
@cherrypy.expose
|
||||
def unqueueAlbum(self, AlbumID, ArtistID):
|
||||
@@ -462,10 +484,12 @@ class WebInterface(object):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
myDB.action('DELETE from have WHERE Matched=?', [AlbumID])
|
||||
album = myDB.action('SELECT ArtistID, ArtistName, AlbumTitle from albums where AlbumID=?', [AlbumID]).fetchone()
|
||||
album = myDB.action('SELECT ArtistID, ArtistName, AlbumTitle from albums where AlbumID=?',
|
||||
[AlbumID]).fetchone()
|
||||
if album:
|
||||
ArtistID = album['ArtistID']
|
||||
myDB.action('DELETE from have WHERE ArtistName=? AND AlbumTitle=?', [album['ArtistName'], album['AlbumTitle']])
|
||||
myDB.action('DELETE from have WHERE ArtistName=? AND AlbumTitle=?',
|
||||
[album['ArtistName'], album['AlbumTitle']])
|
||||
|
||||
myDB.action('DELETE from albums WHERE AlbumID=?', [AlbumID])
|
||||
myDB.action('DELETE from tracks WHERE AlbumID=?', [AlbumID])
|
||||
@@ -505,9 +529,11 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def upcoming(self):
|
||||
myDB = db.DBConnection()
|
||||
upcoming = myDB.select("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC")
|
||||
upcoming = myDB.select(
|
||||
"SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate ASC")
|
||||
wanted = myDB.select("SELECT * from albums WHERE Status='Wanted'")
|
||||
return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming, wanted=wanted)
|
||||
return serve_template(templatename="upcoming.html", title="Upcoming", upcoming=upcoming,
|
||||
wanted=wanted)
|
||||
|
||||
@cherrypy.expose
|
||||
def manage(self):
|
||||
@@ -519,7 +545,8 @@ class WebInterface(object):
|
||||
def manageArtists(self):
|
||||
myDB = db.DBConnection()
|
||||
artists = myDB.select('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
|
||||
return serve_template(templatename="manageartists.html", title="Manage Artists", artists=artists)
|
||||
return serve_template(templatename="manageartists.html", title="Manage Artists",
|
||||
artists=artists)
|
||||
|
||||
@cherrypy.expose
|
||||
def manageAlbums(self, Status=None):
|
||||
@@ -530,87 +557,115 @@ class WebInterface(object):
|
||||
albums = myDB.select('SELECT * from albums WHERE Status=?', [Status])
|
||||
else:
|
||||
albums = myDB.select('SELECT * from albums')
|
||||
return serve_template(templatename="managealbums.html", title="Manage Albums", albums=albums)
|
||||
return serve_template(templatename="managealbums.html", title="Manage Albums",
|
||||
albums=albums)
|
||||
|
||||
@cherrypy.expose
|
||||
def manageNew(self):
|
||||
myDB = db.DBConnection()
|
||||
newartists = myDB.select('SELECT * from newartists')
|
||||
return serve_template(templatename="managenew.html", title="Manage New Artists", newartists=newartists)
|
||||
return serve_template(templatename="managenew.html", title="Manage New Artists",
|
||||
newartists=newartists)
|
||||
|
||||
@cherrypy.expose
|
||||
def manageUnmatched(self):
|
||||
myDB = db.DBConnection()
|
||||
have_album_dictionary = []
|
||||
headphones_album_dictionary = []
|
||||
have_albums = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName from have WHERE Matched = "Failed" GROUP BY AlbumTitle ORDER BY ArtistName')
|
||||
have_albums = myDB.select(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName from have WHERE Matched = "Failed" GROUP BY AlbumTitle ORDER BY ArtistName')
|
||||
for albums in have_albums:
|
||||
#Have to skip over manually matched tracks
|
||||
# Have to skip over manually matched tracks
|
||||
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
|
||||
original_clean = helpers.cleanName(albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
||||
# else:
|
||||
# original_clean = None
|
||||
original_clean = helpers.cleanName(
|
||||
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
||||
# else:
|
||||
# original_clean = None
|
||||
if original_clean == albums['CleanName']:
|
||||
have_dict = {'ArtistName': albums['ArtistName'], 'AlbumTitle': albums['AlbumTitle']}
|
||||
have_dict = {'ArtistName': albums['ArtistName'],
|
||||
'AlbumTitle': albums['AlbumTitle']}
|
||||
have_album_dictionary.append(have_dict)
|
||||
headphones_albums = myDB.select('SELECT ArtistName, AlbumTitle from albums ORDER BY ArtistName')
|
||||
headphones_albums = myDB.select(
|
||||
'SELECT ArtistName, AlbumTitle from albums ORDER BY ArtistName')
|
||||
for albums in headphones_albums:
|
||||
if albums['ArtistName'] and albums['AlbumTitle']:
|
||||
headphones_dict = {'ArtistName': albums['ArtistName'], 'AlbumTitle': albums['AlbumTitle']}
|
||||
headphones_dict = {'ArtistName': albums['ArtistName'],
|
||||
'AlbumTitle': albums['AlbumTitle']}
|
||||
headphones_album_dictionary.append(headphones_dict)
|
||||
#unmatchedalbums = [f for f in have_album_dictionary if f not in [x for x in headphones_album_dictionary]]
|
||||
# unmatchedalbums = [f for f in have_album_dictionary if f not in [x for x in headphones_album_dictionary]]
|
||||
|
||||
check = set([(cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) for d in headphones_album_dictionary])
|
||||
unmatchedalbums = [d for d in have_album_dictionary if (cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) not in check]
|
||||
check = set(
|
||||
[(cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) for d in
|
||||
headphones_album_dictionary])
|
||||
unmatchedalbums = [d for d in have_album_dictionary if (
|
||||
cleanName(d['ArtistName']).lower(), cleanName(d['AlbumTitle']).lower()) not in check]
|
||||
|
||||
return serve_template(templatename="manageunmatched.html", title="Manage Unmatched Items", unmatchedalbums=unmatchedalbums)
|
||||
return serve_template(templatename="manageunmatched.html", title="Manage Unmatched Items",
|
||||
unmatchedalbums=unmatchedalbums)
|
||||
|
||||
@cherrypy.expose
|
||||
def markUnmatched(self, action=None, existing_artist=None, existing_album=None, new_artist=None, new_album=None):
|
||||
def markUnmatched(self, action=None, existing_artist=None, existing_album=None, new_artist=None,
|
||||
new_album=None):
|
||||
myDB = db.DBConnection()
|
||||
|
||||
if action == "ignoreArtist":
|
||||
artist = existing_artist
|
||||
myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND Matched = "Failed"', [artist])
|
||||
myDB.action(
|
||||
'UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND Matched = "Failed"',
|
||||
[artist])
|
||||
|
||||
elif action == "ignoreAlbum":
|
||||
artist = existing_artist
|
||||
album = existing_album
|
||||
myDB.action('UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND AlbumTitle=? AND Matched = "Failed"', (artist, album))
|
||||
myDB.action(
|
||||
'UPDATE have SET Matched="Ignored" WHERE ArtistName=? AND AlbumTitle=? AND Matched = "Failed"',
|
||||
(artist, album))
|
||||
|
||||
elif action == "matchArtist":
|
||||
existing_artist_clean = helpers.cleanName(existing_artist).lower()
|
||||
new_artist_clean = helpers.cleanName(new_artist).lower()
|
||||
if new_artist_clean != existing_artist_clean:
|
||||
have_tracks = myDB.action('SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?', [existing_artist])
|
||||
have_tracks = myDB.action(
|
||||
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
|
||||
[existing_artist])
|
||||
update_count = 0
|
||||
for entry in have_tracks:
|
||||
old_clean_filename = entry['CleanName']
|
||||
if old_clean_filename.startswith(existing_artist_clean):
|
||||
new_clean_filename = old_clean_filename.replace(existing_artist_clean, new_artist_clean, 1)
|
||||
myDB.action('UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?', [new_clean_filename, existing_artist, old_clean_filename])
|
||||
new_clean_filename = old_clean_filename.replace(existing_artist_clean,
|
||||
new_artist_clean, 1)
|
||||
myDB.action(
|
||||
'UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?',
|
||||
[new_clean_filename, existing_artist, old_clean_filename])
|
||||
controlValueDict = {"CleanName": new_clean_filename}
|
||||
newValueDict = {"Location": entry['Location'],
|
||||
"BitRate": entry['BitRate'],
|
||||
"Format": entry['Format']
|
||||
}
|
||||
#Attempt to match tracks with new CleanName
|
||||
match_alltracks = myDB.action('SELECT CleanName from alltracks WHERE CleanName=?', [new_clean_filename]).fetchone()
|
||||
# Attempt to match tracks with new CleanName
|
||||
match_alltracks = myDB.action(
|
||||
'SELECT CleanName from alltracks WHERE CleanName=?',
|
||||
[new_clean_filename]).fetchone()
|
||||
if match_alltracks:
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
match_tracks = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName=?', [new_clean_filename]).fetchone()
|
||||
match_tracks = myDB.action(
|
||||
'SELECT CleanName, AlbumID from tracks WHERE CleanName=?',
|
||||
[new_clean_filename]).fetchone()
|
||||
if match_tracks:
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?', [new_clean_filename])
|
||||
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?',
|
||||
[new_clean_filename])
|
||||
update_count += 1
|
||||
#This was throwing errors and I don't know why, but it seems to be working fine.
|
||||
#else:
|
||||
#logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist)
|
||||
logger.info("Manual matching yielded %s new matches for Artist: %s" % (update_count, new_artist))
|
||||
# This was throwing errors and I don't know why, but it seems to be working fine.
|
||||
# else:
|
||||
# logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist)
|
||||
logger.info("Manual matching yielded %s new matches for Artist: %s" % (
|
||||
update_count, new_artist))
|
||||
if update_count > 0:
|
||||
librarysync.update_album_status()
|
||||
else:
|
||||
logger.info("Artist %s already named appropriately; nothing to modify" % existing_artist)
|
||||
logger.info(
|
||||
"Artist %s already named appropriately; nothing to modify" % existing_artist)
|
||||
|
||||
elif action == "matchAlbum":
|
||||
existing_artist_clean = helpers.cleanName(existing_artist).lower()
|
||||
@@ -620,83 +675,115 @@ class WebInterface(object):
|
||||
existing_clean_string = existing_artist_clean + " " + existing_album_clean
|
||||
new_clean_string = new_artist_clean + " " + new_album_clean
|
||||
if existing_clean_string != new_clean_string:
|
||||
have_tracks = myDB.action('SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=? AND AlbumTitle=?', (existing_artist, existing_album))
|
||||
have_tracks = myDB.action(
|
||||
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=? AND AlbumTitle=?',
|
||||
(existing_artist, existing_album))
|
||||
update_count = 0
|
||||
for entry in have_tracks:
|
||||
old_clean_filename = entry['CleanName']
|
||||
if old_clean_filename.startswith(existing_clean_string):
|
||||
new_clean_filename = old_clean_filename.replace(existing_clean_string, new_clean_string, 1)
|
||||
myDB.action('UPDATE have SET CleanName=? WHERE ArtistName=? AND AlbumTitle=? AND CleanName=?', [new_clean_filename, existing_artist, existing_album, old_clean_filename])
|
||||
new_clean_filename = old_clean_filename.replace(existing_clean_string,
|
||||
new_clean_string, 1)
|
||||
myDB.action(
|
||||
'UPDATE have SET CleanName=? WHERE ArtistName=? AND AlbumTitle=? AND CleanName=?',
|
||||
[new_clean_filename, existing_artist, existing_album,
|
||||
old_clean_filename])
|
||||
controlValueDict = {"CleanName": new_clean_filename}
|
||||
newValueDict = {"Location": entry['Location'],
|
||||
"BitRate": entry['BitRate'],
|
||||
"Format": entry['Format']
|
||||
}
|
||||
#Attempt to match tracks with new CleanName
|
||||
match_alltracks = myDB.action('SELECT CleanName from alltracks WHERE CleanName=?', [new_clean_filename]).fetchone()
|
||||
# Attempt to match tracks with new CleanName
|
||||
match_alltracks = myDB.action(
|
||||
'SELECT CleanName from alltracks WHERE CleanName=?',
|
||||
[new_clean_filename]).fetchone()
|
||||
if match_alltracks:
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
match_tracks = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName=?', [new_clean_filename]).fetchone()
|
||||
match_tracks = myDB.action(
|
||||
'SELECT CleanName, AlbumID from tracks WHERE CleanName=?',
|
||||
[new_clean_filename]).fetchone()
|
||||
if match_tracks:
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?', [new_clean_filename])
|
||||
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?',
|
||||
[new_clean_filename])
|
||||
album_id = match_tracks['AlbumID']
|
||||
update_count += 1
|
||||
#This was throwing errors and I don't know why, but it seems to be working fine.
|
||||
#else:
|
||||
#logger.info("There was an error modifying Artist %s / Album %s with clean name %s" % (existing_artist, existing_album, existing_clean_string))
|
||||
logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % (update_count, new_artist, new_album))
|
||||
# This was throwing errors and I don't know why, but it seems to be working fine.
|
||||
# else:
|
||||
# logger.info("There was an error modifying Artist %s / Album %s with clean name %s" % (existing_artist, existing_album, existing_clean_string))
|
||||
logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % (
|
||||
update_count, new_artist, new_album))
|
||||
if update_count > 0:
|
||||
librarysync.update_album_status(album_id)
|
||||
else:
|
||||
logger.info("Artist %s / Album %s already named appropriately; nothing to modify" % (existing_artist, existing_album))
|
||||
logger.info(
|
||||
"Artist %s / Album %s already named appropriately; nothing to modify" % (
|
||||
existing_artist, existing_album))
|
||||
|
||||
@cherrypy.expose
|
||||
def manageManual(self):
|
||||
myDB = db.DBConnection()
|
||||
manual_albums = []
|
||||
manualalbums = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have')
|
||||
manualalbums = myDB.select(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have')
|
||||
for albums in manualalbums:
|
||||
if albums['ArtistName'] and albums['AlbumTitle'] and albums['TrackTitle']:
|
||||
original_clean = helpers.cleanName(albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
||||
if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums['CleanName'] != original_clean:
|
||||
original_clean = helpers.cleanName(
|
||||
albums['ArtistName'] + " " + albums['AlbumTitle'] + " " + albums['TrackTitle'])
|
||||
if albums['Matched'] == "Ignored" or albums['Matched'] == "Manual" or albums[
|
||||
'CleanName'] != original_clean:
|
||||
if albums['Matched'] == "Ignored":
|
||||
album_status = "Ignored"
|
||||
elif albums['Matched'] == "Manual" or albums['CleanName'] != original_clean:
|
||||
album_status = "Matched"
|
||||
manual_dict = {'ArtistName': albums['ArtistName'], 'AlbumTitle': albums['AlbumTitle'], 'AlbumStatus': album_status}
|
||||
manual_dict = {'ArtistName': albums['ArtistName'],
|
||||
'AlbumTitle': albums['AlbumTitle'], 'AlbumStatus': album_status}
|
||||
if manual_dict not in manual_albums:
|
||||
manual_albums.append(manual_dict)
|
||||
manual_albums_sorted = sorted(manual_albums, key=itemgetter('ArtistName', 'AlbumTitle'))
|
||||
|
||||
return serve_template(templatename="managemanual.html", title="Manage Manual Items", manualalbums=manual_albums_sorted)
|
||||
return serve_template(templatename="managemanual.html", title="Manage Manual Items",
|
||||
manualalbums=manual_albums_sorted)
|
||||
|
||||
@cherrypy.expose
|
||||
def markManual(self, action=None, existing_artist=None, existing_album=None):
|
||||
myDB = db.DBConnection()
|
||||
if action == "unignoreArtist":
|
||||
artist = existing_artist
|
||||
myDB.action('UPDATE have SET Matched="Failed" WHERE ArtistName=? AND Matched="Ignored"', [artist])
|
||||
myDB.action('UPDATE have SET Matched="Failed" WHERE ArtistName=? AND Matched="Ignored"',
|
||||
[artist])
|
||||
logger.info("Artist: %s successfully restored to unmatched list" % artist)
|
||||
|
||||
elif action == "unignoreAlbum":
|
||||
artist = existing_artist
|
||||
album = existing_album
|
||||
myDB.action('UPDATE have SET Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND Matched="Ignored"', (artist, album))
|
||||
myDB.action(
|
||||
'UPDATE have SET Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND Matched="Ignored"',
|
||||
(artist, album))
|
||||
logger.info("Album: %s successfully restored to unmatched list" % album)
|
||||
|
||||
elif action == "unmatchArtist":
|
||||
artist = existing_artist
|
||||
update_clean = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=?', [artist])
|
||||
update_clean = myDB.select(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=?',
|
||||
[artist])
|
||||
update_count = 0
|
||||
for tracks in update_clean:
|
||||
original_clean = helpers.cleanName(tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks['TrackTitle']).lower()
|
||||
original_clean = helpers.cleanName(
|
||||
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
|
||||
'TrackTitle']).lower()
|
||||
album = tracks['AlbumTitle']
|
||||
track_title = tracks['TrackTitle']
|
||||
if tracks['CleanName'] != original_clean:
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']])
|
||||
myDB.action('UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?', (original_clean, artist, album, track_title))
|
||||
myDB.action(
|
||||
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
|
||||
[None, None, None, tracks['CleanName']])
|
||||
myDB.action(
|
||||
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
|
||||
[None, None, None, tracks['CleanName']])
|
||||
myDB.action(
|
||||
'UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?',
|
||||
(original_clean, artist, album, track_title))
|
||||
update_count += 1
|
||||
if update_count > 0:
|
||||
librarysync.update_album_status()
|
||||
@@ -705,18 +792,29 @@ class WebInterface(object):
|
||||
elif action == "unmatchAlbum":
|
||||
artist = existing_artist
|
||||
album = existing_album
|
||||
update_clean = myDB.select('SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=? AND AlbumTitle=?', (artist, album))
|
||||
update_clean = myDB.select(
|
||||
'SELECT ArtistName, AlbumTitle, TrackTitle, CleanName, Matched from have WHERE ArtistName=? AND AlbumTitle=?',
|
||||
(artist, album))
|
||||
update_count = 0
|
||||
for tracks in update_clean:
|
||||
original_clean = helpers.cleanName(tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks['TrackTitle']).lower()
|
||||
original_clean = helpers.cleanName(
|
||||
tracks['ArtistName'] + " " + tracks['AlbumTitle'] + " " + tracks[
|
||||
'TrackTitle']).lower()
|
||||
track_title = tracks['TrackTitle']
|
||||
if tracks['CleanName'] != original_clean:
|
||||
album_id_check = myDB.action('SELECT AlbumID from tracks WHERE CleanName=?', [tracks['CleanName']]).fetchone()
|
||||
album_id_check = myDB.action('SELECT AlbumID from tracks WHERE CleanName=?',
|
||||
[tracks['CleanName']]).fetchone()
|
||||
if album_id_check:
|
||||
album_id = album_id_check[0]
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']])
|
||||
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?', [None, None, None, tracks['CleanName']])
|
||||
myDB.action('UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?', (original_clean, artist, album, track_title))
|
||||
myDB.action(
|
||||
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
|
||||
[None, None, None, tracks['CleanName']])
|
||||
myDB.action(
|
||||
'UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE CleanName=?',
|
||||
[None, None, None, tracks['CleanName']])
|
||||
myDB.action(
|
||||
'UPDATE have SET CleanName=?, Matched="Failed" WHERE ArtistName=? AND AlbumTitle=? AND TrackTitle=?',
|
||||
(original_clean, artist, album, track_title))
|
||||
update_count += 1
|
||||
if update_count > 0:
|
||||
librarysync.update_album_status(album_id)
|
||||
@@ -802,7 +900,9 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def forcePostProcess(self, dir=None, album_dir=None, keep_original_folder=False):
|
||||
from headphones import postprocessor
|
||||
threading.Thread(target=postprocessor.forcePostProcess, kwargs={'dir': dir, 'album_dir': album_dir, 'keep_original_folder':keep_original_folder == 'True'}).start()
|
||||
threading.Thread(target=postprocessor.forcePostProcess,
|
||||
kwargs={'dir': dir, 'album_dir': album_dir,
|
||||
'keep_original_folder': keep_original_folder == 'True'}).start()
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -814,7 +914,8 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def history(self):
|
||||
myDB = db.DBConnection()
|
||||
history = myDB.select('''SELECT AlbumID, Title, Size, URL, DateAdded, Status, Kind, ifnull(FolderName, '?') FolderName FROM snatched WHERE Status NOT LIKE "Seed%" ORDER BY DateAdded DESC''')
|
||||
history = myDB.select(
|
||||
'''SELECT AlbumID, Title, Size, URL, DateAdded, Status, Kind, ifnull(FolderName, '?') FolderName FROM snatched WHERE Status NOT LIKE "Seed%" ORDER BY DateAdded DESC''')
|
||||
return serve_template(templatename="history.html", title="History", history=history)
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -831,13 +932,14 @@ class WebInterface(object):
|
||||
def toggleVerbose(self):
|
||||
headphones.VERBOSE = not headphones.VERBOSE
|
||||
logger.initLogger(console=not headphones.QUIET,
|
||||
log_dir=headphones.CONFIG.LOG_DIR, verbose=headphones.VERBOSE)
|
||||
log_dir=headphones.CONFIG.LOG_DIR, verbose=headphones.VERBOSE)
|
||||
logger.info("Verbose toggled, set to %s", headphones.VERBOSE)
|
||||
logger.debug("If you read this message, debug logging is available")
|
||||
raise cherrypy.HTTPRedirect("logs")
|
||||
|
||||
@cherrypy.expose
|
||||
def getLog(self, iDisplayStart=0, iDisplayLength=100, iSortCol_0=0, sSortDir_0="desc", sSearch="", **kwargs):
|
||||
def getLog(self, iDisplayStart=0, iDisplayLength=100, iSortCol_0=0, sSortDir_0="desc",
|
||||
sSearch="", **kwargs):
|
||||
iDisplayStart = int(iDisplayStart)
|
||||
iDisplayLength = int(iDisplayLength)
|
||||
|
||||
@@ -845,7 +947,8 @@ class WebInterface(object):
|
||||
if sSearch == "":
|
||||
filtered = headphones.LOG_LIST[::]
|
||||
else:
|
||||
filtered = [row for row in headphones.LOG_LIST for column in row if sSearch.lower() in column.lower()]
|
||||
filtered = [row for row in headphones.LOG_LIST for column in row if
|
||||
sSearch.lower() in column.lower()]
|
||||
|
||||
sortcolumn = 0
|
||||
if iSortCol_0 == '1':
|
||||
@@ -864,7 +967,8 @@ class WebInterface(object):
|
||||
})
|
||||
|
||||
@cherrypy.expose
|
||||
def getArtists_json(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', sSortDir_0='asc', **kwargs):
|
||||
def getArtists_json(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0',
|
||||
sSortDir_0='asc', **kwargs):
|
||||
iDisplayStart = int(iDisplayStart)
|
||||
iDisplayLength = int(iDisplayLength)
|
||||
filtered = []
|
||||
@@ -885,15 +989,18 @@ class WebInterface(object):
|
||||
filtered = myDB.select(query)
|
||||
totalcount = len(filtered)
|
||||
else:
|
||||
query = 'SELECT * from artists WHERE ArtistSortName LIKE "%' + sSearch + '%" OR LatestAlbum LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (sortcolumn, sSortDir_0)
|
||||
query = 'SELECT * from artists WHERE ArtistSortName LIKE "%' + sSearch + '%" OR LatestAlbum LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (
|
||||
sortcolumn, sSortDir_0)
|
||||
filtered = myDB.select(query)
|
||||
totalcount = myDB.select('SELECT COUNT(*) from artists')[0][0]
|
||||
|
||||
if sortbyhavepercent:
|
||||
filtered.sort(key=lambda x: (float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0, x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc")
|
||||
filtered.sort(key=lambda x: (
|
||||
float(x['HaveTracks']) / x['TotalTracks'] if x['TotalTracks'] > 0 else 0.0,
|
||||
x['HaveTracks'] if x['HaveTracks'] else 0.0), reverse=sSortDir_0 == "asc")
|
||||
|
||||
#can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill
|
||||
#just reverse it here and the first click on the "Latest Album" header will sort by descending release date
|
||||
# can't figure out how to change the datatables default sorting order when its using an ajax datasource so ill
|
||||
# just reverse it here and the first click on the "Latest Album" header will sort by descending release date
|
||||
if sortcolumn == 'ReleaseDate':
|
||||
filtered.reverse()
|
||||
|
||||
@@ -901,16 +1008,16 @@ class WebInterface(object):
|
||||
rows = []
|
||||
for artist in artists:
|
||||
row = {"ArtistID": artist['ArtistID'],
|
||||
"ArtistName": artist["ArtistName"],
|
||||
"ArtistSortName": artist["ArtistSortName"],
|
||||
"Status": artist["Status"],
|
||||
"TotalTracks": artist["TotalTracks"],
|
||||
"HaveTracks": artist["HaveTracks"],
|
||||
"LatestAlbum": "",
|
||||
"ReleaseDate": "",
|
||||
"ReleaseInFuture": "False",
|
||||
"AlbumID": "",
|
||||
}
|
||||
"ArtistName": artist["ArtistName"],
|
||||
"ArtistSortName": artist["ArtistSortName"],
|
||||
"Status": artist["Status"],
|
||||
"TotalTracks": artist["TotalTracks"],
|
||||
"HaveTracks": artist["HaveTracks"],
|
||||
"LatestAlbum": "",
|
||||
"ReleaseDate": "",
|
||||
"ReleaseInFuture": "False",
|
||||
"AlbumID": "",
|
||||
}
|
||||
|
||||
if not row['HaveTracks']:
|
||||
row['HaveTracks'] = 0
|
||||
@@ -954,9 +1061,9 @@ class WebInterface(object):
|
||||
myDB = db.DBConnection()
|
||||
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
|
||||
artist_json = json.dumps({
|
||||
'ArtistName': artist['ArtistName'],
|
||||
'Status': artist['Status']
|
||||
})
|
||||
'ArtistName': artist['ArtistName'],
|
||||
'Status': artist['Status']
|
||||
})
|
||||
return artist_json
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -964,9 +1071,9 @@ class WebInterface(object):
|
||||
myDB = db.DBConnection()
|
||||
album = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
|
||||
album_json = json.dumps({
|
||||
'AlbumTitle': album['AlbumTitle'],
|
||||
'ArtistName': album['ArtistName'],
|
||||
'Status': album['Status']
|
||||
'AlbumTitle': album['AlbumTitle'],
|
||||
'ArtistName': album['ArtistName'],
|
||||
'Status': album['Status']
|
||||
})
|
||||
return album_json
|
||||
|
||||
@@ -982,7 +1089,9 @@ class WebInterface(object):
|
||||
myDB.action('DELETE from snatched WHERE Status=?', [type])
|
||||
else:
|
||||
logger.info(u"Deleting '%s' from history" % title)
|
||||
myDB.action('DELETE from snatched WHERE Status NOT LIKE "Seed%" AND Title=? AND DateAdded=?', [title, date_added])
|
||||
myDB.action(
|
||||
'DELETE from snatched WHERE Status NOT LIKE "Seed%" AND Title=? AND DateAdded=?',
|
||||
[title, date_added])
|
||||
raise cherrypy.HTTPRedirect("history")
|
||||
|
||||
@cherrypy.expose
|
||||
@@ -995,7 +1104,7 @@ class WebInterface(object):
|
||||
def forceScan(self, keepmatched=None):
|
||||
myDB = db.DBConnection()
|
||||
#########################################
|
||||
#NEED TO MOVE THIS INTO A SEPARATE FUNCTION BEFORE RELEASE
|
||||
# NEED TO MOVE THIS INTO A SEPARATE FUNCTION BEFORE RELEASE
|
||||
myDB.select('DELETE from Have')
|
||||
logger.info('Removed all entries in local library database')
|
||||
myDB.select('UPDATE alltracks SET Location=NULL, BitRate=NULL, Format=NULL')
|
||||
@@ -1003,7 +1112,8 @@ class WebInterface(object):
|
||||
logger.info('All tracks in library unmatched')
|
||||
myDB.action('UPDATE artists SET HaveTracks=NULL')
|
||||
logger.info('Reset track counts for all artists')
|
||||
myDB.action('UPDATE albums SET Status="Skipped" WHERE Status="Skipped" OR Status="Downloaded"')
|
||||
myDB.action(
|
||||
'UPDATE albums SET Status="Skipped" WHERE Status="Skipped" OR Status="Downloaded"')
|
||||
logger.info('Marking all unwanted albums as Skipped')
|
||||
try:
|
||||
threading.Thread(target=librarysync.libraryScan).start()
|
||||
@@ -1014,7 +1124,8 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def config(self):
|
||||
interface_dir = os.path.join(headphones.PROG_DIR, 'data/interfaces/')
|
||||
interface_list = [name for name in os.listdir(interface_dir) if os.path.isdir(os.path.join(interface_dir, name))]
|
||||
interface_list = [name for name in os.listdir(interface_dir) if
|
||||
os.path.isdir(os.path.join(interface_dir, name))]
|
||||
|
||||
config = {
|
||||
"http_host": headphones.CONFIG.HTTP_HOST,
|
||||
@@ -1115,7 +1226,8 @@ class WebInterface(object):
|
||||
"preferred_bitrate": headphones.CONFIG.PREFERRED_BITRATE,
|
||||
"preferred_bitrate_high": headphones.CONFIG.PREFERRED_BITRATE_HIGH_BUFFER,
|
||||
"preferred_bitrate_low": headphones.CONFIG.PREFERRED_BITRATE_LOW_BUFFER,
|
||||
"preferred_bitrate_allow_lossless": checked(headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS),
|
||||
"preferred_bitrate_allow_lossless": checked(
|
||||
headphones.CONFIG.PREFERRED_BITRATE_ALLOW_LOSSLESS),
|
||||
"detect_bitrate": checked(headphones.CONFIG.DETECT_BITRATE),
|
||||
"lossless_bitrate_from": headphones.CONFIG.LOSSLESS_BITRATE_FROM,
|
||||
"lossless_bitrate_to": headphones.CONFIG.LOSSLESS_BITRATE_TO,
|
||||
@@ -1133,7 +1245,7 @@ class WebInterface(object):
|
||||
"embed_album_art": checked(headphones.CONFIG.EMBED_ALBUM_ART),
|
||||
"embed_lyrics": checked(headphones.CONFIG.EMBED_LYRICS),
|
||||
"replace_existing_folders": checked(headphones.CONFIG.REPLACE_EXISTING_FOLDERS),
|
||||
"keep_original_folder" : checked(headphones.CONFIG.KEEP_ORIGINAL_FOLDER),
|
||||
"keep_original_folder": checked(headphones.CONFIG.KEEP_ORIGINAL_FOLDER),
|
||||
"destination_dir": headphones.CONFIG.DESTINATION_DIR,
|
||||
"lossless_destination_dir": headphones.CONFIG.LOSSLESS_DESTINATION_DIR,
|
||||
"folder_format": headphones.CONFIG.FOLDER_FORMAT,
|
||||
@@ -1286,18 +1398,31 @@ class WebInterface(object):
|
||||
# Handle the variable config options. Note - keys with False values aren't getting passed
|
||||
|
||||
checked_configs = [
|
||||
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer", "use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
||||
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay", "use_mininova", "use_waffles", "use_rutracker",
|
||||
"use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
||||
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art", "embed_album_art", "embed_lyrics",
|
||||
"replace_existing_folders", "keep_original_folder", "file_underscores", "include_extras", "official_releases_only",
|
||||
"wait_until_release_date", "autowant_upcoming", "autowant_all", "autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files", "music_encoder",
|
||||
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled", "growl_onsnatch", "prowl_enabled",
|
||||
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled", "plex_enabled", "plex_update", "plex_notify",
|
||||
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch", "synoindex_enabled", "pushover_enabled",
|
||||
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled", "twitter_enabled", "twitter_onsnatch",
|
||||
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch", "songkick_enabled", "songkick_filter_enabled",
|
||||
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch", "customauth", "idtag"
|
||||
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer",
|
||||
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
||||
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
|
||||
"use_mininova", "use_waffles", "use_rutracker",
|
||||
"use_whatcd", "use_strike", "preferred_bitrate_allow_lossless", "detect_bitrate",
|
||||
"ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
||||
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
|
||||
"embed_album_art", "embed_lyrics",
|
||||
"replace_existing_folders", "keep_original_folder", "file_underscores",
|
||||
"include_extras", "official_releases_only",
|
||||
"wait_until_release_date", "autowant_upcoming", "autowant_all",
|
||||
"autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files",
|
||||
"music_encoder",
|
||||
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled",
|
||||
"growl_onsnatch", "prowl_enabled",
|
||||
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled",
|
||||
"plex_enabled", "plex_update", "plex_notify",
|
||||
"nma_enabled", "nma_onsnatch", "pushalot_enabled", "pushalot_onsnatch",
|
||||
"synoindex_enabled", "pushover_enabled",
|
||||
"pushover_onsnatch", "pushbullet_enabled", "pushbullet_onsnatch", "subsonic_enabled",
|
||||
"twitter_enabled", "twitter_onsnatch",
|
||||
"osx_notify_enabled", "osx_notify_onsnatch", "boxcar_enabled", "boxcar_onsnatch",
|
||||
"songkick_enabled", "songkick_filter_enabled",
|
||||
"mpc_enabled", "email_enabled", "email_ssl", "email_tls", "email_onsnatch",
|
||||
"customauth", "idtag"
|
||||
]
|
||||
for checked_config in checked_configs:
|
||||
if checked_config not in kwargs:
|
||||
@@ -1473,9 +1598,11 @@ class WebInterface(object):
|
||||
image_dict = {'artwork': image_url, 'thumbnail': thumb_url}
|
||||
elif AlbumID and (not image_dict['artwork'] or not image_dict['thumbnail']):
|
||||
if not image_dict['artwork']:
|
||||
image_dict['artwork'] = "http://coverartarchive.org/release/%s/front-500.jpg" % AlbumID
|
||||
image_dict[
|
||||
'artwork'] = "http://coverartarchive.org/release/%s/front-500.jpg" % AlbumID
|
||||
if not image_dict['thumbnail']:
|
||||
image_dict['thumbnail'] = "http://coverartarchive.org/release/%s/front-250.jpg" % AlbumID
|
||||
image_dict[
|
||||
'thumbnail'] = "http://coverartarchive.org/release/%s/front-250.jpg" % AlbumID
|
||||
|
||||
return json.dumps(image_dict)
|
||||
|
||||
@@ -1514,7 +1641,8 @@ class WebInterface(object):
|
||||
if result:
|
||||
osx_notify = notifiers.OSX_NOTIFY()
|
||||
osx_notify.notify('Registered', result, 'Success :-)')
|
||||
logger.info('Registered %s, to re-register a different app, delete this app first' % result)
|
||||
logger.info(
|
||||
'Registered %s, to re-register a different app, delete this app first' % result)
|
||||
else:
|
||||
logger.warn(msg)
|
||||
return msg
|
||||
@@ -1536,7 +1664,8 @@ class WebInterface(object):
|
||||
def testPushbullet(self):
|
||||
logger.info("Testing Pushbullet notifications")
|
||||
pushbullet = notifiers.PUSHBULLET()
|
||||
pushbullet.notify("it works!")
|
||||
pushbullet.notify("it works!", "Test message")
|
||||
|
||||
|
||||
class Artwork(object):
|
||||
@cherrypy.expose
|
||||
@@ -1605,4 +1734,6 @@ class Artwork(object):
|
||||
return fp.read()
|
||||
|
||||
thumbs = Thumbs()
|
||||
|
||||
|
||||
WebInterface.artwork = Artwork()
|
||||
|
||||
@@ -13,18 +13,17 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import os
|
||||
import cherrypy
|
||||
import headphones
|
||||
|
||||
from headphones import logger
|
||||
from headphones.webserve import WebInterface
|
||||
from headphones.helpers import create_https_certificates
|
||||
|
||||
|
||||
def initialize(options):
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
enable_https = options['enable_https']
|
||||
https_cert = options['https_cert']
|
||||
@@ -33,15 +32,16 @@ def initialize(options):
|
||||
if enable_https:
|
||||
# If either the HTTPS certificate or key do not exist, try to make
|
||||
# self-signed ones.
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (
|
||||
https_key and os.path.exists(https_key)):
|
||||
if not create_https_certificates(https_cert, https_key):
|
||||
logger.warn("Unable to create certificate and key. Disabling " \
|
||||
"HTTPS")
|
||||
"HTTPS")
|
||||
enable_https = False
|
||||
|
||||
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
||||
"key.")
|
||||
"key.")
|
||||
enable_https = False
|
||||
|
||||
options_dict = {
|
||||
@@ -63,7 +63,7 @@ def initialize(options):
|
||||
protocol = "http"
|
||||
|
||||
logger.info("Starting Headphones web server on %s://%s:%d/", protocol,
|
||||
options['http_host'], options['http_port'])
|
||||
options['http_host'], options['http_port'])
|
||||
cherrypy.config.update(options_dict)
|
||||
|
||||
conf = {
|
||||
@@ -99,7 +99,8 @@ def initialize(options):
|
||||
}
|
||||
|
||||
if options['http_password']:
|
||||
logger.info("Web server authentication is enabled, username is '%s'", options['http_username'])
|
||||
logger.info("Web server authentication is enabled, username is '%s'",
|
||||
options['http_username'])
|
||||
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
@@ -118,7 +119,8 @@ def initialize(options):
|
||||
cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
|
||||
cherrypy.server.start()
|
||||
except IOError:
|
||||
sys.stderr.write('Failed to start on port: %i. Is something else running?\n' % (options['http_port']))
|
||||
sys.stderr.write(
|
||||
'Failed to start on port: %i. Is something else running?\n' % (options['http_port']))
|
||||
sys.exit(1)
|
||||
|
||||
cherrypy.server.wait()
|
||||
|
||||
Reference in New Issue
Block a user