Skip to content

Commit 489c6cb

Browse files
committed
Basic soulseek support
1 parent 6afe31b commit 489c6cb

File tree

6 files changed

+311
-12
lines changed

6 files changed

+311
-12
lines changed

data/interfaces/default/config.html

+32-2
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,33 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
477477
<label>Prefer</label>
478478
<input type="radio" name="prefer_torrents" id="prefer_torrents_0" value="0" ${config['prefer_torrents_0']}>NZBs
479479
<input type="radio" name="prefer_torrents" id="prefer_torrents_1" value="1" ${config['prefer_torrents_1']}>Torrents
480-
<input type="radio" name="prefer_torrents" id="prefer_torrents_2" value="2" ${config['prefer_torrents_2']}>No Preference
480+
<input type="radio" name="prefer_torrents" id="prefer_torrents_2" value="2" ${config['prefer_torrents_2']}>Soulseek
481+
<input type="radio" name="prefer_torrents" id="prefer_torrents_3" value="3" ${config['prefer_torrents_3']}>No Preference
482+
</div>
483+
</fieldset>
484+
</td>
485+
<td>
486+
<fieldset>
487+
<legend>Soulseek</legend>
488+
<div class="row">
489+
<label>Soulseek API URL</label>
490+
<input type="text" name="soulseek_api_url" value="${config['soulseek_api_url']}" size="50">
491+
</div>
492+
<div class="row">
493+
<label>Soulseek API KEY</label>
494+
<input type="text" name="soulseek_api_key" value="${config['soulseek_api_key']}" size="20">
495+
</div>
496+
<div class="row">
497+
<label title="Path to folder where Headphones can find the downloads.">
498+
Soulseek Download Dir:
499+
</label>
500+
<input type="text" name="soulseek_download_dir" value="${config['soulseek_download_dir']}" size="50">
501+
</div>
502+
<div class="row">
503+
<label title="Path to folder where Headphones can find the downloads.">
504+
Soulseek Incomplete Download Dir:
505+
</label>
506+
<input type="text" name="soulseek_incomplete_download_dir" value="${config['soulseek_incomplete_download_dir']}" size="50">
481507
</div>
482508
</fieldset>
483509
</td>
@@ -589,14 +615,18 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
589615
</div>
590616
</div>
591617
</fieldset>
592-
593618
<fieldset>
594619
<legend>Other</legend>
595620
<fieldset>
596621
<div class="row checkbox left">
597622
<input id="use_bandcamp" type="checkbox" class="bigcheck" name="use_bandcamp" value="1" ${config['use_bandcamp']} /><label for="use_bandcamp"><span class="option">Bandcamp</span></label>
598623
</div>
599624
</fieldset>
625+
<fieldset>
626+
<div class="row checkbox left">
627+
<input id="use_soulseek" type="checkbox" class="bigcheck" name="use_soulseek" value="1" ${config['use_soulseek']} /><label for="use_soulseek"><span class="option">Soulseek</span></label>
628+
</div>
629+
</fieldset>
600630
</fieldset>
601631
</td>
602632
<td>

headphones/config.py

+5
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ def __repr__(self):
269269
'SONGKICK_ENABLED': (int, 'Songkick', 1),
270270
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
271271
'SONGKICK_LOCATION': (str, 'Songkick', ''),
272+
'SOULSEEK_API_URL': (str, 'Soulseek', ''),
273+
'SOULSEEK_API_KEY': (str, 'Soulseek', ''),
274+
'SOULSEEK_DOWNLOAD_DIR': (str, 'Soulseek', ''),
275+
'SOULSEEK_INCOMPLETE_DOWNLOAD_DIR': (str, 'Soulseek', ''),
276+
'SOULSEEK': (int, 'Soulseek', 0),
272277
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
273278
'SUBSONIC_HOST': (str, 'Subsonic', ''),
274279
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),

headphones/postprocessor.py

