Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
rembo10 committed May 26, 2024
2 parents c7bc852 + 3e354ff commit b54218a
Show file tree
Hide file tree
Showing 164 changed files with 19,845 additions and 10,283 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
# Changelog

## v0.6.2
Released 26 May 2024

Highlights:
* Added soulseek support
* Added bandcamp support
* Changes and dependency updates to work with Python >= 3.12

The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.6.1...v0.6.2).

## v0.6.1
Released 26 November 2023
R eleased 26 November 2023

Highlights:
* Dependency updates to work with > Python 3.11
Expand Down
58 changes: 56 additions & 2 deletions data/interfaces/default/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -310,14 +310,24 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
<input type="text" name="usenet_retention" value="${config['usenet_retention']}" size="5">
</div>
</fieldset>
<fieldset title="Method for downloading Bandcamp.com files.">
<legend>Bandcamp</legend>
<div class="row">
<label title="Path to folder where Headphones can store raw downloads from Bandcamp.com.">
Bandcamp Directory
</label>
<input type="text" name="bandcamp_dir" value="${config['bandcamp_dir']}" size="50">
<small>Full path where raw MP3s will be stored, e.g. /Users/name/Downloads/bandcamp</small>
</div>
</fieldset>
</td>
<td>
<fieldset title="Method for downloading torrent files.">
<legend>Torrents</legend>
<input type="radio" name="torrent_downloader" id="torrent_downloader_blackhole" value="0" ${config['torrent_downloader_blackhole']}> Black Hole
<input type="radio" name="torrent_downloader" id="torrent_downloader_transmission" value="1" ${config['torrent_downloader_transmission']}> Transmission
<input type="radio" name="torrent_downloader" id="torrent_downloader_utorrent" value="2" ${config['torrent_downloader_utorrent']}> uTorrent (Beta)
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge (Beta)
<input type="radio" name="torrent_downloader" id="torrent_downloader_deluge" value="3" ${config['torrent_downloader_deluge']}> Deluge
<input type="radio" name="torrent_downloader" id="torrent_downloader_qbittorrent" value="4" ${config['torrent_downloader_qbittorrent']}> QBitTorrent
</fieldset>
<fieldset id="torrent_blackhole_options">
Expand Down Expand Up @@ -438,6 +448,11 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
<input type="text" name="deluge_label" value="${config['deluge_label']}" size="30">
<small>Labels shouldn't contain spaces (requires Label plugin)</small>
</div>
<div class="row">
<label>Download Directory</label>
<input type="text" name="deluge_download_directory" value="${config['deluge_download_directory']}" size="30">
<small>Directory where Deluge should download to</small>
</div>
<div class="row">
<label>Move When Completed</label>
<input type="text" name="deluge_done_directory" value="${config['deluge_done_directory']}" size="30">
Expand Down Expand Up @@ -467,7 +482,33 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
<label>Prefer</label>
<input type="radio" name="prefer_torrents" id="prefer_torrents_0" value="0" ${config['prefer_torrents_0']}>NZBs
<input type="radio" name="prefer_torrents" id="prefer_torrents_1" value="1" ${config['prefer_torrents_1']}>Torrents
<input type="radio" name="prefer_torrents" id="prefer_torrents_2" value="2" ${config['prefer_torrents_2']}>No Preference
<input type="radio" name="prefer_torrents" id="prefer_torrents_2" value="2" ${config['prefer_torrents_2']}>Soulseek
<input type="radio" name="prefer_torrents" id="prefer_torrents_3" value="3" ${config['prefer_torrents_3']}>No Preference
</div>
</fieldset>
</td>
<td>
<fieldset>
<legend>Soulseek</legend>
<div class="row">
<label>Soulseek API URL</label>
<input type="text" name="soulseek_api_url" value="${config['soulseek_api_url']}" size="50">
</div>
<div class="row">
<label>Soulseek API KEY</label>
<input type="text" name="soulseek_api_key" value="${config['soulseek_api_key']}" size="20">
</div>
<div class="row">
<label title="Path to folder where Headphones can find the downloads.">
Soulseek Download Dir:
</label>
<input type="text" name="soulseek_download_dir" value="${config['soulseek_download_dir']}" size="50">
</div>
<div class="row">
<label title="Path to folder where Headphones can find the downloads.">
Soulseek Incomplete Download Dir:
</label>
<input type="text" name="soulseek_incomplete_download_dir" value="${config['soulseek_incomplete_download_dir']}" size="50">
</div>
</fieldset>
</td>
Expand Down Expand Up @@ -579,6 +620,19 @@ <h1 class="clearfix"><i class="fa fa-gear"></i> Settings</h1>
</div>
</div>
</fieldset>
<fieldset>
<legend>Other</legend>
<fieldset>
<div class="row checkbox left">
<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>
</div>
</fieldset>
<fieldset>
<div class="row checkbox left">
<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>
</div>
</fieldset>
</fieldset>
</td>
<td>
<fieldset>
Expand Down
2 changes: 2 additions & 0 deletions data/interfaces/default/history.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ <h1 class="clearfix"><i class="fa fa-calendar"></i> History</h1>
fileid = 'torrent'
if item['URL'].find('codeshy') != -1:
fileid = 'nzb'
if item['URL'].find('bandcamp') != -1:
fileid = 'bandcamp'

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

