Skip to content

Commit b54218a

Browse files
committed
Merge branch 'develop'
2 parents c7bc852 + 3e354ff commit b54218a

File tree

164 files changed

+19845
-10283
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

164 files changed

+19845
-10283
lines changed

CHANGELOG.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
# Changelog
22

3+
## v0.6.2
4+
Released 26 May 2024
5+
6+
Highlights:
7+
* Added soulseek support
8+
* Added bandcamp support
9+
* Changes and dependency updates to work with Python >= 3.12
10+
11+
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.6.1...v0.6.2).
12+
313
## v0.6.1
4-
Released 26 November 2023
14+
R eleased 26 November 2023
515

616
Highlights:
717
* Dependency updates to work with > Python 3.11

data/interfaces/default/config.html

+56-2
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,24 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
310310
<input type="text" name="usenet_retention" value="${config['usenet_retention']}" size="5">
311311
</div>
312312
</fieldset>
313+
<fieldset title="Method for downloading Bandcamp.com files.">
314+
<legend>Bandcamp</legend>
315+
<div class="row">
316+
<label title="Path to folder where Headphones can store raw downloads from Bandcamp.com.">
317+
Bandcamp Directory
318+
</label>
319+
<input type="text" name="bandcamp_dir" value="${config['bandcamp_dir']}" size="50">
320+
<small>Full path where raw MP3s will be stored, e.g. /Users/name/Downloads/bandcamp</small>
321+
</div>
322+
</fieldset>
313323
</td>
314324
<td>
315325
<fieldset title="Method for downloading torrent files.">
316326
<legend>Torrents</legend>
317327
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}> Black Hole
318328
<input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission
319329
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
320-
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
330+
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge
321331
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
322332
</fieldset>
323333
<fieldset id="torrent_blackhole_options">
@@ -438,6 +448,11 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
438448
<input type="text" name="deluge_label" value="${config['deluge_label']}" size="30">
439449
<small>Labels shouldn't contain spaces (requires Label plugin)</small>
440450
</div>
451+
<div class="row">
452+
<label>Download Directory</label>
453+
<input type="text" name="deluge_download_directory" value="${config['deluge_download_directory']}" size="30">
454+
<small>Directory where Deluge should download to</small>
455+
</div>
441456
<div class="row">
442457
<label>Move When Completed</label>
443458
<input type="text" name="deluge_done_directory" value="${config['deluge_done_directory']}" size="30">
@@ -467,7 +482,33 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
467482
<label>Prefer</label>
468483
<input type="radio" name="prefer_torrents" id="prefer_torrents_0" value="0" ${config['prefer_torrents_0']}>NZBs
469484
<input type="radio" name="prefer_torrents" id="prefer_torrents_1" value="1" ${config['prefer_torrents_1']}>Torrents
470-
<input type="radio" name="prefer_torrents" id="prefer_torrents_2" value="2" ${config['prefer_torrents_2']}>No Preference
485+
<input type="radio" name="prefer_torrents" id="prefer_torrents_2" value="2" ${config['prefer_torrents_2']}>Soulseek
486+
<input type="radio" name="prefer_torrents" id="prefer_torrents_3" value="3" ${config['prefer_torrents_3']}>No Preference
487+
</div>
488+
</fieldset>
489+
</td>
490+
<td>
491+
<fieldset>
492+
<legend>Soulseek</legend>
493+
<div class="row">
494+
<label>Soulseek API URL</label>
495+
<input type="text" name="soulseek_api_url" value="${config['soulseek_api_url']}" size="50">
496+
</div>
497+
<div class="row">
498+
<label>Soulseek API KEY</label>
499+
<input type="text" name="soulseek_api_key" value="${config['soulseek_api_key']}" size="20">
500+
</div>
501+
<div class="row">
502+
<label title="Path to folder where Headphones can find the downloads.">
503+
Soulseek Download Dir:
504+
</label>
505+
<input type="text" name="soulseek_download_dir" value="${config['soulseek_download_dir']}" size="50">
506+
</div>
507+
<div class="row">
508+
<label title="Path to folder where Headphones can find the downloads.">
509+
Soulseek Incomplete Download Dir:
510+
</label>
511+
<input type="text" name="soulseek_incomplete_download_dir" value="${config['soulseek_incomplete_download_dir']}" size="50">
471512
</div>
472513
</fieldset>
473514
</td>
@@ -579,6 +620,19 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
579620
</div>
580621
</div>
581622
</fieldset>
623+
<fieldset>
624+
<legend>Other</legend>
625+
<fieldset>
626+
<div class="row checkbox left">
627+
<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>
628+
</div>
629+
</fieldset>
630+
<fieldset>
631+
<div class="row checkbox left">
632+
<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>
633+
</div>
634+
</fieldset>
635+
</fieldset>
582636
</td>
583637
<td>
584638
<fieldset>