+31-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from beets import logging as beetslogging
2828
from mediafile import MediaFile, FileTypeError, UnreadableFileError
2929
from beetsplug import lyrics as beetslyrics
30-
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent
30+
from headphones import notifiers, utorrent, transmission, deluge, qbittorrent, soulseek
3131
from headphones import db, albumart, librarysync
3232
from headphones import logger, helpers, mb, music_encoder
3333
from headphones import metadata
@@ -36,20 +36,44 @@
3636

3737

3838
def checkFolder():
39-
logger.debug("Checking download folder for completed downloads (only snatched ones).")
39+
logger.info("Checking download folder for completed downloads (only snatched ones).")
4040

4141
with postprocessor_lock:
4242
myDB = db.DBConnection()
4343
snatched = myDB.select('SELECT * from snatched WHERE Status="Snatched"')
4444

45+
# If soulseek is used, this part will get the status from the soulseek api and return completed and errored albums
46+
completed_albums, errored_albums = set(), set()
47+
if any(album['Kind'] == 'soulseek' for album in snatched):
48+
completed_albums, errored_albums = soulseek.download_completed()
49+
4550
for album in snatched:
4651
if album['FolderName']:
4752
folder_name = album['FolderName']
4853
single = False
49-
if album['Kind'] == 'nzb':
50-
download_dir = headphones.CONFIG.DOWNLOAD_DIR
54+
if album['Kind'] == 'soulseek':
55+
if folder_name in errored_albums:
56+
# If the album had any tracks with errors in it, the whole download is considered faulty. Status will be reset to wanted.
57+
logger.info(f"Album with folder '{folder_name}' had errors during download. Setting status to 'Wanted'.")
58+
myDB.action('UPDATE albums SET Status="Wanted" WHERE AlbumID=? AND Status="Snatched"', (album['AlbumID'],))
59+
60+
# Folder will be removed from configured complete and Incomplete directory
61+
complete_path = os.path.join(headphones.CONFIG.SOULSEEK_DOWNLOAD_DIR, folder_name)
62+
incomplete_path = os.path.join(headphones.CONFIG.SOULSEEK_INCOMPLETE_DOWNLOAD_DIR, folder_name)
63+
for path in [complete_path, incomplete_path]:
64+
try:
65+
shutil.rmtree(path)
66+
except Exception as e:
67+
pass
68+
continue
69+
elif folder_name in completed_albums:
70+
download_dir = headphones.CONFIG.SOULSEEK_DOWNLOAD_DIR
71+
else:
72+
continue
73+
elif album['Kind'] == 'nzb':
74+
download_dir = headphones.CONFIG.DOWNLOAD_DIR
5175
elif album['Kind'] == 'bandcamp':
52-
download_dir = headphones.CONFIG.BANDCAMP_DIR
76+
download_dir = headphones.CONFIG.BANDCAMP_DIR
5377
else:
5478
if headphones.CONFIG.DELUGE_DONE_DIRECTORY and headphones.CONFIG.TORRENT_DOWNLOADER == 3:
5579
download_dir = headphones.CONFIG.DELUGE_DONE_DIRECTORY
@@ -1172,6 +1196,8 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None, keep_orig
11721196
download_dirs.append(dir)
11731197
if headphones.CONFIG.DOWNLOAD_DIR and not dir:
11741198
download_dirs.append(headphones.CONFIG.DOWNLOAD_DIR)
1199+
if headphones.CONFIG.SOULSEEK_DOWNLOAD_DIR and not dir:
1200+
download_dirs.append(headphones.CONFIG.SOULSEEK_DOWNLOAD_DIR)
11751201
if headphones.CONFIG.DOWNLOAD_TORRENT_DIR and not dir:
11761202
download_dirs.append(
11771203
headphones.CONFIG.DOWNLOAD_TORRENT_DIR.encode(headphones.SYS_ENCODING, 'replace'))

headphones/searcher.py

+51-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
from headphones.common import USER_AGENT
4040
from headphones.types import Result
4141
from headphones import logger, db, helpers, classes, sab, nzbget, request
42-
from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent, bandcamp
42+
from headphones import utorrent, transmission, notifiers, rutracker, deluge, qbittorrent, bandcamp, soulseek
43+
4344

