mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-10 00:32:52 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9d331041d | ||
|
|
4b76e156a9 | ||
|
|
65660e57ca | ||
|
|
48e2722d9e | ||
|
|
91ec945a37 | ||
|
|
ba62e297b5 | ||
|
|
7f3d89aaa7 | ||
|
|
b7321e3121 | ||
|
|
2ecf0bcb41 |
@@ -68,6 +68,8 @@ Unmark album as wanted / i.e. mark as skipped
|
|||||||
force search for wanted albums - not launched in a separate thread so it may take a bit to complete
|
force search for wanted albums - not launched in a separate thread so it may take a bit to complete
|
||||||
### forceProcess
|
### forceProcess
|
||||||
Force post process albums in download directory - also not launched in a separate thread
|
Force post process albums in download directory - also not launched in a separate thread
|
||||||
|
### forceActiveArtistsUpdate
|
||||||
|
force Active Artist Update - also not launched in a separate thread
|
||||||
|
|
||||||
### getVersion
|
### getVersion
|
||||||
Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
|
Returns some version information: git_path, install_type, current_version, installed_version, commits_behind
|
||||||
|
|||||||
+13
-3
@@ -1,9 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v0.5.2
|
## v0.5.3
|
||||||
Released 28 december 2014
|
Released 15 January 2015
|
||||||
|
|
||||||
Highlight:
|
Highlights:
|
||||||
|
* Added: update active artists via API (#2075)
|
||||||
|
* Fixed: queue instance lacks `add` method (#2055)
|
||||||
|
* Improved: better SSL error messages (#2058)
|
||||||
|
|
||||||
|
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5.2...v0.5.3).
|
||||||
|
|
||||||
|
## v0.5.2
|
||||||
|
Released 28 December 2014
|
||||||
|
|
||||||
|
Highlights:
|
||||||
* Added: advanced option to ignore certain folders by patterns. (#2037)
|
* Added: advanced option to ignore certain folders by patterns. (#2037)
|
||||||
* Added: advanced option to ignore certain files by patterns (library only)
|
* Added: advanced option to ignore certain files by patterns (library only)
|
||||||
* Added: specify optional paths to CUE splitting tools (#1938)
|
* Added: specify optional paths to CUE splitting tools (#1938)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=all',$(this),'table')" data-success="All History cleared"><i class="fa fa-trash-o"></i> Clear All History</a>
|
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=all',$(this),'table')" data-success="All History cleared"><i class="fa fa-trash-o"></i> Clear All History</a>
|
||||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Processed',$(this),'table')" data-success="All Processed cleared"><i class="fa fa-trash-o"></i> Clear Processed</a>
|
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Processed',$(this),'table')" data-success="All Processed cleared"><i class="fa fa-trash-o"></i> Clear Processed</a>
|
||||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Unprocessed',$(this),'table')" data-success="All Unprocessed cleared"><i class="fa fa-trash-o"></i> Clear Unprocessed</a>
|
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Unprocessed',$(this),'table')" data-success="All Unprocessed cleared"><i class="fa fa-trash-o"></i> Clear Unprocessed</a>
|
||||||
|
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Frozen',$(this),'table')" data-success="All Frozen cleared"><i class="fa fa-trash-o"></i> Clear Frozen</a>
|
||||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Snatched',$(this),'table')" data-success="All Snatched cleared"><i class="fa fa-trash-o"></i> Clear Snatched</a>
|
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearhistory?type=Snatched',$(this),'table')" data-success="All Snatched cleared"><i class="fa fa-trash-o"></i> Clear Snatched</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,6 +40,8 @@
|
|||||||
grade = 'C'
|
grade = 'C'
|
||||||
elif item['Status'] == 'Unprocessed':
|
elif item['Status'] == 'Unprocessed':
|
||||||
grade = 'X'
|
grade = 'X'
|
||||||
|
elif item['Status'] == 'Frozen':
|
||||||
|
grade = 'X'
|
||||||
else:
|
else:
|
||||||
grade = 'U'
|
grade = 'U'
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -13,16 +13,17 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from headphones import db, mb, importer, searcher, cache, postprocessor, versioncheck, logger
|
from headphones import db, mb, updater, importer, searcher, cache, postprocessor, versioncheck, logger
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
import json
|
import json
|
||||||
|
|
||||||
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs',
|
cmd_list = ['getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs',
|
||||||
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
|
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
|
||||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
|
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'forceActiveArtistsUpdate',
|
||||||
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
|
'getVersion', 'checkGithub','shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt',
|
||||||
'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release']
|
'getArtistInfo', 'getAlbumInfo', 'getArtistThumb', 'getAlbumThumb',
|
||||||
|
'choose_specific_download', 'download_specific_release']
|
||||||
|
|
||||||
|
|
||||||
class Api(object):
|
class Api(object):
|
||||||
@@ -321,6 +322,9 @@ class Api(object):
|
|||||||
self.dir = kwargs['dir']
|
self.dir = kwargs['dir']
|
||||||
postprocessor.forcePostProcess(self.dir)
|
postprocessor.forcePostProcess(self.dir)
|
||||||
|
|
||||||
|
def _forceActiveArtistsUpdate(self, **kwargs):
|
||||||
|
updater.dbUpdate()
|
||||||
|
|
||||||
def _getVersion(self, **kwargs):
|
def _getVersion(self, **kwargs):
|
||||||
self.data = {
|
self.data = {
|
||||||
'git_path': headphones.CONFIG.GIT_PATH,
|
'git_path': headphones.CONFIG.GIT_PATH,
|
||||||
|
|||||||
@@ -64,20 +64,18 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
encoded_track_string = track['Location'].encode(headphones.SYS_ENCODING, 'replace')
|
||||||
if not os.path.isfile(encoded_track_string):
|
if not os.path.isfile(encoded_track_string):
|
||||||
if track['ArtistName']:
|
if track['ArtistName']:
|
||||||
#Make sure deleted files get accounted for when updating artist track counts
|
# Make sure deleted files get accounted for when updating artist track counts
|
||||||
new_artists.append(track['ArtistName'])
|
new_artists.append(track['ArtistName'])
|
||||||
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
||||||
logger.info('File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace'))
|
logger.info('File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(headphones.SYS_ENCODING, 'replace'))
|
||||||
###############myDB.action('DELETE from have')
|
|
||||||
|
|
||||||
bitrates = []
|
bitrates = []
|
||||||
|
|
||||||
song_list = []
|
song_list = []
|
||||||
|
latest_subdirectory = []
|
||||||
|
|
||||||
new_song_count = 0
|
new_song_count = 0
|
||||||
file_count = 0
|
file_count = 0
|
||||||
|
|
||||||
latest_subdirectory = []
|
|
||||||
|
|
||||||
for r, d, f in helpers.walk_directory(dir):
|
for r, d, f in helpers.walk_directory(dir):
|
||||||
# Filter paths based on config. Note that these methods work directly
|
# Filter paths based on config. Note that these methods work directly
|
||||||
# on the inputs
|
# on the inputs
|
||||||
@@ -313,7 +311,6 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Update track counts
|
# Update track counts
|
||||||
|
|
||||||
for artist in artists_checked:
|
for artist in artists_checked:
|
||||||
# Have tracks are selected from tracks table and not all tracks because of duplicates
|
# 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
|
# We update the track count upon an album switch to compliment this
|
||||||
@@ -321,13 +318,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist]))
|
len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist]))
|
||||||
+ len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist]))
|
+ len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist]))
|
||||||
)
|
)
|
||||||
#Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases
|
# Note: some people complain about having "artist have tracks" > # of tracks total in artist official releases
|
||||||
# (can fix by getting rid of second len statement)
|
# (can fix by getting rid of second len statement)
|
||||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
|
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
|
||||||
|
|
||||||
logger.info('Found %i new artists' % len(artist_list))
|
logger.info('Found %i new artists' % len(artist_list))
|
||||||
|
|
||||||
if len(artist_list):
|
if artist_list:
|
||||||
if headphones.CONFIG.AUTO_ADD_ARTISTS:
|
if headphones.CONFIG.AUTO_ADD_ARTISTS:
|
||||||
logger.info('Importing %i new artists' % len(artist_list))
|
logger.info('Importing %i new artists' % len(artist_list))
|
||||||
importer.artistlist_to_mbids(artist_list)
|
importer.artistlist_to_mbids(artist_list)
|
||||||
|
|||||||
+5
-6
@@ -39,12 +39,12 @@ class TimedLock(object):
|
|||||||
sleep_amount = self.minimum_delta - delta
|
sleep_amount = self.minimum_delta - delta
|
||||||
if sleep_amount >= 0:
|
if sleep_amount >= 0:
|
||||||
# zero sleeps give the cpu a chance to task-switch
|
# zero sleeps give the cpu a chance to task-switch
|
||||||
headphones.logger.info('Sleeping %s (interval)', sleep_amount)
|
headphones.logger.debug('Sleeping %s (interval)', sleep_amount)
|
||||||
time.sleep(sleep_amount)
|
time.sleep(sleep_amount)
|
||||||
while not self.queue.empty():
|
while not self.queue.empty():
|
||||||
try:
|
try:
|
||||||
seconds = self.queue.get(False)
|
seconds = self.queue.get(False)
|
||||||
headphones.logger.info('Sleeping %s (queued)', seconds)
|
headphones.logger.debug('Sleeping %s (queued)', seconds)
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
except Queue.Empty:
|
except Queue.Empty:
|
||||||
continue
|
continue
|
||||||
@@ -59,15 +59,14 @@ class TimedLock(object):
|
|||||||
|
|
||||||
def snooze(self, seconds):
|
def snooze(self, seconds):
|
||||||
"""
|
"""
|
||||||
Asynchronously add time to the next request.
|
Asynchronously add time to the next request. Can be called outside
|
||||||
Can be called outside
|
|
||||||
of the lock context, but it is possible for the next lock holder
|
of the lock context, but it is possible for the next lock holder
|
||||||
to not check the queue until after something adds time to it.
|
to not check the queue until after something adds time to it.
|
||||||
"""
|
"""
|
||||||
# we use a queue so that we don't have to synchronize
|
# We use a queue so that we don't have to synchronize
|
||||||
# across threads and with or without locks
|
# across threads and with or without locks
|
||||||
headphones.logger.info('Adding %s to queue', seconds)
|
headphones.logger.info('Adding %s to queue', seconds)
|
||||||
self.queue.add(seconds)
|
self.queue.put(seconds)
|
||||||
|
|
||||||
|
|
||||||
class FakeLock(object):
|
class FakeLock(object):
|
||||||
|
|||||||
+20
-14
@@ -19,6 +19,7 @@ import shutil
|
|||||||
import uuid
|
import uuid
|
||||||
import beets
|
import beets
|
||||||
import threading
|
import threading
|
||||||
|
import itertools
|
||||||
import headphones
|
import headphones
|
||||||
|
|
||||||
from beets import autotag
|
from beets import autotag
|
||||||
@@ -33,7 +34,7 @@ postprocessor_lock = threading.Lock()
|
|||||||
|
|
||||||
|
|
||||||
def checkFolder():
|
def checkFolder():
|
||||||
logger.info("Checking download folder for completed downloads")
|
logger.info("Checking download folder for completed downloads (only snatched ones).")
|
||||||
|
|
||||||
with postprocessor_lock:
|
with postprocessor_lock:
|
||||||
myDB = db.DBConnection()
|
myDB = db.DBConnection()
|
||||||
@@ -55,7 +56,7 @@ def checkFolder():
|
|||||||
else:
|
else:
|
||||||
logger.info("No folder name found for " + album['Title'])
|
logger.info("No folder name found for " + album['Title'])
|
||||||
|
|
||||||
logger.info("Checking download folder finished")
|
logger.info("Checking download folder finished.")
|
||||||
|
|
||||||
def verify(albumid, albumpath, Kind=None, forced=False):
|
def verify(albumid, albumpath, Kind=None, forced=False):
|
||||||
|
|
||||||
@@ -98,6 +99,11 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
|||||||
"but database is frozen. Will skip postprocessing for " \
|
"but database is frozen. Will skip postprocessing for " \
|
||||||
"album with rgid: %s", release_dict['artist_name'],
|
"album with rgid: %s", release_dict['artist_name'],
|
||||||
release_dict['artist_id'], albumid)
|
release_dict['artist_id'], albumid)
|
||||||
|
|
||||||
|
myDB.action('UPDATE snatched SET status = "Frozen" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||||
|
frozen = re.search(r' \(Frozen\)(?:\[\d+\])?', albumpath)
|
||||||
|
if not frozen:
|
||||||
|
renameUnprocessedFolder(albumpath, tag="Frozen")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
|
logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
|
||||||
@@ -188,7 +194,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
|||||||
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||||
if not processed:
|
if not processed:
|
||||||
renameUnprocessedFolder(albumpath)
|
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
||||||
return
|
return
|
||||||
|
|
||||||
# test #1: metadata - usually works
|
# test #1: metadata - usually works
|
||||||
@@ -272,9 +278,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
|||||||
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid])
|
||||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||||
if not processed:
|
if not processed:
|
||||||
renameUnprocessedFolder(albumpath)
|
renameUnprocessedFolder(albumpath, tag="Unprocessed")
|
||||||
else:
|
|
||||||
logger.info(u"Already marked as unprocessed: " + albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
|
||||||
|
|
||||||
|
|
||||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None):
|
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None):
|
||||||
@@ -1024,20 +1028,22 @@ def updateFilePermissions(albumpaths):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
def renameUnprocessedFolder(albumpath):
|
def renameUnprocessedFolder(path, tag):
|
||||||
|
"""
|
||||||
|
Rename a unprocessed folder to a new unique name to indicate a certain
|
||||||
|
status.
|
||||||
|
"""
|
||||||
|
|
||||||
i = 0
|
for i in itertools.count():
|
||||||
while True:
|
|
||||||
if i == 0:
|
if i == 0:
|
||||||
new_folder_name = albumpath + ' (Unprocessed)'
|
new_path = "%s (%s)" % (path, tag)
|
||||||
else:
|
else:
|
||||||
new_folder_name = albumpath + ' (Unprocessed)[%i]' % i
|
new_path = "%s (%s[%d])" % (path, tag, i)
|
||||||
|
|
||||||
if os.path.exists(new_folder_name):
|
if os.path.exists(new_path):
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
os.rename(albumpath, new_folder_name)
|
os.rename(path, new_path)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,15 @@ def request_response(url, method="get", auto_raise=True,
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except requests.exceptions.SSLError:
|
except requests.exceptions.SSLError as e:
|
||||||
logger.error("Unable to connect to remote host because of a SSL " \
|
if not kwargs["verify"]:
|
||||||
"error. It's likely the remote certificate is untrusted by your " \
|
logger.error("Unable to connect to remote host because of a SSL " \
|
||||||
"system. This check can be disabled (advanced users only).")
|
"error. It's likely the remote certificate is untrusted by " \
|
||||||
|
"your system. This check can be disabled (advanced users " \
|
||||||
|
"only).")
|
||||||
|
else:
|
||||||
|
logger.error("SSL error raised during connection, even with " \
|
||||||
|
"SSL certificate verification turned off: %s", e)
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Unable to connect to remote host. Check if the remote "
|
"Unable to connect to remote host. Check if the remote "
|
||||||
|
|||||||
Reference in New Issue
Block a user