data/interfaces/default/history.html

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ <h1 class="clearfix"><i class="fa fa-calendar"></i> History</h1>
5656
fileid = 'torrent'
5757
if item['URL'].find('codeshy') != -1:
5858
fileid = 'nzb'
59+
if item['URL'].find('bandcamp') != -1:
60+
fileid = 'bandcamp'
5961

6062
folder = 'Folder: ' + item['FolderName']
6163

headphones/bandcamp.py

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# This file is part of Headphones.
2+
#
3+
# Headphones is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# Headphones is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with Headphones. If not, see <http://www.gnu.org/licenses/>
15+
16+
import headphones
17+
import json
18+
import os
19+
import re
20+
21+
from headphones import logger, helpers, metadata, request
22+
from headphones.common import USER_AGENT
23+
from headphones.types import Result
24+
25+
from mediafile import MediaFile, UnreadableFileError
26+
from bs4 import BeautifulSoup
27+
from bs4 import FeatureNotFound
28+
29+
30+
def search(album, albumlength=None, page=1, resultlist=None):
31+
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ',
32+
'"': '', ',': '', '*': '', '.': '', ':': ''}
33+
if resultlist is None:
34+
resultlist = []
35+
36+
cleanalbum = helpers.latinToAscii(
37+
helpers.replace_all(album['AlbumTitle'], dic)
38+
).strip()
39+
cleanartist = helpers.latinToAscii(
40+
helpers.replace_all(album['ArtistName'], dic)
41+
).strip()
42+
43+
headers = {'User-Agent': USER_AGENT}
44+
params = {
45+
"page": page,
46+
"q": cleanalbum,
47+
}
48+
logger.info("Looking up https://bandcamp.com/search with {}".format(
49+
params))
50+
content = request.request_content(
51+
url='https://bandcamp.com/search',
52+
params=params,
53+
headers=headers
54+
).decode('utf8')
55+
try:
56+
soup = BeautifulSoup(content, "html5lib")
57+
except FeatureNotFound:
58+
soup = BeautifulSoup(content, "html.parser")
59+
60+
for item in soup.find_all("li", class_="searchresult"):
61+
type = item.find('div', class_='itemtype').text.strip().lower()
62+
if type == "album":
63+
data = parse_album(item)
64+
65+
cleanartist_found = helpers.latinToAscii(data['artist'])
66+
cleanalbum_found = helpers.latinToAscii(data['album'])
67+
68+
logger.debug(u"{} - {}".format(data['album'], cleanalbum_found))
69+
70+
logger.debug("Comparing {} to {}".format(
71+
cleanalbum, cleanalbum_found))
72+
if (cleanartist.lower() == cleanartist_found.lower() and
73+
cleanalbum.lower() == cleanalbum_found.lower()):
74+
resultlist.append(Result(
75+
data['title'], data['size'], data['url'],
76+
'bandcamp', 'bandcamp', True))
77+
else:
78+
continue
79+
80+
if(soup.find('a', class_='next')):
81+
page += 1
82+
logger.debug("Calling next page ({})".format(page))
83+
search(album, albumlength=albumlength,
84+
page=page, resultlist=resultlist)
85+
86+
return resultlist
87+
88+
89+
def download(album, bestqual):
90+
html = request.request_content(url=bestqual.url).decode('utf-8')
91+
trackinfo = []
92+
try:
93+
trackinfo = json.loads(
94+
re.search(r"trackinfo&quot;:(\[.*?\]),", html)
95+
.group(1)
96+
.replace('&quot;', '"'))
97+
except ValueError as e:
98+
logger.warn("Couldn't load json: {}".format(e))
99+
100+
directory = os.path.join(
101+
headphones.CONFIG.BANDCAMP_DIR,
102+
u'{} - {}'.format(
103+
album['ArtistName'].replace('/', '_'),
104+
album['AlbumTitle'].replace('/', '_')))
105+
directory = helpers.latinToAscii(directory)
106+
107+
if not os.path.exists(directory):
108+
try:
109+
os.makedirs(directory)
110+
except Exception as e:
111+
logger.warn("Could not create directory ({})".format(e))
112+
113+
index = 1
114+
for track in trackinfo:
115+
filename = helpers.replace_illegal_chars(
116+
u'{:02d} - {}.mp3'.format(index, track['title']))
117+
fullname = os.path.join(directory.encode('utf-8'),
118+
filename.encode('utf-8'))
119+
logger.debug("Downloading to {}".format(fullname))
120+
121+
if 'file' in track and track['file'] != None and 'mp3-128' in track['file']:
122+
content = request.request_content(track['file']['mp3-128'])
123+
open(fullname, 'wb').write(content)
124+
try:
125+
f = MediaFile(fullname)
126+
date, year = metadata._date_year(album)
127+
f.update({
128+
'artist': album['ArtistName'].encode('utf-8'),
129+
'album': album['AlbumTitle'].encode('utf-8'),
130+
'title': track['title'].encode('utf-8'),
131+
'track': track['track_num'],
132+
'tracktotal': len(trackinfo),
133+
'year': year,
134+
})
135+
f.save()
136+
except UnreadableFileError as ex:
137+
logger.warn("MediaFile couldn't parse: %s (%s)",
138+
fullname,
139+
str(ex))
140+
141+
index += 1
142+
143+
return directory
144+
145+
146+
def parse_album(item):
147+
album = item.find('div', class_='heading').text.strip()
148+
artist = item.find('div', class_='subhead').text.strip().replace("by ", "")
149+
released = item.find('div', class_='released').text.strip().replace(
150+
"released ", "")
151+
year = re.search(r"(\d{4})", released).group(1)
152+
153+
url = item.find('div', class_='heading').find('a')['href'].split("?")[0]
154+
155+
length = item.find('div', class_='length').text.strip()
156+
tracks, minutes = length.split(",")
157+
tracks = tracks.replace(" tracks", "").replace(" track", "").strip()
158+
minutes = minutes.replace(" minutes", "").strip()
159+
# bandcamp offers mp3 128b with should be 960KB/minute
160+
size = int(minutes) * 983040
161+
162+
data = {"title": u'{} - {} [{}]'.format(artist, album, year),
163+
"artist": artist, "album": album,
164+
"url": url, "size": size}
165+
166+
return data