4445
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
4546
TORRENT_TO_MAGNET_SERVICES = [
@@ -260,6 +261,8 @@ def strptime_musicbrainz(date_str):
260261

261262

262263
def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
264+
265+
263266
NZB_PROVIDERS = (headphones.CONFIG.HEADPHONES_INDEXER or
264267
headphones.CONFIG.NEWZNAB or
265268
headphones.CONFIG.NZBSORG or
@@ -300,7 +303,11 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
300303
results = searchNZB(album, new, losslessOnly, albumlength)
301304

302305
if not results and headphones.CONFIG.BANDCAMP:
303-
results = searchBandcamp(album, new, albumlength)
306+
results = searchBandcamp(album, new, albumlength)
307+
308+
elif headphones.CONFIG.PREFER_TORRENTS == 2 and not choose_specific_download:
309+
results = searchSoulseek(album, new, losslessOnly, albumlength)
310+
304311
else:
305312

306313
nzb_results = None
@@ -344,6 +351,7 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
344351
(data, result) = preprocess(sorted_search_results)
345352

346353
if data and result:
354+
#print(f'going to send stuff to downloader. data: {data}, album: {album}')
347355
send_to_downloader(data, result, album)
348356

349357

@@ -849,11 +857,15 @@ def send_to_downloader(data, result, album):
849857
except Exception as e:
850858
logger.error('Couldn\'t write NZB file: %s', e)
851859
return
852-
853860
elif kind == 'bandcamp':
854861
folder_name = bandcamp.download(album, result)
855862
logger.info("Setting folder_name to: {}".format(folder_name))
856863

864+
865+
elif kind == 'soulseek':
866+
soulseek.download(user=result.user, filelist=result.files)
867+
folder_name = result.folder
868+
857869
else:
858870
folder_name = '%s - %s [%s]' % (
859871
unidecode(album['ArtistName']).replace('/', '_'),
@@ -1918,14 +1930,49 @@ def set_proxy(proxy_url):
19181930
return results
19191931

19201932

1933+
def searchSoulseek(album, new=False, losslessOnly=False, albumlength=None):
1934+
# Not using some of the input stuff for now or ever
1935+
replacements = {
1936+
'...': '',
1937+
' & ': ' ',
1938+
' = ': ' ',
1939+
'?': '',
1940+
'$': '',
1941+
' + ': ' ',
1942+
'"': '',
1943+
',': '',
1944+
'*': '',
1945+
'.': '',
1946+
':': ''
1947+
}
1948+
1949+
num_tracks = get_album_track_count(album['AlbumID'])
1950+
year = get_year_from_release_date(album['ReleaseDate'])
1951+
cleanalbum = unidecode(helpers.replace_all(album['AlbumTitle'], replacements)).strip()
1952+
cleanartist = unidecode(helpers.replace_all(album['ArtistName'], replacements)).strip()
1953+
1954+
results = soulseek.search(artist=cleanartist, album=cleanalbum, year=year, losslessOnly=losslessOnly, num_tracks=num_tracks)
1955+
1956+
return results
1957+
1958+
1959+
def get_album_track_count(album_id):
1960+
# Not sure if this should be considered a helper function.
1961+
myDB = db.DBConnection()
1962+
track_count = myDB.select('SELECT COUNT(*) as count FROM tracks WHERE AlbumID=?', [album_id])[0]['count']
1963+
return track_count
1964+
1965+
19211966
# THIS IS KIND OF A MESS AND PROBABLY NEEDS TO BE CLEANED UP
19221967

19231968

19241969
def preprocess(resultlist):
19251970
for result in resultlist:
1926-
19271971
headers = {'User-Agent': USER_AGENT}
19281972

1973+
if result.kind == 'soulseek':
1974+
return True, result
1975+
19291976
if result.kind == 'torrent':
19301977

19311978
# rutracker always needs the torrent data

0 commit comments

Comments
 (0)