Expand Down
166 changes: 166 additions & 0 deletions headphones/bandcamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# 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
import json
import os
import re

from headphones import logger, helpers, metadata, request
from headphones.common import USER_AGENT
from headphones.types import Result

from mediafile import MediaFile, UnreadableFileError
from bs4 import BeautifulSoup
from bs4 import FeatureNotFound


def search(album, albumlength=None, page=1, resultlist=None):
dic = {'...': '', ' & ': ' ', ' = ': ' ', '?': '', '$': 's', ' + ': ' ',
'"': '', ',': '', '*': '', '.': '', ':': ''}
if resultlist is None:
resultlist = []

cleanalbum = helpers.latinToAscii(
helpers.replace_all(album['AlbumTitle'], dic)
).strip()
cleanartist = helpers.latinToAscii(
helpers.replace_all(album['ArtistName'], dic)
).strip()

headers = {'User-Agent': USER_AGENT}
params = {
"page": page,
"q": cleanalbum,
}
logger.info("Looking up https://bandcamp.com/search with {}".format(
params))
content = request.request_content(
url='https://bandcamp.com/search',
params=params,
headers=headers
).decode('utf8')
try:
soup = BeautifulSoup(content, "html5lib")
except FeatureNotFound:
soup = BeautifulSoup(content, "html.parser")

for item in soup.find_all("li", class_="searchresult"):
type = item.find('div', class_='itemtype').text.strip().lower()
if type == "album":
data = parse_album(item)

cleanartist_found = helpers.latinToAscii(data['artist'])
cleanalbum_found = helpers.latinToAscii(data['album'])

logger.debug(u"{} - {}".format(data['album'], cleanalbum_found))

logger.debug("Comparing {} to {}".format(
cleanalbum, cleanalbum_found))
if (cleanartist.lower() == cleanartist_found.lower() and
cleanalbum.lower() == cleanalbum_found.lower()):
resultlist.append(Result(
data['title'], data['size'], data['url'],
'bandcamp', 'bandcamp', True))
else:
continue

if(soup.find('a', class_='next')):
page += 1
logger.debug("Calling next page ({})".format(page))
search(album, albumlength=albumlength,
page=page, resultlist=resultlist)

return resultlist


def download(album, bestqual):
html = request.request_content(url=bestqual.url).decode('utf-8')
trackinfo = []
try:
trackinfo = json.loads(
re.search(r"trackinfo&quot;:(\[.*?\]),", html)
.group(1)
.replace('&quot;', '"'))
except ValueError as e:
logger.warn("Couldn't load json: {}".format(e))

directory = os.path.join(
headphones.CONFIG.BANDCAMP_DIR,
u'{} - {}'.format(
album['ArtistName'].replace('/', '_'),
album['AlbumTitle'].replace('/', '_')))
directory = helpers.latinToAscii(directory)

if not os.path.exists(directory):
try:
os.makedirs(directory)
except Exception as e:
logger.warn("Could not create directory ({})".format(e))

index = 1
for track in trackinfo:
filename = helpers.replace_illegal_chars(
u'{:02d} - {}.mp3'.format(index, track['title']))
fullname = os.path.join(directory.encode('utf-8'),
filename.encode('utf-8'))
logger.debug("Downloading to {}".format(fullname))

if 'file' in track and track['file'] != None and 'mp3-128' in track['file']:
content = request.request_content(track['file']['mp3-128'])
open(fullname, 'wb').write(content)
try:
f = MediaFile(fullname)
date, year = metadata._date_year(album)
f.update({
'artist': album['ArtistName'].encode('utf-8'),
'album': album['AlbumTitle'].encode('utf-8'),
'title': track['title'].encode('utf-8'),
'track': track['track_num'],
'tracktotal': len(trackinfo),
'year': year,
})
f.save()
except UnreadableFileError as ex:
logger.warn("MediaFile couldn't parse: %s (%s)",
fullname,
str(ex))