headphones/common.py

-37
Original file line numberDiff line numberDiff line change
@@ -102,36 +102,6 @@ def splitQuality(quality):
102102

103103
return (anyQualities, bestQualities)
104104

105-
@staticmethod
106-
def nameQuality(name):
107-
108-
def checkName(list, func):
109-
return func([re.search(x, name, re.I) for x in list])
110-
111-
name = os.path.basename(name)
112-
113-
# if we have our exact text then assume we put it there
114-
for x in Quality.qualityStrings:
115-
if x == Quality.UNKNOWN:
116-
continue
117-
118-
regex = '\W' + Quality.qualityStrings[x].replace(' ', '\W') + '\W'
119-
regex_match = re.search(regex, name, re.I)
120-
if regex_match:
121-
return x
122-
123-
# TODO: fix quality checking here
124-
if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
125-
return Quality.B192
126-
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
127-
return Quality.B256
128-
elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
129-
return Quality.VBR
130-
elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
131-
return Quality.B320
132-
else:
133-
return Quality.UNKNOWN
134-
135105
@staticmethod
136106
def assumeQuality(name):
137107
if name.lower().endswith(".mp3"):
@@ -158,13 +128,6 @@ def splitCompositeStatus(status):
158128

159129
return (Quality.NONE, status)
160130

161-
@staticmethod
162-
def statusFromName(name, assume=True):
163-
quality = Quality.nameQuality(name)
164-
if assume and quality == Quality.UNKNOWN:
165-
quality = Quality.assumeQuality(name)
166-
return Quality.compositeStatus(DOWNLOADED, quality)
167-
168131
DOWNLOADED = None
169132
SNATCHED = None
170133
SNATCHED_PROPER = None

headphones/config.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def __repr__(self):
8080
'DELUGE_PASSWORD': (str, 'Deluge', ''),
8181
'DELUGE_LABEL': (str, 'Deluge', ''),
8282
'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
83+
'DELUGE_DOWNLOAD_DIRECTORY': (str, 'Deluge', ''),
8384
'DELUGE_PAUSED': (int, 'Deluge', 0),
8485
'DESTINATION_DIR': (str, 'General', ''),
8586
'DETECT_BITRATE': (int, 'General', 0),
@@ -269,6 +270,11 @@ def __repr__(self):
269270
'SONGKICK_ENABLED': (int, 'Songkick', 1),
270271
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
271272
'SONGKICK_LOCATION': (str, 'Songkick', ''),
273+
'SOULSEEK_API_URL': (str, 'Soulseek', ''),
274+
'SOULSEEK_API_KEY': (str, 'Soulseek', ''),
275+
'SOULSEEK_DOWNLOAD_DIR': (str, 'Soulseek', ''),
276+
'SOULSEEK_INCOMPLETE_DOWNLOAD_DIR': (str, 'Soulseek', ''),
277+
'SOULSEEK': (int, 'Soulseek', 0),
272278
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
273279
'SUBSONIC_HOST': (str, 'Subsonic', ''),
274280
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
@@ -317,7 +323,9 @@ def __repr__(self):
317323
'XBMC_PASSWORD': (str, 'XBMC', ''),
318324
'XBMC_UPDATE': (int, 'XBMC', 0),
319325
'XBMC_USERNAME': (str, 'XBMC', ''),
320-
'XLDPROFILE': (str, 'General', '')
326+
'XLDPROFILE': (str, 'General', ''),
327+
'BANDCAMP': (int, 'General', 0),
328+
'BANDCAMP_DIR': (path, 'General', '')
321329
}
322330

323331

0 commit comments

Comments
 (0)