mirror of
https://github.com/rembo10/headphones.git
synced 2026-05-19 10:05:30 +01:00
Merge branch 'develop'. Feature update: allow switching between
musicbrainz releases, much improved track matching, separate lossless destination directories. Bug fixes: quite a few (check log for more details)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<%inherit file="base.html" />
|
||||
<%!
|
||||
<%!
|
||||
from headphones import db, helpers
|
||||
myDB = db.DBConnection()
|
||||
%>
|
||||
@@ -16,7 +16,32 @@
|
||||
%else:
|
||||
<a id="menu_link_retry" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=False', $(this),true);" data-success="Retrying the same version of '${album['AlbumTitle']}'">Retry Download</a>
|
||||
<a id="menu_link_new" href="#" onclick="doAjaxCall('queueAlbum?AlbumID=${album['AlbumID']}&ArtistID=${album['ArtistID']}&new=True', $(this),true);" data-success="Looking for a new version of '${album['AlbumTitle']}'">Try New Version</a>
|
||||
%endif
|
||||
%endif
|
||||
<a class="menu_link_edit" id="album_chooser" href="#">Choose Alternate Release</a>
|
||||
<div id="dialog" title="Choose an Alternate Release" style="display:none" class="configtable">
|
||||
<div class="links">
|
||||
<%
|
||||
alternate_albums = myDB.select("SELECT * from allalbums WHERE AlbumID=?", [album['AlbumID']])
|
||||
%>
|
||||
%if not alternate_albums:
|
||||
<p>No alternate releases found. Try refreshing the artist (if the artist is being refreshed, please wait until it's finished)</p>
|
||||
<h2><a id="refresh_artist" onclick="doAjaxCall('refreshArtist?ArtistID=${album['ArtistID']}', $(this)), true" href="#" data-success="'${album['ArtistName']}' is being refreshed">Refresh Artist</a></h2>
|
||||
%else:
|
||||
%for alternate_album in alternate_albums:
|
||||
<%
|
||||
track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=?", [alternate_album['ReleaseID']]))
|
||||
have_track_count = len(myDB.select("SELECT * from alltracks WHERE ReleaseID=? AND Location IS NOT NULL", [alternate_album['ReleaseID']]))
|
||||
if alternate_album['AlbumID'] == alternate_album['ReleaseID']:
|
||||
alternate_album_name = "Headphones Default Release [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||
else:
|
||||
alternate_album_name = alternate_album['AlbumTitle'] + " (" + alternate_album['ReleaseCountry'] + ", " + alternate_album['ReleaseFormat'] + ") [" + str(have_track_count) + "/" + str(track_count) + " tracks]"
|
||||
|
||||
%>
|
||||
<a href="#" onclick="doAjaxCall('switchAlbum?AlbumID=${album['AlbumID']}&ReleaseID=${alternate_album['ReleaseID']}', $(this), 'table');" data-success="Switched release to: ${alternate_album_name}">${alternate_album_name}</a><br>
|
||||
%endfor
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="artistPage?ArtistID=${album['ArtistID']}" class="back">« Back to ${album['ArtistName']}</a>
|
||||
@@ -82,7 +107,7 @@
|
||||
trackduration = 'n/a'
|
||||
|
||||
if not track['Format']:
|
||||
format = 'Unknown'
|
||||
format = ''
|
||||
else:
|
||||
format = track['Format']
|
||||
%>
|
||||
@@ -143,9 +168,14 @@
|
||||
getInfo(elem,id,'album');
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
getAlbumInfo();
|
||||
getAlbumArt();
|
||||
function initThisPage() {
|
||||
$('#album_chooser').click(function() {
|
||||
$('#dialog').dialog();
|
||||
return false;
|
||||
});
|
||||
$('#refresh_artist').click(function() {
|
||||
$('#dialog').dialog("close");
|
||||
});
|
||||
initActions();
|
||||
setTimeout(function(){
|
||||
initFancybox();
|
||||
@@ -155,8 +185,15 @@
|
||||
"aaSorting": [],
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false
|
||||
});
|
||||
"bPaginate": false,
|
||||
"bDestroy": true
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
getAlbumInfo();
|
||||
getAlbumArt();
|
||||
initThisPage();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
showMsg("Getting artist information",true);
|
||||
%endif
|
||||
getArtistArt();
|
||||
getArtistBio();
|
||||
getAlbumArt();
|
||||
$('#album_table').dataTable({
|
||||
"bDestroy": true,
|
||||
@@ -220,6 +219,7 @@
|
||||
$(document).ready(function() {
|
||||
initActions();
|
||||
initThisPage();
|
||||
getArtistBio();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -342,10 +342,15 @@ m<%inherit file="base.html"/>
|
||||
<input type="checkbox" name="embed_lyrics" value="1" ${config['embed_lyrics']}><label>Embed lyrics</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Path to Destination folder</label>
|
||||
<label>Path to Destination Folder</label>
|
||||
<input type="text" name="destination_dir" value="${config['dest_dir']}" size="50">
|
||||
<small>e.g. /Users/name/Music/iTunes or /Volumes/share/music</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Path to Lossless Destination folder (optional)</label>
|
||||
<input type="text" name="lossless_destination_dir" value="${config['lossless_dest_dir']}" size="50">
|
||||
<small>Set this if you have a separate directory for lossless music</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -379,8 +384,14 @@ m<%inherit file="base.html"/>
|
||||
<input type="checkbox" name="music_encoder" id="music_encoder" value="1" ${config['music_encoder']}/><label>Re-encode downloads during postprocessing</label>
|
||||
</div>
|
||||
<div id="encoderoptions" class="row clearfix checkbox">
|
||||
<div class="row">
|
||||
<input type="checkbox" name="encoderlossless" value="1" ${config['encoderlossless']}/><label>Only re-encode lossless files (.flac)</label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<input type="checkbox" name="delete_lossless_files" value="1" ${config['delete_lossless_files']}/><label>Delete original lossless files after encoding</label>
|
||||
</div>
|
||||
</div>
|
||||
<%
|
||||
if config['encoder'] == 'lame':
|
||||
lameselect = 'selected="selected"'
|
||||
|
||||
@@ -144,6 +144,7 @@ function initConfigCheckbox(elem) {
|
||||
function initActions() {
|
||||
$("#subhead_menu #menu_link_refresh").button({ icons: { primary: "ui-icon-refresh" } });
|
||||
$("#subhead_menu #menu_link_edit").button({ icons: { primary: "ui-icon-pencil" } });
|
||||
$("#subhead_menu .menu_link_edit").button({ icons: { primary: "ui-icon-pencil" } });
|
||||
$("#subhead_menu #menu_link_delete" ).button({ icons: { primary: "ui-icon-trash" } });
|
||||
$("#subhead_menu #menu_link_pauze").button({ icons: { primary: "ui-icon-pause"} });
|
||||
$("#subhead_menu #menu_link_resume").button({ icons: { primary: "ui-icon-play"} });
|
||||
@@ -256,7 +257,7 @@ function doAjaxCall(url,elem,reload,form) {
|
||||
var dataError = $(elem).data('error');
|
||||
if (typeof dataError === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
var dataError = "There was a error";
|
||||
var dataError = "There was an error";
|
||||
}
|
||||
// Get Success & Error message from inline data, else use standard message
|
||||
var succesMsg = $("<div class='msg'><span class='ui-icon ui-icon-check'></span>" + dataSucces + "</div>");
|
||||
|
||||
@@ -6,10 +6,21 @@
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_edit" href="manageAlbums">Manage Albums</a>
|
||||
<a id="menu_link_edit" href="manageArtists">Manage Artists</a>
|
||||
<a class="menu_link_edit" id="manage_albums" href="#">Manage Albums</a>
|
||||
<div id="dialog" title="Choose Album Filter" style="display:none" class="configtable">
|
||||
<div class="links">
|
||||
<a href="manageAlbums?Status=Downloaded"><span class="ui-icon ui-icon-check"></span>Manage Downloaded Albums</a><br>
|
||||
<a href="manageAlbums?Status=Skipped"><span class="ui-icon ui-icon-flag"></span>Manage Skipped Albums</a><br>
|
||||
<a href="manageAlbums?Status=Snatched"><span class="ui-icon ui-icon-arrowthickstop-1-s"></span>Manage Snatched Albums</a><br>
|
||||
<a href="manageAlbums?Status=Upcoming"><span class="ui-icon ui-icon-calendar"></span>Manage Upcoming Albums</a><br>
|
||||
<a href="manageAlbums?Status=Wanted"><span class="ui-icon ui-icon-heart"></span>Manage Wanted Albums</a><br>
|
||||
<br><br>
|
||||
<a href="manageAlbums">Manage All Albums</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="menu_link_edit" href="manageArtists">Manage Artists</a>
|
||||
%if not headphones.ADD_ARTISTS:
|
||||
<a id="menu_link_edit" href="manageNew">Manage New Artists</a>
|
||||
<a class="menu_link_edit" href="manageNew">Manage New Artists</a>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +106,10 @@
|
||||
<%def name="javascriptIncludes()">
|
||||
<script>
|
||||
function initThisPage() {
|
||||
$('#manage_albums').click(function() {
|
||||
$('#dialog').dialog();
|
||||
return false;
|
||||
});
|
||||
jQuery( "#tabs" ).tabs();
|
||||
initActions();
|
||||
};
|
||||
|
||||
@@ -84,6 +84,7 @@ CHECK_GITHUB_INTERVAL = None
|
||||
|
||||
MUSIC_DIR = None
|
||||
DESTINATION_DIR = None
|
||||
LOSSLESS_DESTINATION_DIR = None
|
||||
FOLDER_FORMAT = None
|
||||
FILE_FORMAT = None
|
||||
PATH_TO_XML = None
|
||||
@@ -165,6 +166,7 @@ ENCODEROUTPUTFORMAT = None
|
||||
ENCODERQUALITY = None
|
||||
ENCODERVBRCBR = None
|
||||
ENCODERLOSSLESS = False
|
||||
DELETE_LOSSLESS_FILES = False
|
||||
PROWL_ENABLED = True
|
||||
PROWL_PRIORITY = 1
|
||||
PROWL_KEYS = None
|
||||
@@ -238,7 +240,8 @@ def initialize():
|
||||
|
||||
global __INITIALIZED__, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, \
|
||||
HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, HTTP_PROXY, LAUNCH_BROWSER, API_ENABLED, API_KEY, GIT_PATH, \
|
||||
CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, \
|
||||
CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, MUSIC_DIR, DESTINATION_DIR, \
|
||||
LOSSLESS_DESTINATION_DIR, PREFERRED_QUALITY, PREFERRED_BITRATE, DETECT_BITRATE, \
|
||||
ADD_ARTISTS, CORRECT_METADATA, MOVE_FILES, RENAME_FILES, FOLDER_FORMAT, FILE_FORMAT, CLEANUP_FILES, INCLUDE_EXTRAS, AUTOWANT_UPCOMING, AUTOWANT_ALL, \
|
||||
ADD_ALBUM_ART, EMBED_ALBUM_ART, EMBED_LYRICS, DOWNLOAD_DIR, BLACKHOLE, BLACKHOLE_DIR, USENET_RETENTION, SEARCH_INTERVAL, \
|
||||
TORRENTBLACKHOLE_DIR, NUMBEROFSEEDERS, ISOHUNT, KAT, MININOVA, WAFFLES, WAFFLES_UID, WAFFLES_PASSKEY, DOWNLOAD_TORRENT_DIR, \
|
||||
@@ -246,7 +249,7 @@ def initialize():
|
||||
NZBMATRIX, NZBMATRIX_USERNAME, NZBMATRIX_APIKEY, NEWZNAB, NEWZNAB_HOST, NEWZNAB_APIKEY, NEWZNAB_ENABLED, EXTRA_NEWZNABS,\
|
||||
NZBSORG, NZBSORG_UID, NZBSORG_HASH, NEWZBIN, NEWZBIN_UID, NEWZBIN_PASSWORD, LASTFM_USERNAME, INTERFACE, FOLDER_PERMISSIONS, \
|
||||
ENCODERFOLDER, ENCODER, BITRATE, SAMPLINGFREQUENCY, MUSIC_ENCODER, ADVANCEDENCODER, ENCODEROUTPUTFORMAT, ENCODERQUALITY, ENCODERVBRCBR, \
|
||||
ENCODERLOSSLESS, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, MIRRORLIST, MIRROR, CUSTOMHOST, CUSTOMPORT, \
|
||||
ENCODERLOSSLESS, DELETE_LOSSLESS_FILES, PROWL_ENABLED, PROWL_PRIORITY, PROWL_KEYS, PROWL_ONSNATCH, MIRRORLIST, MIRROR, CUSTOMHOST, CUSTOMPORT, \
|
||||
CUSTOMSLEEP, HPUSER, HPPASS, XBMC_ENABLED, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, XBMC_UPDATE, XBMC_NOTIFY, NMA_ENABLED, NMA_APIKEY, NMA_PRIORITY, SYNOINDEX_ENABLED, \
|
||||
ALBUM_COMPLETION_PCT
|
||||
|
||||
@@ -295,6 +298,7 @@ def initialize():
|
||||
|
||||
MUSIC_DIR = check_setting_str(CFG, 'General', 'music_dir', '')
|
||||
DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '')
|
||||
LOSSLESS_DESTINATION_DIR = check_setting_str(CFG, 'General', 'lossless_destination_dir', '')
|
||||
PREFERRED_QUALITY = check_setting_int(CFG, 'General', 'preferred_quality', 0)
|
||||
PREFERRED_BITRATE = check_setting_int(CFG, 'General', 'preferred_bitrate', '')
|
||||
DETECT_BITRATE = bool(check_setting_int(CFG, 'General', 'detect_bitrate', 0))
|
||||
@@ -373,6 +377,7 @@ def initialize():
|
||||
ENCODERQUALITY = check_setting_int(CFG, 'General', 'encoderquality', 2)
|
||||
ENCODERVBRCBR = check_setting_str(CFG, 'General', 'encodervbrcbr', 'cbr')
|
||||
ENCODERLOSSLESS = bool(check_setting_int(CFG, 'General', 'encoderlossless', 1))
|
||||
DELETE_LOSSLESS_FILES = bool(check_setting_int(CFG, 'General', 'delete_lossless_files', 1))
|
||||
|
||||
PROWL_ENABLED = bool(check_setting_int(CFG, 'Prowl', 'prowl_enabled', 0))
|
||||
PROWL_KEYS = check_setting_str(CFG, 'Prowl', 'prowl_keys', '')
|
||||
@@ -570,6 +575,7 @@ def config_write():
|
||||
|
||||
new_config['General']['music_dir'] = MUSIC_DIR
|
||||
new_config['General']['destination_dir'] = DESTINATION_DIR
|
||||
new_config['General']['lossless_destination_dir'] = LOSSLESS_DESTINATION_DIR
|
||||
new_config['General']['preferred_quality'] = PREFERRED_QUALITY
|
||||
new_config['General']['preferred_bitrate'] = PREFERRED_BITRATE
|
||||
new_config['General']['detect_bitrate'] = int(DETECT_BITRATE)
|
||||
@@ -677,7 +683,8 @@ def config_write():
|
||||
new_config['General']['encoderoutputformat'] = ENCODEROUTPUTFORMAT
|
||||
new_config['General']['encoderquality'] = ENCODERQUALITY
|
||||
new_config['General']['encodervbrcbr'] = ENCODERVBRCBR
|
||||
new_config['General']['encoderlossless'] = ENCODERLOSSLESS
|
||||
new_config['General']['encoderlossless'] = int(ENCODERLOSSLESS)
|
||||
new_config['General']['delete_lossless_files'] = int(DELETE_LOSSLESS_FILES)
|
||||
|
||||
new_config['General']['mirror'] = MIRROR
|
||||
new_config['General']['customhost'] = CUSTOMHOST
|
||||
@@ -719,10 +726,12 @@ def dbcheck():
|
||||
conn=sqlite3.connect(DB_FILE)
|
||||
c=conn.cursor()
|
||||
c.execute('CREATE TABLE IF NOT EXISTS artists (ArtistID TEXT UNIQUE, ArtistName TEXT, ArtistSortName TEXT, DateAdded TEXT, Status TEXT, IncludeExtras INTEGER, LatestAlbum TEXT, ReleaseDate TEXT, AlbumID TEXT, HaveTracks INTEGER, TotalTracks INTEGER, LastUpdated TEXT, ArtworkURL TEXT, ThumbURL TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT, ArtworkURL TEXT, ThumbURL TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS albums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, DateAdded TEXT, AlbumID TEXT UNIQUE, Status TEXT, Type TEXT, ArtworkURL TEXT, ThumbURL TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT)') # ReleaseFormat here means CD,Digital,Vinyl, etc. If using the default Headphones hybrid release, ReleaseID will equal AlbumID (AlbumID is releasegroup id)
|
||||
c.execute('CREATE TABLE IF NOT EXISTS tracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)') # Format here means mp3, flac, etc.
|
||||
c.execute('CREATE TABLE IF NOT EXISTS allalbums (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, ReleaseDate TEXT, AlbumID TEXT, Type TEXT, ReleaseID TEXT, ReleaseCountry TEXT, ReleaseFormat TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS alltracks (ArtistID TEXT, ArtistName TEXT, AlbumTitle TEXT, AlbumASIN TEXT, AlbumID TEXT, TrackTitle TEXT, TrackDuration, TrackID TEXT, TrackNumber INTEGER, Location TEXT, BitRate INTEGER, CleanName TEXT, Format TEXT, ReleaseID TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS snatched (AlbumID TEXT, Title TEXT, Size INTEGER, URL TEXT, DateAdded TEXT, Status TEXT, FolderName TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS have (ArtistName TEXT, AlbumTitle TEXT, TrackNumber TEXT, TrackTitle TEXT, TrackLength TEXT, BitRate TEXT, Genre TEXT, Date TEXT, TrackID TEXT, Location TEXT, CleanName TEXT, Format TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS have (ArtistName TEXT, AlbumTitle TEXT, TrackNumber TEXT, TrackTitle TEXT, TrackLength TEXT, BitRate TEXT, Genre TEXT, Date TEXT, TrackID TEXT, Location TEXT, CleanName TEXT, Format TEXT, Matched TEXT)') # Matched is a temporary value used to see if there was a match found in alltracks
|
||||
c.execute('CREATE TABLE IF NOT EXISTS lastfmcloud (ArtistName TEXT, ArtistID TEXT, Count INTEGER)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS descriptions (ArtistID TEXT, ReleaseGroupID TEXT, ReleaseID TEXT, Summary TEXT, Content TEXT, LastUpdated TEXT)')
|
||||
c.execute('CREATE TABLE IF NOT EXISTS blacklist (ArtistID TEXT UNIQUE)')
|
||||
@@ -846,6 +855,31 @@ def dbcheck():
|
||||
c.execute('SELECT LastUpdated from descriptions')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE descriptions ADD COLUMN LastUpdated TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT ReleaseID from albums')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE albums ADD COLUMN ReleaseID TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT ReleaseFormat from albums')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE albums ADD COLUMN ReleaseFormat TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT ReleaseCountry from albums')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE albums ADD COLUMN ReleaseCountry TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT ReleaseID from tracks')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE tracks ADD COLUMN ReleaseID TEXT DEFAULT NULL')
|
||||
|
||||
try:
|
||||
c.execute('SELECT Matched from have')
|
||||
except sqlite3.OperationalError:
|
||||
c.execute('ALTER TABLE have ADD COLUMN Matched TEXT DEFAULT NULL')
|
||||
|
||||
conn.commit()
|
||||
c.close()
|
||||
|
||||
@@ -37,8 +37,12 @@ def getCachedArt(albumid):
|
||||
return None
|
||||
|
||||
if artwork_path.startswith('http://'):
|
||||
artwork = urllib2.urlopen(artwork_path, timeout=20).read()
|
||||
return artwork
|
||||
try:
|
||||
artwork = urllib2.urlopen(artwork_path, timeout=20).read()
|
||||
return artwork
|
||||
except:
|
||||
logger.warn("Unable to open url: " + artwork_path)
|
||||
return None
|
||||
else:
|
||||
artwork = open(artwork_path, "r").read()
|
||||
return artwork
|
||||
|
||||
82
headphones/albumswitcher.py
Normal file
82
headphones/albumswitcher.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# This file is part of Headphones.
|
||||
#
|
||||
# Headphones is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Headphones is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import headphones
|
||||
from headphones import db, logger
|
||||
|
||||
def switch(AlbumID, ReleaseID):
|
||||
'''
|
||||
Takes the contents from allalbums & alltracks (based on ReleaseID) and switches them into
|
||||
the albums & tracks table.
|
||||
'''
|
||||
myDB = db.DBConnection()
|
||||
oldalbumdata = myDB.action('SELECT * from albums WHERE AlbumID=?', [AlbumID]).fetchone()
|
||||
newalbumdata = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [ReleaseID]).fetchone()
|
||||
newtrackdata = myDB.action('SELECT * from alltracks WHERE ReleaseID=?', [ReleaseID]).fetchall()
|
||||
myDB.action('DELETE from tracks WHERE AlbumID=?', [AlbumID])
|
||||
|
||||
controlValueDict = {"AlbumID": AlbumID}
|
||||
|
||||
newValueDict = {"ArtistID": newalbumdata['ArtistID'],
|
||||
"ArtistName": newalbumdata['ArtistName'],
|
||||
"AlbumTitle": newalbumdata['AlbumTitle'],
|
||||
"ReleaseID": newalbumdata['ReleaseID'],
|
||||
"AlbumASIN": newalbumdata['AlbumASIN'],
|
||||
"ReleaseDate": newalbumdata['ReleaseDate'],
|
||||
"Type": newalbumdata['Type'],
|
||||
"ReleaseCountry": newalbumdata['ReleaseCountry'],
|
||||
"ReleaseFormat": newalbumdata['ReleaseFormat']
|
||||
}
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
for track in newtrackdata:
|
||||
|
||||
controlValueDict = {"TrackID": track['TrackID'],
|
||||
"AlbumID": AlbumID}
|
||||
|
||||
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']
|
||||
}
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
|
||||
total_track_count = len(newtrackdata)
|
||||
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.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=?', [newalbumdata['ArtistID']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [newalbumdata['ArtistID']]))
|
||||
|
||||
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
|
||||
|
||||
newValueDict = { "TotalTracks": totaltracks,
|
||||
"HaveTracks": havetracks}
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
@@ -229,7 +229,7 @@ class Cache(object):
|
||||
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No artist image found on url: ' + url)
|
||||
image_url = None
|
||||
|
||||
@@ -266,11 +266,12 @@ class Cache(object):
|
||||
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No artist image found on url: ' + url)
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
|
||||
if not thumb_url:
|
||||
logger.debug('No artist thumbnail image found on url: ' + url)
|
||||
|
||||
@@ -310,17 +311,17 @@ class Cache(object):
|
||||
return
|
||||
try:
|
||||
self.info_summary = data['artist']['bio']['summary']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No artist bio summary found on url: ' + url)
|
||||
self.info_summary = None
|
||||
try:
|
||||
self.info_content = data['artist']['bio']['content']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No artist bio found on url: ' + url)
|
||||
self.info_content = None
|
||||
try:
|
||||
image_url = data['artist']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No artist image found on url: ' + url)
|
||||
image_url = None
|
||||
|
||||
@@ -356,21 +357,22 @@ class Cache(object):
|
||||
return
|
||||
try:
|
||||
self.info_summary = data['album']['wiki']['summary']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No album summary found from: ' + url)
|
||||
self.info_summary = None
|
||||
try:
|
||||
self.info_content = data['album']['wiki']['content']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No album infomation found from: ' + url)
|
||||
self.info_content = None
|
||||
try:
|
||||
image_url = data['album']['image'][-1]['#text']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
logger.debug('No album image link found on url: ' + url)
|
||||
image_url = None
|
||||
|
||||
thumb_url = self._get_thumb_url(data)
|
||||
|
||||
if not thumb_url:
|
||||
logger.debug('No album thumbnail image found on url: ' + url)
|
||||
|
||||
@@ -402,7 +404,6 @@ class Cache(object):
|
||||
|
||||
# Should we grab the artwork here if we're just grabbing thumbs or info?? Probably not since the files can be quite big
|
||||
if image_url and self.query_type == 'artwork':
|
||||
|
||||
try:
|
||||
artwork = urllib2.urlopen(image_url, timeout=20).read()
|
||||
except Exception, e:
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import os, time
|
||||
from operator import itemgetter
|
||||
import datetime
|
||||
import re
|
||||
import re, shutil
|
||||
|
||||
import headphones
|
||||
|
||||
@@ -218,3 +218,35 @@ def extract_song_data(s):
|
||||
else:
|
||||
logger.info("Couldn't parse " + s + " into a valid Newbin format")
|
||||
return (name, album, year)
|
||||
|
||||
def smartMove(src, dest, delete=True):
|
||||
|
||||
source_dir = os.path.dirname(src)
|
||||
filename = os.path.basename(src)
|
||||
|
||||
if os.path.isfile(os.path.join(dest, filename)):
|
||||
logger.info('Destination file exists: %s' % os.path.join(dest, filename).decode(headphones.SYS_ENCODING))
|
||||
title = os.path.splitext(filename)[0]
|
||||
ext = os.path.splitext(filename)[1]
|
||||
i = 1
|
||||
while True:
|
||||
newfile = title + '(' + str(i) + ')' + ext
|
||||
if os.path.isfile(os.path.join(dest, newfile)):
|
||||
i += 1
|
||||
else:
|
||||
logger.info('Renaming to %s' % newfile)
|
||||
try:
|
||||
os.rename(src, os.path.join(source_dir, newfile))
|
||||
filename = newfile
|
||||
except Exception, e:
|
||||
logger.warn('Error renaming %s: %s' % (src.decode(headphones.SYS_ENCODING), e))
|
||||
break
|
||||
|
||||
try:
|
||||
if delete:
|
||||
shutil.move(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
||||
else:
|
||||
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
||||
return True
|
||||
except Exception, e:
|
||||
logger.warn('Error moving file %s: %s' % (filename.decode(headphones.SYS_ENCODING), e))
|
||||
|
||||
@@ -153,68 +153,132 @@ def addArtisttoDB(artistid, extrasonly=False):
|
||||
|
||||
for rg in artist['releasegroups']:
|
||||
|
||||
logger.info("Now adding/updating: " + rg['title'])
|
||||
|
||||
rgid = rg['id']
|
||||
|
||||
# check if the album already exists
|
||||
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
|
||||
|
||||
try:
|
||||
release_dict = mb.getReleaseGroup(rgid)
|
||||
releaselist = mb.getReleaseGroup(rgid)
|
||||
except Exception, e:
|
||||
logger.info('Unable to get release information for %s - there may not be any official releases in this release group' % rg['title'])
|
||||
continue
|
||||
|
||||
if not release_dict:
|
||||
if not releaselist:
|
||||
continue
|
||||
|
||||
logger.info(u"Now adding/updating album: " + rg['title'])
|
||||
|
||||
# This will be used later to build a hybrid release
|
||||
fullreleaselist = []
|
||||
|
||||
for release in releaselist:
|
||||
# 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
|
||||
|
||||
releaseid = release['id']
|
||||
|
||||
try:
|
||||
releasedict = mb.getRelease(releaseid, include_artist_info=False)
|
||||
except Exception, e:
|
||||
logger.info('Unable to get release information for %s: %s' % (release['id'], e))
|
||||
continue
|
||||
|
||||
if not releasedict:
|
||||
continue
|
||||
|
||||
controlValueDict = {"AlbumID": rg['id']}
|
||||
controlValueDict = {"ReleaseID": release['id']}
|
||||
|
||||
newValueDict = {"ArtistID": artistid,
|
||||
"ArtistName": artist['artist_name'],
|
||||
"AlbumTitle": rg['title'],
|
||||
"AlbumID": rg['id'],
|
||||
"AlbumASIN": releasedict['asin'],
|
||||
"ReleaseDate": releasedict['date'],
|
||||
"Type": rg['type'],
|
||||
"ReleaseCountry": releasedict['country'],
|
||||
"ReleaseFormat": releasedict['format']
|
||||
}
|
||||
|
||||
myDB.upsert("allalbums", newValueDict, controlValueDict)
|
||||
|
||||
# Build the dictionary for the fullreleaselist
|
||||
newValueDict['ReleaseID'] = release['id']
|
||||
newValueDict['Tracks'] = releasedict['tracks']
|
||||
fullreleaselist.append(newValueDict)
|
||||
|
||||
for track in releasedict['tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": release['id']}
|
||||
|
||||
newValueDict = {"ArtistID": artistid,
|
||||
"ArtistName": artist['artist_name'],
|
||||
"AlbumTitle": rg['title'],
|
||||
"AlbumASIN": releasedict['asin'],
|
||||
"AlbumID": rg['id'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
|
||||
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()
|
||||
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.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
# Basically just do the same thing again for the hybrid release
|
||||
hybridrelease = getHybridRelease(fullreleaselist)
|
||||
|
||||
# Use the ReleaseGroupID as the ReleaseID for the hybrid release to differentiate it
|
||||
# We can then use the condition WHERE ReleaseID == ReleaseGroupID to select it
|
||||
# The hybrid won't have a country or a format
|
||||
controlValueDict = {"ReleaseID": rg['id']}
|
||||
|
||||
newValueDict = {"ArtistID": artistid,
|
||||
"ArtistName": artist['artist_name'],
|
||||
"AlbumTitle": rg['title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"ReleaseDate": release_dict['releasedate'],
|
||||
"AlbumID": rg['id'],
|
||||
"AlbumASIN": hybridrelease['AlbumASIN'],
|
||||
"ReleaseDate": hybridrelease['ReleaseDate'],
|
||||
"Type": rg['type']
|
||||
}
|
||||
}
|
||||
|
||||
myDB.upsert("allalbums", newValueDict, controlValueDict)
|
||||
|
||||
# Only change the status & add DateAdded if the album is not already in the database
|
||||
if not rg_exists:
|
||||
for track in hybridrelease['Tracks']:
|
||||
|
||||
newValueDict['DateAdded']= helpers.today()
|
||||
|
||||
if headphones.AUTOWANT_ALL:
|
||||
newValueDict['Status'] = "Wanted"
|
||||
elif release_dict['releasedate'] > helpers.today() and headphones.AUTOWANT_UPCOMING:
|
||||
newValueDict['Status'] = "Wanted"
|
||||
else:
|
||||
newValueDict['Status'] = "Skipped"
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
# 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 ALBUM_COMPLETION_PCT
|
||||
total_track_count = len(release_dict['tracks'])
|
||||
|
||||
for track in release_dict['tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(artist['artist_name'] + ' ' + rg['title'] + ' ' + track['title'])
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"AlbumID": rg['id']}
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": rg['id']}
|
||||
|
||||
newValueDict = {"ArtistID": artistid,
|
||||
"ArtistName": artist['artist_name'],
|
||||
"AlbumTitle": rg['title'],
|
||||
"AlbumASIN": release_dict['asin'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
newValueDict = {"ArtistID": artistid,
|
||||
"ArtistName": artist['artist_name'],
|
||||
"AlbumTitle": rg['title'],
|
||||
"AlbumASIN": hybridrelease['AlbumASIN'],
|
||||
"AlbumID": rg['id'],
|
||||
"TrackTitle": track['title'],
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
@@ -223,8 +287,94 @@ def addArtisttoDB(artistid, extrasonly=False):
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
myDB.action('DELETE from have WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
# Delete matched tracks from the have table
|
||||
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)
|
||||
if not rg_exists:
|
||||
releaseid = rg['id']
|
||||
elif rg_exists and not rg_exists['ReleaseID']:
|
||||
# Need to do some importing here - to transition the old format of using the release group
|
||||
# only to using releasegroup & releaseid. These are the albums that are missing a ReleaseID
|
||||
# so we'll need to move over the locations, bitrates & formats from the tracks table to the new
|
||||
# alltracks table. Thankfully we can just use TrackIDs since they span releases/releasegroups
|
||||
logger.info("Copying current track information to alternate releases")
|
||||
tracks = myDB.action('SELECT * from tracks WHERE AlbumID=?', [rg['id']]).fetchall()
|
||||
for track in tracks:
|
||||
if track['Location']:
|
||||
controlValueDict = {"TrackID": track['TrackID']}
|
||||
newValueDict = {"Location": track['Location'],
|
||||
"BitRate": track['BitRate'],
|
||||
"Format": track['Format'],
|
||||
}
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
releaseid = rg['id']
|
||||
else:
|
||||
releaseid = rg_exists['ReleaseID']
|
||||
|
||||
album = myDB.action('SELECT * from allalbums WHERE ReleaseID=?', [releaseid]).fetchone()
|
||||
|
||||
controlValueDict = {"AlbumID": rg['id']}
|
||||
|
||||
newValueDict = {"ArtistID": album['ArtistID'],
|
||||
"ArtistName": album['ArtistName'],
|
||||
"AlbumTitle": album['AlbumTitle'],
|
||||
"ReleaseID": album['ReleaseID'],
|
||||
"AlbumASIN": album['AlbumASIN'],
|
||||
"ReleaseDate": album['ReleaseDate'],
|
||||
"Type": album['Type'],
|
||||
"ReleaseCountry": album['ReleaseCountry'],
|
||||
"ReleaseFormat": album['ReleaseFormat']
|
||||
}
|
||||
|
||||
if not rg_exists:
|
||||
|
||||
newValueDict['DateAdded']= helpers.today()
|
||||
|
||||
if headphones.AUTOWANT_ALL:
|
||||
newValueDict['Status'] = "Wanted"
|
||||
|
||||
#start a search for the album
|
||||
import searcher
|
||||
searcher.searchforalbum(albumid=rg['id'])
|
||||
|
||||
elif album['ReleaseDate'] > helpers.today() and headphones.AUTOWANT_UPCOMING:
|
||||
newValueDict['Status'] = "Wanted"
|
||||
else:
|
||||
newValueDict['Status'] = "Skipped"
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
myDB.action('DELETE from tracks WHERE AlbumID=?', [rg['id']])
|
||||
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 ALBUM_COMPLETION_PCT
|
||||
total_track_count = len(tracks)
|
||||
|
||||
for track in tracks:
|
||||
|
||||
controlValueDict = {"TrackID": track['TrackID'],
|
||||
"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']
|
||||
}
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
|
||||
@@ -237,7 +387,7 @@ def addArtisttoDB(artistid, extrasonly=False):
|
||||
if ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', rg['id']])
|
||||
|
||||
logger.debug(u"Updating album cache for " + rg['title'])
|
||||
logger.info(u"Seeing if we need album art for " + rg['title'])
|
||||
cache.getThumb(AlbumID=rg['id'])
|
||||
|
||||
latestalbum = myDB.action('SELECT AlbumTitle, ReleaseDate, AlbumID from albums WHERE ArtistID=? order by ReleaseDate DESC', [artistid]).fetchone()
|
||||
@@ -262,7 +412,7 @@ def addArtisttoDB(artistid, extrasonly=False):
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
logger.debug(u"Updating cache for: " + artist['artist_name'])
|
||||
logger.info(u"Seeing if we need album art for: " + artist['artist_name'])
|
||||
cache.getThumb(ArtistID=artistid)
|
||||
|
||||
logger.info(u"Updating complete for: " + artist['artist_name'])
|
||||
@@ -377,7 +527,7 @@ def addReleaseById(rid):
|
||||
|
||||
#start a search for the album
|
||||
import searcher
|
||||
searcher.searchNZB(rgid, False)
|
||||
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.")
|
||||
return
|
||||
@@ -412,3 +562,76 @@ def updateFormat():
|
||||
newValueDict = {"Format": f.format}
|
||||
myDB.upsert("have", newValueDict, controlValueDict)
|
||||
logger.info('Finished finding media format for %s files' % len(havetracks))
|
||||
|
||||
def getHybridRelease(fullreleaselist):
|
||||
"""
|
||||
Returns a dictionary of best group of tracks from the list of releases & earliest release date
|
||||
"""
|
||||
sortable_release_list = []
|
||||
|
||||
for release in fullreleaselist:
|
||||
|
||||
formats = {
|
||||
'2xVinyl': '2',
|
||||
'Vinyl': '2',
|
||||
'CD': '0',
|
||||
'Cassette': '3',
|
||||
'2xCD': '1',
|
||||
'Digital Media': '0'
|
||||
}
|
||||
|
||||
countries = {
|
||||
'US': '0',
|
||||
'GB': '1',
|
||||
'JP': '2',
|
||||
}
|
||||
|
||||
try:
|
||||
format = int(formats[release['Format']])
|
||||
except:
|
||||
format = 3
|
||||
|
||||
try:
|
||||
country = int(countries[release['Country']])
|
||||
except:
|
||||
country = 3
|
||||
|
||||
release_dict = {
|
||||
'hasasin': bool(release['AlbumASIN']),
|
||||
'asin': release['AlbumASIN'],
|
||||
'trackscount': len(release['Tracks']),
|
||||
'releaseid': release['ReleaseID'],
|
||||
'releasedate': release['ReleaseDate'],
|
||||
'format': format,
|
||||
'country': country,
|
||||
'tracks': release['Tracks']
|
||||
}
|
||||
|
||||
sortable_release_list.append(release_dict)
|
||||
|
||||
#necessary to make dates that miss the month and/or day show up after full dates
|
||||
def getSortableReleaseDate(releaseDate):
|
||||
if releaseDate == None:
|
||||
return 'None';#change this value to change the sorting behaviour of none, returning 'None' will put it at the top
|
||||
#which was normal behaviour for pre-ngs versions
|
||||
if releaseDate.count('-') == 2:
|
||||
return releaseDate
|
||||
elif releaseDate.count('-') == 1:
|
||||
return releaseDate + '32'
|
||||
else:
|
||||
return releaseDate + '13-32'
|
||||
|
||||
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))
|
||||
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'])
|
||||
|
||||
release_dict = {'ReleaseDate' : sortable_release_list[0]['releasedate'],
|
||||
'Tracks' : a[0]['tracks'],
|
||||
'AlbumASIN' : a[0]['asin']
|
||||
}
|
||||
|
||||
return release_dict
|
||||
|
||||
@@ -42,7 +42,7 @@ def getSimilar():
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
if len(data) < 200:
|
||||
if not data or len(data) < 200:
|
||||
continue
|
||||
|
||||
try:
|
||||
|
||||
@@ -21,12 +21,16 @@ from lib.beets.mediafile import MediaFile
|
||||
import headphones
|
||||
from headphones import db, logger, helpers, importer
|
||||
|
||||
def libraryScan(dir=None):
|
||||
# You can scan a single directory and append it to the current library by specifying append=True, ArtistID & ArtistName
|
||||
def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None):
|
||||
|
||||
if not dir:
|
||||
dir = headphones.MUSIC_DIR
|
||||
|
||||
dir = dir.encode(headphones.SYS_ENCODING)
|
||||
|
||||
# If we're appending a dir, it's coming from the post processor which is
|
||||
# already bytestring
|
||||
if not append:
|
||||
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))
|
||||
@@ -34,19 +38,22 @@ def libraryScan(dir=None):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
# Clean up bad filepaths
|
||||
tracks = myDB.select('SELECT Location, TrackID from tracks WHERE Location IS NOT NULL')
|
||||
if not append:
|
||||
# Clean up bad filepaths
|
||||
tracks = myDB.select('SELECT Location, TrackID from tracks WHERE Location IS NOT NULL')
|
||||
|
||||
for track in tracks:
|
||||
if not os.path.isfile(track['Location'].encode(headphones.SYS_ENCODING)):
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE TrackID=?', [None, None, None, track['TrackID']])
|
||||
for track in tracks:
|
||||
if not os.path.isfile(track['Location'].encode(headphones.SYS_ENCODING)):
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE TrackID=?', [None, None, None, track['TrackID']])
|
||||
|
||||
myDB.action('DELETE from have')
|
||||
|
||||
logger.info('Scanning music directory: %s' % dir)
|
||||
|
||||
new_artists = []
|
||||
bitrates = []
|
||||
|
||||
myDB.action('DELETE from have')
|
||||
|
||||
song_list = []
|
||||
|
||||
for r,d,f in os.walk(dir):
|
||||
for files in f:
|
||||
@@ -69,69 +76,261 @@ def libraryScan(dir=None):
|
||||
# Grab the bitrates for the auto detect bit rate option
|
||||
if f.bitrate:
|
||||
bitrates.append(f.bitrate)
|
||||
|
||||
# Try to find a match based on artist/album/tracktitle
|
||||
|
||||
# Use the album artist over the artist if available
|
||||
if f.albumartist:
|
||||
f_artist = f.albumartist
|
||||
elif f.artist:
|
||||
f_artist = f.artist
|
||||
else:
|
||||
continue
|
||||
|
||||
if f_artist and f.album and f.title:
|
||||
|
||||
track = myDB.action('SELECT TrackID from tracks WHERE CleanName LIKE ?', [helpers.cleanName(f_artist +' '+f.album+' '+f.title)]).fetchone()
|
||||
|
||||
if not track:
|
||||
track = myDB.action('SELECT TrackID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [f_artist, f.album, f.title]).fetchone()
|
||||
f_artist = None
|
||||
|
||||
if track:
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE TrackID=?', [unicode_song_path, f.bitrate, f.format, track['TrackID']])
|
||||
continue
|
||||
|
||||
# Try to match on mbid if available and we couldn't find a match based on metadata
|
||||
if f.mb_trackid:
|
||||
# Add the song to our song list -
|
||||
# TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements)
|
||||
|
||||
# Wondering if theres a better way to do this -> do one thing if the row exists,
|
||||
# do something else if it doesn't
|
||||
track = myDB.action('SELECT TrackID from tracks WHERE TrackID=?', [f.mb_trackid]).fetchone()
|
||||
song_dict = { '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,
|
||||
'Location' : unicode_song_path }
|
||||
|
||||
song_list.append(song_dict)
|
||||
|
||||
# Now we start track matching
|
||||
total_number_of_songs = len(song_list)
|
||||
logger.info("Found " + str(total_number_of_songs) + " tracks in: '" + dir + "'. 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
|
||||
|
||||
song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
|
||||
|
||||
# We'll use this to give a % completion, just because the track matching might take a while
|
||||
song_count = 0
|
||||
|
||||
for song in song_list:
|
||||
|
||||
if track:
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE TrackID=?', [unicode_song_path, f.bitrate, f.format, track['TrackID']])
|
||||
continue
|
||||
|
||||
# if we can't find a match in the database on a track level, it might be a new artist or it might be on a non-mb release
|
||||
new_artists.append(f_artist)
|
||||
|
||||
# The have table will become the new database for unmatched tracks (i.e. tracks with no associated links in the database
|
||||
myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [f_artist, f.album, f.track, f.title, f.length, f.bitrate, f.genre, f.date, f.mb_trackid, unicode_song_path, helpers.cleanName(f_artist+' '+f.album+' '+f.title), f.format])
|
||||
|
||||
logger.info('Completed scanning directory: %s' % dir)
|
||||
|
||||
# Clean up the new artist list
|
||||
unique_artists = {}.fromkeys(new_artists).keys()
|
||||
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
|
||||
|
||||
artist_list = [f for f in unique_artists if f.lower() not in [x[0].lower() for x in current_artists]]
|
||||
|
||||
# Update track counts
|
||||
logger.info('Updating track counts')
|
||||
|
||||
for artist in current_artists:
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID like ? AND Location IS NOT NULL', [artist['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['ArtistName']]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artist['ArtistID']])
|
||||
song_count += 1
|
||||
completion_percentage = float(song_count)/total_number_of_songs * 100
|
||||
|
||||
logger.info('Found %i new artists' % len(artist_list))
|
||||
if completion_percentage%10 == 0:
|
||||
logger.info("Track matching is " + str(completion_percentage) + "% complete")
|
||||
|
||||
# If the track has a trackid & releaseid (beets: albumid) that the most surefire way
|
||||
# of identifying a track to a specific release so we'll use that first
|
||||
if song['TrackID'] and song['ReleaseID']:
|
||||
|
||||
if len(artist_list):
|
||||
if headphones.ADD_ARTISTS:
|
||||
logger.info('Importing %i new artists' % len(artist_list))
|
||||
importer.artistlist_to_mbids(artist_list)
|
||||
else:
|
||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||
myDB.action('DELETE from newartists')
|
||||
for artist in artist_list:
|
||||
myDB.action('INSERT into newartists VALUES (?)', [artist])
|
||||
# Check both the tracks table & alltracks table in case they haven't populated the alltracks table yet
|
||||
track = myDB.action('SELECT TrackID, ReleaseID, AlbumID from alltracks WHERE TrackID=? AND ReleaseID=?', [song['TrackID'], song['ReleaseID']]).fetchone()
|
||||
|
||||
# It might be the case that the alltracks table isn't populated yet, so maybe we can only find a match in the tracks table
|
||||
if not track:
|
||||
track = myDB.action('SELECT TrackID, ReleaseID, AlbumID from tracks WHERE TrackID=? AND ReleaseID=?', [song['TrackID'], song['ReleaseID']]).fetchone()
|
||||
|
||||
if track:
|
||||
# Use TrackID & ReleaseID here since there can only be one possible match with a TrackID & ReleaseID query combo
|
||||
controlValueDict = { 'TrackID' : track['TrackID'],
|
||||
'ReleaseID' : track['ReleaseID'] }
|
||||
|
||||
# Insert it into the Headphones hybrid release (ReleaseID == AlbumID)
|
||||
hybridControlValueDict = { 'TrackID' : track['TrackID'],
|
||||
'ReleaseID' : track['AlbumID'] }
|
||||
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
|
||||
# Update both the tracks table and the alltracks table using the controlValueDict and hybridControlValueDict
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, hybridControlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, hybridControlValueDict)
|
||||
|
||||
# Matched. Move on to the next one:
|
||||
continue
|
||||
|
||||
# If we can't find it with TrackID & ReleaseID, next most specific will be
|
||||
# releaseid + tracktitle, although perhaps less reliable due to a higher
|
||||
# likelihood of variations in the song title (e.g. feat. artists)
|
||||
if song['ReleaseID'] and song['TrackTitle']:
|
||||
|
||||
track = myDB.action('SELECT TrackID, ReleaseID, AlbumID from alltracks WHERE ReleaseID=? AND TrackTitle=?', [song['ReleaseID'], song['TrackTitle']]).fetchone()
|
||||
|
||||
if not track:
|
||||
track = myDB.action('SELECT TrackID, ReleaseID, AlbumID from tracks WHERE ReleaseID=? AND TrackTitle=?', [song['ReleaseID'], song['TrackTitle']]).fetchone()
|
||||
|
||||
if track:
|
||||
# There can also only be one match for this query as well (although it might be on both the tracks and alltracks table)
|
||||
# So use both TrackID & ReleaseID as the control values
|
||||
controlValueDict = { 'TrackID' : track['TrackID'],
|
||||
'ReleaseID' : track['ReleaseID'] }
|
||||
|
||||
hybridControlValueDict = { 'TrackID' : track['TrackID'],
|
||||
'ReleaseID' : track['AlbumID'] }
|
||||
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
|
||||
# Update both tables here as well
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, hybridControlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, hybridControlValueDict)
|
||||
|
||||
# Done
|
||||
continue
|
||||
|
||||
# Next most specific will be the opposite: a TrackID and an AlbumTitle
|
||||
# TrackIDs span multiple releases so if something is on an official album
|
||||
# and a compilation, for example, this will match it to the right one
|
||||
# However - there may be multiple matches here
|
||||
if song['TrackID'] and song['AlbumTitle']:
|
||||
|
||||
# Even though there might be multiple matches, we just need to grab one to confirm a match
|
||||
track = myDB.action('SELECT TrackID, AlbumTitle from alltracks WHERE TrackID=? AND AlbumTitle LIKE ?', [song['TrackID'], song['AlbumTitle']]).fetchone()
|
||||
|
||||
if not track:
|
||||
track = myDB.action('SELECT TrackID, AlbumTitle from tracks WHERE TrackID=? AND AlbumTitle LIKE ?', [song['TrackID'], song['AlbumTitle']]).fetchone()
|
||||
|
||||
if track:
|
||||
# Don't need the hybridControlValueDict here since ReleaseID is not unique
|
||||
controlValueDict = { 'TrackID' : track['TrackID'],
|
||||
'AlbumTitle' : track['AlbumTitle'] }
|
||||
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
continue
|
||||
|
||||
# Next most specific is the ArtistName + AlbumTitle + TrackTitle combo (but probably
|
||||
# even more unreliable than the previous queries, and might span multiple releases)
|
||||
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
|
||||
|
||||
track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
|
||||
if not track:
|
||||
track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
||||
|
||||
if track:
|
||||
controlValueDict = { 'ArtistName' : track['ArtistName'],
|
||||
'AlbumTitle' : track['AlbumTitle'],
|
||||
'TrackTitle' : track['TrackTitle'] }
|
||||
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
continue
|
||||
|
||||
# Use the "CleanName" (ArtistName + AlbumTitle + TrackTitle stripped of punctuation, capitalization, etc)
|
||||
# This is more reliable than the former but requires some string manipulation so we'll do it only
|
||||
# if we can't find a match with the original data
|
||||
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
|
||||
|
||||
CleanName = helpers.cleanName(song['ArtistName'] +' '+ song['AlbumTitle'] +' '+song['TrackTitle'])
|
||||
|
||||
track = myDB.action('SELECT CleanName from alltracks WHERE CleanName LIKE ?', [CleanName]).fetchone()
|
||||
|
||||
if not track:
|
||||
track = myDB.action('SELECT CleanName from tracks WHERE CleanName LIKE ?', [CleanName]).fetchone()
|
||||
|
||||
if track:
|
||||
controlValueDict = { 'CleanName' : track['CleanName'] }
|
||||
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
continue
|
||||
|
||||
# Match on TrackID alone if we can't find it using any of the above methods. This method is reliable
|
||||
# but spans multiple releases - but that's why we're putting at the beginning as a last resort. If a track
|
||||
# with more specific information exists in the library, it'll overwrite these values
|
||||
if song['TrackID']:
|
||||
|
||||
track = myDB.action('SELECT TrackID from alltracks WHERE TrackID=?', [song['TrackID']]).fetchone()
|
||||
|
||||
if not track:
|
||||
track = myDB.action('SELECT TrackID from tracks WHERE TrackID=?', [song['TrackID']]).fetchone()
|
||||
|
||||
if track:
|
||||
controlValueDict = { 'TrackID' : track['TrackID'] }
|
||||
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
continue
|
||||
|
||||
# if we can't find a match in the database on a track level, it might be a new artist or it might be on a non-mb release
|
||||
new_artists.append(song['ArtistName'])
|
||||
|
||||
# The have table will become the new database for unmatched tracks (i.e. tracks with no associated links in the database
|
||||
CleanName = helpers.cleanName(song['ArtistName'] +' '+ song['AlbumTitle'] +' '+song['TrackTitle'])
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if not append:
|
||||
# Clean up the new artist list
|
||||
unique_artists = {}.fromkeys(new_artists).keys()
|
||||
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
|
||||
|
||||
artist_list = [f for f in unique_artists if f.lower() not in [x[0].lower() for x in current_artists]]
|
||||
|
||||
# Update track counts
|
||||
logger.info('Updating current artist track counts')
|
||||
|
||||
for artist in current_artists:
|
||||
# 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 ArtistID=? AND Location IS NOT NULL', [artist['ArtistID']])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist['ArtistName']]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artist['ArtistID']])
|
||||
|
||||
logger.info('Found %i new artists' % len(artist_list))
|
||||
|
||||
if len(artist_list):
|
||||
if headphones.ADD_ARTISTS:
|
||||
logger.info('Importing %i new artists' % len(artist_list))
|
||||
importer.artistlist_to_mbids(artist_list)
|
||||
else:
|
||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||
myDB.action('DELETE from newartists')
|
||||
for artist in artist_list:
|
||||
myDB.action('INSERT into newartists VALUES (?)', [artist])
|
||||
|
||||
if headphones.DETECT_BITRATE:
|
||||
headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000
|
||||
|
||||
else:
|
||||
# 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 ?', [ArtistName]))
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||
|
||||
if headphones.DETECT_BITRATE:
|
||||
headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import urllib
|
||||
import urllib, urllib2
|
||||
from xml.dom import minidom
|
||||
import htmlentitydefs
|
||||
|
||||
@@ -30,7 +30,7 @@ def getLyrics(artist, song):
|
||||
searchURL = 'http://lyrics.wikia.com/api.php?' + urllib.urlencode(params)
|
||||
|
||||
try:
|
||||
data = urllib.urlopen(searchURL).read()
|
||||
data = urllib2.urlopen(searchURL, timeout=20).read()
|
||||
except Exception, e:
|
||||
logger.warn('Error opening: %s. Error: %s' % (searchURL, e))
|
||||
return
|
||||
|
||||
192
headphones/mb.py
192
headphones/mb.py
@@ -124,9 +124,9 @@ def findArtist(name, limit=1):
|
||||
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
|
||||
'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
|
||||
@@ -158,10 +158,10 @@ def findRelease(name, limit=1):
|
||||
'uniquename': unicode(result['artist-credit'][0]['artist']['name']),
|
||||
'title': unicode(result['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'])
|
||||
'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'])
|
||||
})
|
||||
return releaselistngs
|
||||
|
||||
@@ -215,8 +215,7 @@ def getArtist(artistid, extrasonly=False):
|
||||
# if 'end' in artist['life-span']:
|
||||
# artist_dict['artist_enddate'] = unicode(artist['life-span']['end'])
|
||||
|
||||
|
||||
|
||||
|
||||
releasegroups = []
|
||||
|
||||
if not extrasonly:
|
||||
@@ -224,10 +223,10 @@ def getArtist(artistid, extrasonly=False):
|
||||
if rg['type'] != 'Album': #only add releases without a secondary type
|
||||
continue
|
||||
releasegroups.append({
|
||||
'title': unicode(rg['title']),
|
||||
'id': unicode(rg['id']),
|
||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
||||
'type': unicode(rg['type'])
|
||||
'title': unicode(rg['title']),
|
||||
'id': unicode(rg['id']),
|
||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
||||
'type': unicode(rg['type'])
|
||||
})
|
||||
|
||||
# See if we need to grab extras
|
||||
@@ -255,18 +254,18 @@ def getArtist(artistid, extrasonly=False):
|
||||
for rg in artist['release-group-list']:
|
||||
releasegroups.append({
|
||||
'title': unicode(rg['title']),
|
||||
'id': unicode(rg['id']),
|
||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
||||
'type': unicode(rg['type'])
|
||||
'id': unicode(rg['id']),
|
||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
||||
'type': unicode(rg['type'])
|
||||
})
|
||||
|
||||
artist_dict['releasegroups'] = releasegroups
|
||||
|
||||
return artist_dict
|
||||
|
||||
|
||||
def getReleaseGroup(rgid):
|
||||
"""
|
||||
Returns a dictionary of the best stuff from a release group
|
||||
Returns a list of releases in a release group
|
||||
"""
|
||||
with mb_lock:
|
||||
|
||||
@@ -284,113 +283,10 @@ def getReleaseGroup(rgid):
|
||||
|
||||
if not releaseGroup:
|
||||
return False
|
||||
|
||||
|
||||
time.sleep(sleepytime)
|
||||
|
||||
# I think for now we have to make separate queries for each release, in order
|
||||
# to get more detailed release info (ASIN, track count, etc.)
|
||||
for release in releaseGroup['release-list']:
|
||||
releaseResult = None
|
||||
|
||||
try:
|
||||
releaseResult = musicbrainzngs.get_release_by_id(release['id'],["recordings","media"])['release']
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed (%s)' % (releaseResult.title, str(e)))
|
||||
time.sleep(5)
|
||||
|
||||
if not releaseResult:
|
||||
continue
|
||||
|
||||
if releaseGroup['type'] == 'live' and releaseResult['status'] != 'Official':
|
||||
logger.debug('%s is not an official live album. Skipping' % releaseResult.name)
|
||||
continue
|
||||
|
||||
time.sleep(sleepytime)
|
||||
|
||||
formats = {
|
||||
'2xVinyl': '2',
|
||||
'Vinyl': '2',
|
||||
'CD': '0',
|
||||
'Cassette': '3',
|
||||
'2xCD': '1',
|
||||
'Digital Media': '0'
|
||||
}
|
||||
|
||||
countries = {
|
||||
'US': '0',
|
||||
|
||||
'GB': '1',
|
||||
'JP': '2',
|
||||
}
|
||||
try:
|
||||
format = int(formats[releaseResult['medium-list'][0]['format']])
|
||||
except:
|
||||
format = 3
|
||||
|
||||
try:
|
||||
country = int(countries[releaseResult['country']])
|
||||
except:
|
||||
country = 3
|
||||
totalTracks = 0
|
||||
tracks = []
|
||||
for medium in releaseResult['medium-list']:
|
||||
for track in medium['track-list']:
|
||||
tracks.append({
|
||||
'number': totalTracks + 1,
|
||||
'title': unicode(track['recording']['title']),
|
||||
'id': unicode(track['recording']['id']),
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['recording']['length'] if 'length' in track['recording'] else track['length'] if 'length' in track else 0)
|
||||
})
|
||||
totalTracks += 1
|
||||
|
||||
release_dict = {
|
||||
'hasasin': bool(releaseResult.get('asin')),
|
||||
'asin': unicode(releaseResult.get('asin')) if 'asin' in releaseResult else None,
|
||||
'trackscount': totalTracks,
|
||||
'releaseid': unicode(releaseResult.get('id')),
|
||||
'releasedate': unicode(releaseResult.get('date')) if 'date' in releaseResult else None,
|
||||
'format': format,
|
||||
'country': country
|
||||
}
|
||||
release_dict['tracks'] = tracks
|
||||
releaselist.append(release_dict)
|
||||
#necessary to make dates that miss the month and/or day show up after full dates
|
||||
def getSortableReleaseDate(releaseDate):
|
||||
if releaseDate == None:
|
||||
return 'None';#change this value to change the sorting behaviour of none, returning 'None' will put it at the top
|
||||
#which was normal behaviour for pre-ngs versions
|
||||
if releaseDate.count('-') == 2:
|
||||
return releaseDate
|
||||
elif releaseDate.count('-') == 1:
|
||||
return releaseDate + '32'
|
||||
else:
|
||||
return releaseDate + '13-32'
|
||||
|
||||
releaselist.sort(key=lambda x:getSortableReleaseDate(x['releasedate']))
|
||||
|
||||
|
||||
average_tracks = sum(x['trackscount'] for x in releaselist) / float(len(releaselist))
|
||||
for item in releaselist:
|
||||
item['trackscount_delta'] = abs(average_tracks - item['trackscount'])
|
||||
a = multikeysort(releaselist, ['-hasasin', 'country', 'format', 'trackscount_delta'])
|
||||
|
||||
release_dict = {'releaseid' :a[0]['releaseid'],
|
||||
'releasedate' : releaselist[0]['releasedate'],
|
||||
'trackcount' : a[0]['trackscount'],
|
||||
'tracks' : a[0]['tracks'],
|
||||
'asin' : a[0]['asin'],
|
||||
'releaselist' : releaselist,
|
||||
'artist_name' : unicode(releaseGroup['artist-credit'][0]['artist']['name']),
|
||||
'artist_id' : unicode(releaseGroup['artist-credit'][0]['artist']['id']),
|
||||
'title' : unicode(releaseGroup['title']),
|
||||
'type' : unicode(releaseGroup['type'])
|
||||
}
|
||||
|
||||
return release_dict
|
||||
else:
|
||||
return releaseGroup['release-list']
|
||||
|
||||
def getRelease(releaseid):
|
||||
def getRelease(releaseid, include_artist_info=True):
|
||||
"""
|
||||
Deep release search to get track info
|
||||
"""
|
||||
@@ -402,7 +298,10 @@ def getRelease(releaseid):
|
||||
q, sleepytime = startmb()
|
||||
|
||||
try:
|
||||
results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release')
|
||||
if include_artist_info:
|
||||
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')
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e)))
|
||||
time.sleep(5)
|
||||
@@ -414,30 +313,41 @@ def getRelease(releaseid):
|
||||
|
||||
release['title'] = unicode(results['title'])
|
||||
release['id'] = unicode(results['id'])
|
||||
release['asin'] = unicode(results['asin']) if 'asin' in results else None
|
||||
release['date'] = unicode(results['date'])
|
||||
release['asin'] = unicode(results['asin']) if 'asin' in results else u'None'
|
||||
release['date'] = unicode(results['date']) if 'date' in results else u'None'
|
||||
try:
|
||||
release['format'] = unicode(results['medium-list'][0]['format'])
|
||||
except:
|
||||
release['format'] = u'Unknown'
|
||||
|
||||
try:
|
||||
release['country'] = unicode(results['country'])
|
||||
except:
|
||||
release['country'] = u'Unknown'
|
||||
|
||||
|
||||
if 'release-group' in results:
|
||||
release['rgid'] = unicode(results['release-group']['id'])
|
||||
release['rg_title'] = unicode(results['release-group']['title'])
|
||||
release['rg_type'] = unicode(results['release-group']['type'])
|
||||
else:
|
||||
logger.warn("Release " + releaseid + "had no ReleaseGroup associated")
|
||||
if include_artist_info:
|
||||
|
||||
if 'release-group' in results:
|
||||
release['rgid'] = unicode(results['release-group']['id'])
|
||||
release['rg_title'] = unicode(results['release-group']['title'])
|
||||
release['rg_type'] = unicode(results['release-group']['type'])
|
||||
else:
|
||||
logger.warn("Release " + releaseid + "had no ReleaseGroup associated")
|
||||
|
||||
release['artist_name'] = unicode(results['artist-credit'][0]['artist']['name'])
|
||||
release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id'])
|
||||
release['artist_name'] = unicode(results['artist-credit'][0]['artist']['name'])
|
||||
release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id'])
|
||||
|
||||
|
||||
totalTracks = 0
|
||||
totalTracks = 1
|
||||
tracks = []
|
||||
for medium in results['medium-list']:
|
||||
for track in medium['track-list']:
|
||||
tracks.append({
|
||||
'number': totalTracks + 1,
|
||||
'title': unicode(track['recording']['title']),
|
||||
'number': totalTracks,
|
||||
'title': unicode(track['recording']['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
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['length']) if 'length' in track else 0
|
||||
})
|
||||
totalTracks += 1
|
||||
|
||||
|
||||
@@ -138,7 +138,8 @@ def command(encoder,musicSource,musicDest,albumPath):
|
||||
time.sleep(10)
|
||||
return_code = call(cmd, shell=True)
|
||||
if (return_code==0) and (os.path.exists(musicDest)):
|
||||
os.remove(musicSource)
|
||||
if headphones.DELETE_LOSSLESS_FILES:
|
||||
os.remove(musicSource)
|
||||
shutil.move(musicDest,albumPath)
|
||||
logger.info('Music "%s" encoded in %s' % (musicSource,getTimeEncode(startMusicTime)))
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from lib.beets import autotag
|
||||
from lib.beets.mediafile import MediaFile
|
||||
|
||||
import headphones
|
||||
from headphones import db, albumart, lyrics, logger, helpers
|
||||
from headphones import db, albumart, librarysync, lyrics, logger, helpers
|
||||
|
||||
postprocessor_lock = threading.Lock()
|
||||
|
||||
@@ -240,7 +240,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list)
|
||||
if len(artwork) < 100:
|
||||
logger.info("No suitable album art found from Amazon. Checking Last.FM....")
|
||||
artwork = albumart.getCachedArt(albumid)
|
||||
if len(artwork) < 100:
|
||||
if not artwork or len(artwork) < 100:
|
||||
artwork = False
|
||||
logger.info("No suitable album art found from Last.FM. Not adding album art")
|
||||
|
||||
@@ -263,27 +263,19 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list)
|
||||
renameFiles(albumpath, downloaded_track_list, release)
|
||||
|
||||
if headphones.MOVE_FILES and headphones.DESTINATION_DIR:
|
||||
albumpath = moveFiles(albumpath, release, tracks)
|
||||
albumpaths = moveFiles(albumpath, release, tracks)
|
||||
|
||||
if headphones.MOVE_FILES and not headphones.DESTINATION_DIR:
|
||||
logger.error('No DESTINATION_DIR has been set. Set "Destination Directory" to the parent directory you want to move the files to')
|
||||
pass
|
||||
|
||||
myDB = db.DBConnection()
|
||||
# There's gotta be a better way to update the have tracks - sqlite
|
||||
|
||||
trackcount = myDB.select('SELECT HaveTracks from artists WHERE ArtistID=?', [release['ArtistID']])
|
||||
|
||||
if not trackcount[0][0]:
|
||||
cur_track_count = 0
|
||||
else:
|
||||
cur_track_count = trackcount[0][0]
|
||||
|
||||
new_track_count = cur_track_count + len(downloaded_track_list)
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [new_track_count, release['ArtistID']])
|
||||
myDB.action('UPDATE albums SET status = "Downloaded" WHERE AlbumID=?', [albumid])
|
||||
myDB.action('UPDATE snatched SET status = "Processed" WHERE AlbumID=?', [albumid])
|
||||
updateHave(albumpath)
|
||||
|
||||
# Update the have tracks for all created dirs:
|
||||
for albumpath in albumpaths:
|
||||
librarysync.libraryScan(dir=albumpath, append=True, ArtistID=release['ArtistID'], ArtistName=release['ArtistName'])
|
||||
|
||||
logger.info('Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle']))
|
||||
|
||||
@@ -306,7 +298,8 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list)
|
||||
|
||||
if headphones.SYNOINDEX_ENABLED:
|
||||
syno = notifiers.Synoindex()
|
||||
syno.notify(albumpath)
|
||||
for albumpath in albumpaths:
|
||||
syno.notify(albumpath)
|
||||
|
||||
def embedAlbumArt(artwork, downloaded_track_list):
|
||||
logger.info('Embedding album art')
|
||||
@@ -384,71 +377,145 @@ def moveFiles(albumpath, release, tracks):
|
||||
|
||||
if folder.startswith('.'):
|
||||
folder = folder.replace(0, '_')
|
||||
|
||||
# Grab our list of files early on so we can determine if we need to create
|
||||
# the lossy_dest_dir, lossless_dest_dir, or both
|
||||
files_to_move = []
|
||||
lossy_media = False
|
||||
lossless_media = False
|
||||
|
||||
destination_path = os.path.normpath(os.path.join(headphones.DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING)
|
||||
|
||||
last_folder = headphones.FOLDER_FORMAT.split('/')[-1]
|
||||
|
||||
# Only rename the folder if they use the album name, otherwise merge into existing folder
|
||||
if os.path.exists(destination_path) and 'album' in last_folder.lower():
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = folder + '[%i]' % i
|
||||
destination_path = os.path.normpath(os.path.join(headphones.DESTINATION_DIR, newfolder)).encode(headphones.SYS_ENCODING)
|
||||
if os.path.exists(destination_path):
|
||||
i += 1
|
||||
else:
|
||||
folder = newfolder
|
||||
break
|
||||
|
||||
logger.info('Moving files from %s to %s' % (unicode(albumpath, headphones.SYS_ENCODING, errors="replace"), unicode(destination_path, headphones.SYS_ENCODING, errors="replace")))
|
||||
|
||||
# Basically check if generic/non-album folders already exist, since we're going to merge
|
||||
if not os.path.exists(destination_path):
|
||||
try:
|
||||
os.makedirs(destination_path)
|
||||
except Exception, e:
|
||||
logger.error('Could not create folder for %s. Not moving: %s' % (release['AlbumTitle'], e))
|
||||
return albumpath
|
||||
|
||||
# Move files to the destination folder, renaming them if they already exist
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for files in f:
|
||||
if os.path.isfile(os.path.join(destination_path, files)):
|
||||
logger.info('Destination file exists: %s' % os.path.join(destination_path, files))
|
||||
title = os.path.splitext(files)[0]
|
||||
ext = os.path.splitext(files)[1]
|
||||
i = 1
|
||||
while True:
|
||||
newfile = title + '(' + str(i) + ')' + ext
|
||||
if os.path.isfile(os.path.join(destination_path, newfile)):
|
||||
i += 1
|
||||
else:
|
||||
logger.info('Renaming to %s' % newfile)
|
||||
try:
|
||||
os.rename(os.path.join(r, files), os.path.join(r, newfile))
|
||||
files = newfile
|
||||
except Exception, e:
|
||||
logger.warn('Error renaming %s: %s' % (files, e))
|
||||
break
|
||||
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):
|
||||
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.DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING)
|
||||
lossless_destination_path = os.path.normpath(os.path.join(headphones.LOSSLESS_DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING)
|
||||
|
||||
# If they set a destination dir for lossless media, only create the lossy folder if there is lossy media
|
||||
if headphones.LOSSLESS_DESTINATION_DIR:
|
||||
if lossy_media:
|
||||
make_lossy_folder = True
|
||||
if lossless_media:
|
||||
make_lossless_folder = True
|
||||
# If they haven't set a lossless dest_dir, just create the "lossy" folder
|
||||
else:
|
||||
make_lossy_folder = True
|
||||
|
||||
last_folder = headphones.FOLDER_FORMAT.split('/')[-1]
|
||||
|
||||
if make_lossless_folder:
|
||||
# Only rename the folder if they use the album name, otherwise merge into existing folder
|
||||
if os.path.exists(lossless_destination_path) and 'album' in last_folder.lower():
|
||||
|
||||
temp_folder = folder
|
||||
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = temp_folder + '[%i]' % i
|
||||
lossless_destination_path = os.path.normpath(os.path.join(headphones.LOSSLESS_DESTINATION_DIR, newfolder)).encode(headphones.SYS_ENCODING)
|
||||
if os.path.exists(lossless_destination_path):
|
||||
i += 1
|
||||
else:
|
||||
temp_folder = newfolder
|
||||
break
|
||||
|
||||
if not os.path.exists(lossless_destination_path):
|
||||
try:
|
||||
shutil.move(os.path.join(r, files), os.path.join(destination_path, files))
|
||||
os.makedirs(lossless_destination_path)
|
||||
except Exception, e:
|
||||
logger.warn('Error moving file %s: %s' % (files, e))
|
||||
logger.error('Could not create lossless folder for %s. (Error: %s)' % (release['AlbumTitle'], e))
|
||||
if not make_lossy_folder:
|
||||
return albumpath
|
||||
|
||||
if make_lossy_folder:
|
||||
if os.path.exists(lossy_destination_path) and 'album' in last_folder.lower():
|
||||
|
||||
temp_folder = folder
|
||||
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = temp_folder + '[%i]' % i
|
||||
lossy_destination_path = os.path.normpath(os.path.join(headphones.DESTINATION_DIR, newfolder)).encode(headphones.SYS_ENCODING)
|
||||
if os.path.exists(lossy_destination_path):
|
||||
i += 1
|
||||
else:
|
||||
temp_folder = newfolder
|
||||
break
|
||||
|
||||
if not os.path.exists(lossy_destination_path):
|
||||
try:
|
||||
os.makedirs(lossy_destination_path)
|
||||
except Exception, 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.....')
|
||||
|
||||
# Move files to the destination folder, renaming them if they already exist
|
||||
# If we have two desination_dirs, move non-music files to both
|
||||
if make_lossy_folder and make_lossless_folder:
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
if moved_to_lossy_folder or moved_to_lossless_folder:
|
||||
try:
|
||||
os.remove(file_to_move)
|
||||
except Exception, e:
|
||||
logger.error("Error deleting file '" + file_to_move.decode(headphones.SYS_ENCODING) + "' from source directory")
|
||||
else:
|
||||
logger.error("Error copying '" + file_to_move.decode(headphones.SYS_ENCODING) + "'. Not deleting from download directory")
|
||||
|
||||
elif make_lossless_folder and not make_lossy_folder:
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
helpers.smartMove(file_to_move, lossless_destination_path)
|
||||
|
||||
else:
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
helpers.smartMove(file_to_move, lossy_destination_path)
|
||||
|
||||
# Chmod the directories using the folder_format (script courtesy of premiso!)
|
||||
folder_list = folder.split('/')
|
||||
temp_f = headphones.DESTINATION_DIR
|
||||
|
||||
for f in folder_list:
|
||||
|
||||
temp_f = os.path.join(temp_f, f)
|
||||
|
||||
try:
|
||||
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING), int(headphones.FOLDER_PERMISSIONS, 8))
|
||||
except Exception, e:
|
||||
logger.error("Error trying to change permissions on folder: %s" % temp_f)
|
||||
temp_fs = []
|
||||
|
||||
if make_lossless_folder:
|
||||
temp_fs.append(headphones.LOSSLESS_DESTINATION_DIR)
|
||||
|
||||
if make_lossy_folder:
|
||||
temp_fs.append(headphones.DESTINATION_DIR)
|
||||
|
||||
for temp_f in temp_fs:
|
||||
|
||||
for f in folder_list:
|
||||
|
||||
temp_f = os.path.join(temp_f, f)
|
||||
|
||||
try:
|
||||
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING), int(headphones.FOLDER_PERMISSIONS, 8))
|
||||
except Exception, e:
|
||||
logger.error("Error trying to change permissions on folder: %s" % temp_f)
|
||||
|
||||
# If we failed to move all the files out of the directory, this will fail too
|
||||
try:
|
||||
@@ -456,46 +523,74 @@ def moveFiles(albumpath, release, tracks):
|
||||
except Exception, e:
|
||||
logger.error('Could not remove directory: %s. %s' % (albumpath, e))
|
||||
|
||||
return destination_path
|
||||
destination_paths = []
|
||||
|
||||
if make_lossy_folder:
|
||||
destination_paths.append(lossy_destination_path)
|
||||
if make_lossless_folder:
|
||||
destination_paths.append(lossless_destination_path)
|
||||
|
||||
return destination_paths
|
||||
|
||||
def correctMetadata(albumid, release, downloaded_track_list):
|
||||
|
||||
logger.info('Writing metadata')
|
||||
items = []
|
||||
logger.info('Preparing to write metadata to tracks....')
|
||||
lossy_items = []
|
||||
lossless_items = []
|
||||
|
||||
# Process lossless & lossy media formats separately
|
||||
for downloaded_track in downloaded_track_list:
|
||||
|
||||
try:
|
||||
|
||||
try:
|
||||
items.append(beets.library.Item.from_path(downloaded_track))
|
||||
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):
|
||||
lossy_items.append(beets.library.Item.from_path(downloaded_track))
|
||||
else:
|
||||
logger.warn("Skipping: " + downloaded_track.decode(headphones.SYS_ENCODING) + " because it is not a mutagen friendly file format")
|
||||
except Exception, e:
|
||||
logger.error("Beets couldn't create an Item from: " + downloaded_track + " - not a media file?" + str(e))
|
||||
|
||||
try:
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle']))
|
||||
except Exception, e:
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata' % e)
|
||||
return
|
||||
if rec == 'RECOMMEND_NONE':
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata' % (release['ArtistName'], release['AlbumTitle']))
|
||||
return
|
||||
|
||||
dist, info, mapping, extra_items, extra_tracks = candidates[0]
|
||||
logger.debug('Beets recommendation: %s' % rec)
|
||||
autotag.apply_metadata(info, mapping)
|
||||
|
||||
if len(items) != len(downloaded_track_list):
|
||||
logger.warn("Mismatch between number of tracks downloaded and the metadata items, but I'll try to write it anyway")
|
||||
|
||||
i = 1
|
||||
for item in items:
|
||||
|
||||
logger.error("Beets couldn't create an Item from: " + downloaded_track.decode(headphones.SYS_ENCODING) + " - not a media file?" + str(e))
|
||||
|
||||
for items in [lossy_items, lossless_items]:
|
||||
|
||||
if not items:
|
||||
continue
|
||||
|
||||
try:
|
||||
item.write()
|
||||
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle']))
|
||||
except Exception, e:
|
||||
logger.warn('Error writing metadata to track %i: %s' % (i,e))
|
||||
i += 1
|
||||
logger.error('Error getting recommendation: %s. Not writing metadata' % e)
|
||||
return
|
||||
if rec == 'RECOMMEND_NONE':
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata' % (release['ArtistName'], release['AlbumTitle']))
|
||||
return
|
||||
|
||||
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']))
|
||||
return
|
||||
|
||||
logger.info('Beets recommendation for tagging items: %s' % rec)
|
||||
|
||||
# TODO: Handle extra_items & extra_tracks
|
||||
|
||||
autotag.apply_metadata(info, mapping)
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
item.write()
|
||||
logger.info("Successfully applied metadata to: " + item.path.decode(headphones.SYS_ENCODING))
|
||||
except Exception, e:
|
||||
logger.warn("Error writing metadata to " + item.path.decode(headphones.SYS_ENCODING) + ": " + str(e))
|
||||
|
||||
def embedLyrics(downloaded_track_list):
|
||||
logger.info('Adding lyrics')
|
||||
|
||||
# TODO: If adding lyrics for flac & lossy, only fetch the lyrics once
|
||||
# and apply it to both files
|
||||
for downloaded_track in downloaded_track_list:
|
||||
|
||||
try:
|
||||
@@ -528,7 +623,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except:
|
||||
logger.info("MediaFile couldn't parse: " + downloaded_track)
|
||||
logger.info("MediaFile couldn't parse: " + downloaded_track.decode(headphones.SYS_ENCODING))
|
||||
continue
|
||||
|
||||
if not f.track:
|
||||
@@ -570,43 +665,17 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
new_file_name = new_file_name.replace(0, '_')
|
||||
|
||||
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) + " is not neccessary")
|
||||
continue
|
||||
|
||||
logger.debug('Renaming %s ---> %s' % (downloaded_track, new_file_name))
|
||||
logger.debug('Renaming %s ---> %s' % (downloaded_track.decode(headphones.SYS_ENCODING), new_file_name.decode(headphones.SYS_ENCODING)))
|
||||
try:
|
||||
os.rename(downloaded_track, new_file)
|
||||
except Exception, e:
|
||||
logger.error('Error renaming file: %s. Error: %s' % (downloaded_track, e))
|
||||
logger.error('Error renaming file: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING), e))
|
||||
continue
|
||||
|
||||
def updateHave(albumpath):
|
||||
|
||||
results = []
|
||||
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for files in f:
|
||||
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
results.append(os.path.join(r, files))
|
||||
|
||||
if results:
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
for song in results:
|
||||
try:
|
||||
f = MediaFile(song)
|
||||
#logger.debug('Reading: %s' % song.decode('UTF-8'))
|
||||
except:
|
||||
logger.warn('Could not read file: %s' % song)
|
||||
continue
|
||||
else:
|
||||
if f.albumartist:
|
||||
artist = f.albumartist
|
||||
elif f.artist:
|
||||
artist = f.artist
|
||||
else:
|
||||
continue
|
||||
|
||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [unicode(song, headphones.SYS_ENCODING, errors="replace"), f.bitrate, f.format, artist, f.album, f.title])
|
||||
|
||||
def renameUnprocessedFolder(albumpath):
|
||||
|
||||
|
||||
@@ -496,6 +496,8 @@ def searchNZB(albumid=None, new=False, losslessOnly=False):
|
||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [albums[2]])
|
||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?)', [albums[2], bestqual[0], bestqual[1], bestqual[2], "Snatched", nzb_folder_name])
|
||||
return "found"
|
||||
else:
|
||||
return "none"
|
||||
else:
|
||||
return "none"
|
||||
|
||||
@@ -673,7 +675,9 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
data = False
|
||||
|
||||
if data:
|
||||
|
||||
|
||||
logger.info(u'Parsing results from <a href="%s">KAT</a>' % searchURL)
|
||||
|
||||
d = feedparser.parse(data)
|
||||
if not len(d.entries):
|
||||
logger.info(u"No results found from %s for %s" % (provider, term))
|
||||
@@ -707,7 +711,7 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
resultlist.append((title, size, url, provider))
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
logger.info('%s is larger than the maxsize, the wrong format or has to little seeders for this category, skipping. (Size: %i bytes, Seeders: %i, Format: %s)' % (title, size, int(seeders), 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(seeders), rightformat))
|
||||
|
||||
except Exception, e:
|
||||
logger.error(u"An unknown error occurred in the KAT parser: %s" % e)
|
||||
@@ -754,7 +758,9 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
data = False
|
||||
|
||||
if data:
|
||||
|
||||
|
||||
logger.info(u'Parsing results from <a href="%s">Waffles.fm</a>' % searchURL)
|
||||
|
||||
d = feedparser.parse(data)
|
||||
if not len(d.entries):
|
||||
logger.info(u"No results found from %s for %s" % (provider, term))
|
||||
@@ -780,7 +786,7 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
|
||||
|
||||
if headphones.ISOHUNT:
|
||||
provider = "ISOhunt"
|
||||
provider = "isoHunt"
|
||||
providerurl = url_fix("http://isohunt.com/js/rss/" + term)
|
||||
if headphones.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||
categories = "7" #music
|
||||
@@ -809,6 +815,8 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
|
||||
if data:
|
||||
|
||||
logger.info(u'Parsing results from <a href="%s">isoHunt</a>' % searchURL)
|
||||
|
||||
d = feedparser.parse(data)
|
||||
if not len(d.entries):
|
||||
logger.info(u"No results found from %s for %s" % (provider, term))
|
||||
@@ -846,10 +854,10 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
resultlist.append((title, size, url, provider))
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
logger.info('%s is larger than the maxsize, the wrong format or has to 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))
|
||||
|
||||
except Exception, e:
|
||||
logger.error(u"An unknown error occurred in the ISOhunt parser: %s" % e)
|
||||
logger.error(u"An unknown error occurred in the isoHunt parser: %s" % e)
|
||||
|
||||
if headphones.MININOVA:
|
||||
provider = "Mininova"
|
||||
@@ -877,6 +885,8 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
|
||||
if data:
|
||||
|
||||
logger.info(u'Parsing results from <a href="%s">Mininova</a>' % searchURL)
|
||||
|
||||
d = feedparser.parse(data)
|
||||
if not len(d.entries):
|
||||
logger.info(u"No results found from %s for %s" % (provider, term))
|
||||
@@ -913,10 +923,10 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
resultlist.append((title, size, url, provider))
|
||||
logger.info('Found %s. Size: %s' % (title, helpers.bytes_to_mb(size)))
|
||||
else:
|
||||
logger.info('%s is larger than the maxsize, the wrong format or has to 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))
|
||||
|
||||
except Exception, e:
|
||||
logger.error(u"An unknown error occurred in the MiniNova Parser: %s" % e)
|
||||
logger.error(u"An unknown error occurred in the Mininova Parser: %s" % e)
|
||||
|
||||
|
||||
|
||||
@@ -983,7 +993,7 @@ def searchTorrent(albumid=None, new=False, losslessOnly=False):
|
||||
(data, bestqual) = preprocesstorrent(torrentlist)
|
||||
|
||||
if data and bestqual:
|
||||
logger.info(u'Found best result: <a href="%s">%s</a> - %s' % (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])))
|
||||
torrent_folder_name = '%s - %s [%s]' % (helpers.latinToAscii(albums[0]).encode('UTF-8').replace('/', '_'), helpers.latinToAscii(albums[1]).encode('UTF-8').replace('/', '_'), year)
|
||||
if headphones.TORRENTBLACKHOLE_DIR == "sendtracker":
|
||||
|
||||
|
||||
@@ -211,6 +211,15 @@ class WebInterface(object):
|
||||
else:
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
deleteAlbum.exposed = True
|
||||
|
||||
def switchAlbum(self, AlbumID, ReleaseID):
|
||||
'''
|
||||
Take the values from allalbums/alltracks (based on the ReleaseID) and swap it into the album & track tables
|
||||
'''
|
||||
from headphones import albumswitcher
|
||||
albumswitcher.switch(AlbumID, ReleaseID)
|
||||
raise cherrypy.HTTPRedirect("albumPage?AlbumID=%s" % AlbumID)
|
||||
switchAlbum.exposed = True
|
||||
|
||||
def upcoming(self):
|
||||
myDB = db.DBConnection()
|
||||
@@ -229,9 +238,14 @@ class WebInterface(object):
|
||||
return serve_template(templatename="manageartists.html", title="Manage Artists", artists=artists)
|
||||
manageArtists.exposed = True
|
||||
|
||||
def manageAlbums(self):
|
||||
def manageAlbums(self, Status=None):
|
||||
myDB = db.DBConnection()
|
||||
albums = myDB.select('SELECT * from albums')
|
||||
if Status == "Upcoming":
|
||||
albums = myDB.select("SELECT * from albums WHERE ReleaseDate > date('now')")
|
||||
elif Status:
|
||||
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)
|
||||
manageAlbums.exposed = True
|
||||
|
||||
@@ -415,6 +429,7 @@ class WebInterface(object):
|
||||
"embed_album_art" : checked(headphones.EMBED_ALBUM_ART),
|
||||
"embed_lyrics" : checked(headphones.EMBED_LYRICS),
|
||||
"dest_dir" : headphones.DESTINATION_DIR,
|
||||
"lossless_dest_dir" : headphones.LOSSLESS_DESTINATION_DIR,
|
||||
"folder_format" : headphones.FOLDER_FORMAT,
|
||||
"file_format" : headphones.FILE_FORMAT,
|
||||
"include_extras" : checked(headphones.INCLUDE_EXTRAS),
|
||||
@@ -432,6 +447,7 @@ class WebInterface(object):
|
||||
"encodervbrcbr": headphones.ENCODERVBRCBR,
|
||||
"encoderquality": headphones.ENCODERQUALITY,
|
||||
"encoderlossless": checked(headphones.ENCODERLOSSLESS),
|
||||
"delete_lossless_files": checked(headphones.DELETE_LOSSLESS_FILES),
|
||||
"prowl_enabled": checked(headphones.PROWL_ENABLED),
|
||||
"prowl_onsnatch": checked(headphones.PROWL_ONSNATCH),
|
||||
"prowl_keys": headphones.PROWL_KEYS,
|
||||
@@ -463,8 +479,8 @@ class WebInterface(object):
|
||||
usenet_retention=None, nzbmatrix=0, nzbmatrix_username=None, nzbmatrix_apikey=None, newznab=0, newznab_host=None, newznab_apikey=None, newznab_enabled=0,
|
||||
nzbsorg=0, nzbsorg_uid=None, nzbsorg_hash=None, newzbin=0, newzbin_uid=None, newzbin_password=None, preferred_quality=0, preferred_bitrate=None, detect_bitrate=0, move_files=0,
|
||||
torrentblackhole_dir=None, download_torrent_dir=None, numberofseeders=10, use_isohunt=0, use_kat=0, use_mininova=0, waffles=0, waffles_uid=None, waffles_passkey=None,
|
||||
rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, embed_album_art=0, embed_lyrics=0, destination_dir=None, folder_format=None, file_format=None, include_extras=0, autowant_upcoming=False, autowant_all=False, interface=None, log_dir=None,
|
||||
music_encoder=0, encoder=None, bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0,
|
||||
rename_files=0, correct_metadata=0, cleanup_files=0, add_album_art=0, embed_album_art=0, embed_lyrics=0, destination_dir=None, lossless_destination_dir=None, folder_format=None, file_format=None, include_extras=0, autowant_upcoming=False, autowant_all=False, interface=None, log_dir=None,
|
||||
music_encoder=0, encoder=None, bitrate=None, samplingfrequency=None, encoderfolder=None, advancedencoder=None, encoderoutputformat=None, encodervbrcbr=None, encoderquality=None, encoderlossless=0, delete_lossless_files=0,
|
||||
prowl_enabled=0, prowl_onsnatch=0, prowl_keys=None, prowl_priority=0, xbmc_enabled=0, xbmc_host=None, xbmc_username=None, xbmc_password=None, xbmc_update=0, xbmc_notify=0,
|
||||
nma_enabled=False, nma_apikey=None, nma_priority=0, synoindex_enabled=False, mirror=None, customhost=None, customport=None, customsleep=None, hpuser=None, hppass=None, **kwargs):
|
||||
|
||||
@@ -520,6 +536,7 @@ class WebInterface(object):
|
||||
headphones.EMBED_ALBUM_ART = embed_album_art
|
||||
headphones.EMBED_LYRICS = embed_lyrics
|
||||
headphones.DESTINATION_DIR = destination_dir
|
||||
headphones.LOSSLESS_DESTINATION_DIR = lossless_destination_dir
|
||||
headphones.FOLDER_FORMAT = folder_format
|
||||
headphones.FILE_FORMAT = file_format
|
||||
headphones.INCLUDE_EXTRAS = include_extras
|
||||
@@ -536,7 +553,8 @@ class WebInterface(object):
|
||||
headphones.ENCODEROUTPUTFORMAT = encoderoutputformat
|
||||
headphones.ENCODERVBRCBR = encodervbrcbr
|
||||
headphones.ENCODERQUALITY = int(encoderquality)
|
||||
headphones.ENCODERLOSSLESS = encoderlossless
|
||||
headphones.ENCODERLOSSLESS = int(encoderlossless)
|
||||
headphones.DELETE_LOSSLESS_FILES = int(delete_lossless_files)
|
||||
headphones.PROWL_ENABLED = prowl_enabled
|
||||
headphones.PROWL_ONSNATCH = prowl_onsnatch
|
||||
headphones.PROWL_KEYS = prowl_keys
|
||||
|
||||
Reference in New Issue
Block a user