index += 1

return directory


def parse_album(item):
album = item.find('div', class_='heading').text.strip()
artist = item.find('div', class_='subhead').text.strip().replace("by ", "")
released = item.find('div', class_='released').text.strip().replace(
"released ", "")
year = re.search(r"(\d{4})", released).group(1)

url = item.find('div', class_='heading').find('a')['href'].split("?")[0]

length = item.find('div', class_='length').text.strip()
tracks, minutes = length.split(",")
tracks = tracks.replace(" tracks", "").replace(" track", "").strip()
minutes = minutes.replace(" minutes", "").strip()
# bandcamp offers mp3 128b with should be 960KB/minute
size = int(minutes) * 983040

data = {"title": u'{} - {} [{}]'.format(artist, album, year),
"artist": artist, "album": album,
"url": url, "size": size}

return data
37 changes: 0 additions & 37 deletions headphones/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,36 +102,6 @@ def splitQuality(quality):

return (anyQualities, bestQualities)

@staticmethod
def nameQuality(name):

def checkName(list, func):
return func([re.search(x, name, re.I) for x in list])

name = os.path.basename(name)

# if we have our exact text then assume we put it there
for x in Quality.qualityStrings:
if x == Quality.UNKNOWN:
continue

regex = '\W' + Quality.qualityStrings[x].replace(' ', '\W') + '\W'
regex_match = re.search(regex, name, re.I)
if regex_match:
return x

# TODO: fix quality checking here
if checkName(["mp3", "192"], any) and not checkName(["flac"], all):
return Quality.B192
elif checkName(["mp3", "256"], any) and not checkName(["flac"], all):
return Quality.B256
elif checkName(["mp3", "vbr"], any) and not checkName(["flac"], all):
return Quality.VBR
elif checkName(["mp3", "320"], any) and not checkName(["flac"], all):
return Quality.B320
else:
return Quality.UNKNOWN

@staticmethod
def assumeQuality(name):
if name.lower().endswith(".mp3"):
Expand All @@ -158,13 +128,6 @@ def splitCompositeStatus(status):

return (Quality.NONE, status)

@staticmethod
def statusFromName(name, assume=True):
quality = Quality.nameQuality(name)
if assume and quality == Quality.UNKNOWN:
quality = Quality.assumeQuality(name)
return Quality.compositeStatus(DOWNLOADED, quality)

DOWNLOADED = None
SNATCHED = None
SNATCHED_PROPER = None
Expand Down
10 changes: 9 additions & 1 deletion headphones/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def __repr__(self):
'DELUGE_PASSWORD': (str, 'Deluge', ''),
'DELUGE_LABEL': (str, 'Deluge', ''),
'DELUGE_DONE_DIRECTORY': (str, 'Deluge', ''),
'DELUGE_DOWNLOAD_DIRECTORY': (str, 'Deluge', ''),
'DELUGE_PAUSED': (int, 'Deluge', 0),
'DESTINATION_DIR': (str, 'General', ''),
'DETECT_BITRATE': (int, 'General', 0),
Expand Down Expand Up @@ -269,6 +270,11 @@ def __repr__(self):
'SONGKICK_ENABLED': (int, 'Songkick', 1),
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
'SONGKICK_LOCATION': (str, 'Songkick', ''),
'SOULSEEK_API_URL': (str, 'Soulseek', ''),
'SOULSEEK_API_KEY': (str, 'Soulseek', ''),
'SOULSEEK_DOWNLOAD_DIR': (str, 'Soulseek', ''),
'SOULSEEK_INCOMPLETE_DOWNLOAD_DIR': (str, 'Soulseek', ''),
'SOULSEEK': (int, 'Soulseek', 0),
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
'SUBSONIC_HOST': (str, 'Subsonic', ''),
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
Expand Down Expand Up @@ -317,7 +323,9 @@ def __repr__(self):
'XBMC_PASSWORD': (str, 'XBMC', ''),
'XBMC_UPDATE': (int, 'XBMC', 0),
'XBMC_USERNAME': (str, 'XBMC', ''),
'XLDPROFILE': (str, 'General', '')
'XLDPROFILE': (str, 'General', ''),
'BANDCAMP': (int, 'General', 0),
'BANDCAMP_DIR': (path, 'General', '')
}


Expand Down
Loading

0 comments on commit b54218a

Please sign in to comment.