41 Commits
v0.4 ... v0.6

Author SHA1 Message Date
a65d6a82c0 Updated code for Version 0.6 2019-06-24 13:36:12 -05:00
659e436dc4 [YouTube]: disable transcode settings due to errors implementing this feature 2019-06-24 13:30:03 -05:00
ab0fc159f1 [tidal]: Fixed error that was resetting quality every time the config dialog was shown 2019-06-24 13:27:42 -05:00
18e90b7502 Updated translations 2019-06-24 13:20:53 -05:00
c8e83d2011 [zaycev.net]: Fixed a typo 2019-06-24 12:55:39 -05:00
4254f444db added a service category in settings for all services 2019-06-24 12:54:43 -05:00
7756d71b32 Added a progress bar for downloads in the application. Fixed a problem with format in Tidal 2019-06-24 12:45:09 -05:00
bb97d017b5 Updated changelog 2019-06-24 09:57:01 -05:00
64d076ce44 The search should be performed without blocking the GUI at all 2019-06-24 09:51:11 -05:00
d33205e84e Reload all extractors' config after saving changes in the settings dialog 2019-06-24 09:41:22 -05:00
bb411e7bbc [zaycev.net]: Added settings GUI 2019-06-21 17:12:15 -05:00
a80bfd53c1 Allow duplicate values in config files 2019-06-21 17:11:26 -05:00
75131a6fe6 Added settings to the zaycev service 2019-06-21 17:10:32 -05:00
d480e06ee3 Added settings section for tidal 2019-06-21 14:47:24 -05:00
1fcdd51358 Added setting to control the output device in libVLC 2019-06-21 13:12:54 -05:00
b254a4eb1b Download files using the new parameters from the base Extractor 2019-06-20 17:45:14 -05:00
93b066804b Defined a base extractor interface from where others will be derived. Added methods to retrieve if the transcoder should be enabled or not, and to retrieve the default file format, which also will be used as file extension in the saveDialog suggestions 2019-06-20 17:44:39 -05:00
edc46ee824 Added _format and bitrate parameters to the transcoder so it will be more flexible. The default for mp3 now is 320 KBPS 2019-06-20 17:24:56 -05:00
7786a31c2c Revert to LibVLc v2.x due to problems to pass arguments to 3.0.7 2019-06-20 17:11:53 -05:00
ffa02088ad Allow displaying up to 50 results in youtube 2019-06-20 10:19:18 -05:00
22b3b31895 Check if credentials exists for displaying this module in the services list 2019-06-20 10:18:49 -05:00
2e67a1ae63 Check if components should be displayer for a service 2019-06-18 16:42:31 -05:00
52265c4f3e Services may be disabled from config 2019-06-18 16:36:45 -05:00
b105dd649d Added WX locale 2019-06-18 16:17:07 -05:00
ad5569f26f Updated tests with new settings 2019-06-18 16:16:46 -05:00
b090d7f896 Add options for all extractors to be enabled or disabled 2019-06-18 16:16:29 -05:00
0447974029 Added base code for settings 2019-06-17 06:01:55 -05:00
26f2da1e6d Updated changelog 2019-06-13 17:43:20 -05:00
a8d6fa84b4 Updated libvlc.dll and libvlccore.dll to 3.0.7 2019-06-13 17:43:00 -05:00
93d21868e6 Added mutagen as a dependency for a future attempt to autotag downloaded files (especially useful for Tidal) 2019-06-13 17:42:21 -05:00
a04dd9c11b File extension when downloading will be determined by the extractor itself. Fixed an issue in youtube that was making VLC to receive encrypted streams only 2019-06-13 17:41:05 -05:00
daf1610054 Removed mail.ru extractor 2019-06-13 17:39:52 -05:00
76b06090e6 Updated VLC components to version 3.0.7 2019-06-13 17:33:28 -05:00
f080977e23 Define info as a conditional value in the song model 2019-06-13 12:34:07 -05:00
cb5b0707bb Add track info from tidal API into song's classes 2019-06-13 12:33:00 -05:00
4e5941cdf4 Fixed issue with logging unicode characters in some cp1252 systems 2019-06-12 22:43:57 -05:00
6a16a66b5e fixed issue with search by title 2019-06-12 22:43:20 -05:00
dd23ce6adf fixed conditional import with testing suite 2019-06-12 22:42:51 -05:00
99d02c97f0 Add available extractors dynamically from imports of extractors package 2019-06-12 22:28:58 -05:00
d0491d8dd0 Added tidal as a service 2019-06-12 17:44:45 -05:00
efeb0fbec6 Updated version to 0.5 2019-05-06 02:20:01 -05:00
25 changed files with 1046 additions and 264 deletions

View File

@@ -1,5 +1,25 @@
## Changelog
## Version 0.6
* Added a settings dialog for the application, from this dialog you will be able to find some general settings, available for MusicDL, and service's settings. Every service defines certain specific settings.
* When searching in any service, the search should be performed without freezing the application window.
* When transcoding to mp3, the default bitrate now will be 320 KBPS instead of 192.
* When downloading, besides the status bar, there is a progress bar which will be updated with the results for the current download.
* From the settings dialog, it is possible to switch between all available output devices in the machine, so MusicDL can output audio to a different device than the default in windows.
* Added a new and experimental extractor for supporting tidal.
* Take into account that this extractor requires you to have a paid account on tidal. Depending in the account level, you will be able to play and download music in high quality or lossless audio. MusicDL will handle both. Lossless audio will be downloaded as flac files, and high quality audio will be downloaded as transcoded 320 KBPS mp3.
* There is a new search mode supported in this service. You can retrieve all work for a certain artist by using the protocol artist://, plus the name of the artist you want to retrieve. For example, artist://The beatles will retrieve everything made by the beatles available in the service. The search results will be grouped by albums, compilations and singles, in this order. Depending in the amount of results to display, this may take a long time.
* Due to recent problems with mail.ru and unavailable content in most cases, the service has been removed from MusicDL.
* YouTube:
* Fixed a long standing issue with playback of some elements, due to Youtube sending encrypted versions of these videos. Now playback should be better.
* Updated YoutubeDL to version 2019.6.7
* Now it is possible to load 50 items for searches as opposed to the previous 20 items limit. This setting can be controlled in the service's preferences
* zaycev.net:
* Fixed extractor for searching and playing music in zaycev.net.
* Unfortunately, it seems this service works only in the russian Federation and some other CIS countries due to copyright reasons.
* Updated Spanish translations.
## Version 0.4
* Fixed an error when creating a directory located in %appdata%, when using MusicDL as an installed version. MusicDL should be able to work normally again.
@@ -7,7 +27,7 @@
* MusicDL will no longer set volume at 50% when it starts. It will save the volume in a settings file, so it will remember volume settings across restarts.
* Added an option in the help menu to report an issue. You can use this feature for sending reports of problems you have encountered while using the application. You will need to provide your email address, though it will not be public anywhere. Your email address will be used only for contacting you if necessary.
* changes in Youtube module:
* Updated YoutubeDL to version 2018.10.05
* Updated YoutubeDL to latest version.
## Version 0.3

View File

@@ -9,4 +9,6 @@ youtube-dl
pyinstaller
isodate
configobj
winpaths
winpaths
tidalapi
mutagen

View File

@@ -5,7 +5,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2018-03-03 09:38+Hora est<73>ndar central (M<EFBFBD>xico)\n"
"POT-Creation-Date: 2019-06-24 13:04+Hora de verano central (Mexico)\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -15,71 +15,212 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n"
#: ../src\application.py:7
#: ../src\application.py:9
msgid " Is an application that will allow you to download music from popular sites such as youtube, zaycev.net."
msgstr ""
#: ../src\application.py:12
#: ../src\application.py:14
msgid "Manuel Cortez (Spanish)"
msgstr ""
#: ../src\controller\mainController.py:27
#: ../src\application.py:14
msgid "Valeria K (Russian)"
msgstr ""
#: ../src\controller\configuration.py:10 ../src\wxUI\mainWindow.py:15
msgid "Settings"
msgstr ""
#: ../src\controller\mainController.py:31
msgid "Ready"
msgstr ""
#: ../src\controller\mainController.py:42
#: ../src\controller\mainController.py:47
msgid "Showing {0} results."
msgstr ""
#: ../src\controller\mainController.py:46
#: ../src\controller\mainController.py:51
msgid "Shuffle on"
msgstr ""
#: ../src\controller\mainController.py:118
#: ../src\controller\mainController.py:138 ../src\wxUI\mainWindow.py:13
#: ../src\wxUI\mainWindow.py:62
msgid "Play"
msgstr ""
#: ../src\controller\mainController.py:121
#: ../src\controller\mainController.py:133
msgid "Pause"
msgstr ""
#: ../src\controller\mainController.py:213
msgid "File downloaded: {0}"
msgstr ""
#: ../src\controller\mainController.py:236
#: ../src\controller\mainController.py:107
msgid "Searching {0}... "
msgstr ""
#: ../src\controller\mainController.py:242
#: ../src\controller\mainController.py:141
#: ../src\controller\mainController.py:161 ../src\wxUI\mainWindow.py:18
#: ../src\wxUI\mainWindow.py:68
msgid "Play"
msgstr ""
#: ../src\controller\mainController.py:144
#: ../src\controller\mainController.py:156
msgid "Pause"
msgstr ""
#: ../src\controller\mainController.py:243
msgid "File downloaded: {0}"
msgstr ""
#: ../src\controller\mainController.py:263
msgid "No results found. "
msgstr ""
#: ../src\controller\player.py:43
#: ../src\controller\player.py:70
msgid "Error playing {0}. {1}."
msgstr ""
#: ../src\controller\player.py:49
#: ../src\controller\player.py:76
msgid "Playing {0}."
msgstr ""
#: ../src\controller\player.py:117 ../src\utils.py:53
#: ../src\controller\player.py:146 ../src\utils.py:58
msgid "Downloading {0}."
msgstr ""
#: ../src\controller\player.py:122 ../src\utils.py:63
#: ../src\controller\player.py:151 ../src\utils.py:69
msgid "Downloading {0} ({1}%)."
msgstr ""
#: ../src\controller\player.py:133
#: ../src\controller\player.py:164
msgid "There was an error while trying to access the file you have requested."
msgstr ""
#: ../src\controller\player.py:164 ../src\issueReporter\wx_ui.py:94
#: ../src\issueReporter\wx_ui.py:97
msgid "Error"
msgstr ""
#: ../src\controller\player.py:133
msgid "There was an error while trying to access the file you have requested."
#: ../src\extractors\tidal.py:102
msgid "Tidal"
msgstr ""
#: ../src\extractors\tidal.py:106
msgid "Lossless"
msgstr ""
#: ../src\extractors\tidal.py:106
msgid "Low"
msgstr ""
#: ../src\extractors\tidal.py:106 ../src\extractors\tidal.py:141
msgid "High"
msgstr ""
#: ../src\extractors\tidal.py:118 ../src\extractors\youtube.py:112
msgid "Enable this service"
msgstr ""
#: ../src\extractors\tidal.py:122
msgid "Tidal username or email address"
msgstr ""
#: ../src\extractors\tidal.py:130
msgid "Password"
msgstr ""
#: ../src\extractors\tidal.py:137
msgid "You can subscribe for a tidal account here"
msgstr ""
#: ../src\extractors\tidal.py:140
msgid "Audio quality"
msgstr ""
#: ../src\extractors\youtube.py:106
msgid "Youtube Settings"
msgstr ""
#: ../src\extractors\youtube.py:116
msgid "Max results per page"
msgstr ""
#: ../src\extractors\youtube.py:123
msgid "Enable transcode when downloading"
msgstr ""
#: ../src\extractors\zaycev.py:52
msgid "zaycev.net"
msgstr ""
#: ../src\extractors\zaycev.py:58
msgid "Enable this service (works only in the Russian Federation)"
msgstr ""
#: ../src\issueReporter\wx_ui.py:26 ../src\wxUI\mainWindow.py:31
msgid "Report an error"
msgstr ""
#: ../src\issueReporter\wx_ui.py:30
msgid "Briefly describe what happened. You will be able to thoroughly explain it later"
msgstr ""
#: ../src\issueReporter\wx_ui.py:40
msgid "First Name"
msgstr ""
#: ../src\issueReporter\wx_ui.py:50
msgid "Last Name"
msgstr ""
#: ../src\issueReporter\wx_ui.py:60
msgid "Email address (Will not be public)"
msgstr ""
#: ../src\issueReporter\wx_ui.py:70
msgid "Here, you can describe the bug in detail"
msgstr ""
#: ../src\issueReporter\wx_ui.py:80
msgid "I know that the {0} bug system will get my email address to contact me and fix the bug quickly"
msgstr ""
#: ../src\issueReporter\wx_ui.py:83
msgid "Send report"
msgstr ""
#: ../src\issueReporter\wx_ui.py:85
msgid "Cancel"
msgstr ""
#: ../src\issueReporter\wx_ui.py:94
msgid "You must fill out the following fields: first name, last name, email address and issue information."
msgstr ""
#: ../src\issueReporter\wx_ui.py:97
msgid "You need to mark the checkbox to provide us your email address to contact you if it is necessary."
msgstr ""
#: ../src\issueReporter\wx_ui.py:100
msgid "Thanks for reporting this bug! In future versions, you may be able to find it in the changes list. You have received an email with more information regarding your report. You've reported the bug number %i"
msgstr ""
#: ../src\issueReporter\wx_ui.py:100
msgid "reported"
msgstr ""
#: ../src\issueReporter\wx_ui.py:104
msgid "Error while reporting"
msgstr ""
#: ../src\issueReporter\wx_ui.py:104
msgid "Something unexpected occurred while trying to report the bug. Please, try again later"
msgstr ""
#: ../src\issueReporter\wx_ui.py:108
msgid "Please wait while your report is being send."
msgstr ""
#: ../src\issueReporter\wx_ui.py:108
msgid "Sending report..."
msgstr ""
#: ../src\test\test_i18n.py:21
msgid "This is a string with no special characters."
msgstr ""
#: ../src\test\test_i18n.py:24
msgid "\320\237\321\200\320\270\320\262\320\265\321\202 \320\262\321\201\320\265\320\274"
msgstr ""
#: ../src\update\utils.py:27
@@ -114,11 +255,11 @@ msgstr ""
msgid "%s seconds"
msgstr ""
#: ../src\update\wxUpdater.py:9
#: ../src\update\wxUpdater.py:10
msgid "New version for %s"
msgstr ""
#: ../src\update\wxUpdater.py:9
#: ../src\update\wxUpdater.py:10
msgid ""
"There's a new %s version available. Would you like to download it now?\n"
"\n"
@@ -128,111 +269,135 @@ msgid ""
"%s"
msgstr ""
#: ../src\update\wxUpdater.py:16
#: ../src\update\wxUpdater.py:17
msgid "Download in Progress"
msgstr ""
#: ../src\update\wxUpdater.py:16
#: ../src\update\wxUpdater.py:17
msgid "Downloading the new version..."
msgstr ""
#: ../src\update\wxUpdater.py:26
#: ../src\update\wxUpdater.py:27
msgid "Updating... %s of %s"
msgstr ""
#: ../src\update\wxUpdater.py:29
#: ../src\update\wxUpdater.py:30
msgid "Done!"
msgstr ""
#: ../src\update\wxUpdater.py:29
#: ../src\update\wxUpdater.py:30
msgid "The update has been downloaded and installed successfully. Press OK to continue."
msgstr ""
#: ../src\wxUI\mainWindow.py:14 ../src\wxUI\mainWindow.py:63
#: ../src\wxUI\configuration.py:9
msgid "Output device"
msgstr ""
#: ../src\wxUI\configuration.py:27
msgid "General"
msgstr ""
#: ../src\wxUI\configuration.py:31
msgid "Services"
msgstr ""
#: ../src\wxUI\configuration.py:34
msgid "Save"
msgstr ""
#: ../src\wxUI\configuration.py:36
msgid "Close"
msgstr ""
#: ../src\wxUI\mainWindow.py:16
msgid "Application"
msgstr ""
#: ../src\wxUI\mainWindow.py:19 ../src\wxUI\mainWindow.py:69
msgid "Stop"
msgstr ""
#: ../src\wxUI\mainWindow.py:15 ../src\wxUI\mainWindow.py:61
#: ../src\wxUI\mainWindow.py:20 ../src\wxUI\mainWindow.py:67
msgid "Previous"
msgstr ""
#: ../src\wxUI\mainWindow.py:16 ../src\wxUI\mainWindow.py:64
#: ../src\wxUI\mainWindow.py:21 ../src\wxUI\mainWindow.py:70
msgid "Next"
msgstr ""
#: ../src\wxUI\mainWindow.py:17
#: ../src\wxUI\mainWindow.py:22
msgid "Shuffle"
msgstr ""
#: ../src\wxUI\mainWindow.py:18
#: ../src\wxUI\mainWindow.py:23
msgid "Volume down"
msgstr ""
#: ../src\wxUI\mainWindow.py:19
#: ../src\wxUI\mainWindow.py:24
msgid "Volume up"
msgstr ""
#: ../src\wxUI\mainWindow.py:20
#: ../src\wxUI\mainWindow.py:25
msgid "Mute"
msgstr ""
#: ../src\wxUI\mainWindow.py:22
#: ../src\wxUI\mainWindow.py:27
msgid "About {0}"
msgstr ""
#: ../src\wxUI\mainWindow.py:23
#: ../src\wxUI\mainWindow.py:28
msgid "Check for updates"
msgstr ""
#: ../src\wxUI\mainWindow.py:24
#: ../src\wxUI\mainWindow.py:29
msgid "What's new in this version?"
msgstr ""
#: ../src\wxUI\mainWindow.py:25
#: ../src\wxUI\mainWindow.py:30
msgid "Visit website"
msgstr ""
#: ../src\wxUI\mainWindow.py:26
#: ../src\wxUI\mainWindow.py:32
msgid "Player"
msgstr ""
#: ../src\wxUI\mainWindow.py:27
#: ../src\wxUI\mainWindow.py:33
msgid "Help"
msgstr ""
#: ../src\wxUI\mainWindow.py:37
#: ../src\wxUI\mainWindow.py:43
msgid "search"
msgstr ""
#: ../src\wxUI\mainWindow.py:42
#: ../src\wxUI\mainWindow.py:48
msgid "Search in"
msgstr ""
#: ../src\wxUI\mainWindow.py:45
#: ../src\wxUI\mainWindow.py:51
msgid "Search"
msgstr ""
#: ../src\wxUI\mainWindow.py:49
#: ../src\wxUI\mainWindow.py:55
msgid "Results"
msgstr ""
#: ../src\wxUI\mainWindow.py:55
#: ../src\wxUI\mainWindow.py:61
msgid "Position"
msgstr ""
#: ../src\wxUI\mainWindow.py:58
#: ../src\wxUI\mainWindow.py:64
msgid "Volume"
msgstr ""
#: ../src\wxUI\mainWindow.py:100
#: ../src\wxUI\mainWindow.py:114
msgid "Audio Files(*.mp3)|*.mp3"
msgstr ""
#: ../src\wxUI\mainWindow.py:100
#: ../src\wxUI\mainWindow.py:114
msgid "Save this file"
msgstr ""
#: ../src\wxUI\menus.py:7
#: ../src\wxUI\menus.py:8
msgid "Play/Pause"
msgstr ""

View File

@@ -1,2 +1,19 @@
[main]
volume = integer(default=50)
language = string(default="system")
output_device = string(default="")
[services]
[[tidal]]
enabled = boolean(default=True)
username = string(default="")
password = string(default="")
quality=string(default="high")
[[youtube]]
enabled = boolean(default=True)
max_results = integer(default=20)
transcode = boolean(default=True)
[[zaycev]]
enabled = boolean(default=True)

View File

@@ -2,7 +2,7 @@
import sys
python_version = int(sys.version[0])
name = "MusicDL"
version = "0.4"
version = "0.6"
author = "Manuel Cortéz"
authorEmail = "manuel@manuelcortez.net"
copyright = "Copyright (C) 2019, Manuel Cortez"

View File

@@ -6,8 +6,8 @@ import string
class ConfigLoadError(Exception): pass
def load_config(config_path, configspec_path=None, *args, **kwargs):
if os.path.exists(config_path):
clean_config(config_path)
# if os.path.exists(config_path):
# clean_config(config_path)
spec = ConfigObj(configspec_path, encoding='UTF8', list_values=False, _inspec=True)
try:
config = ConfigObj(infile=config_path, configspec=spec, create_empty=True, encoding='UTF8', *args, **kwargs)

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import config
from utils import get_extractors
from wxUI.configuration import configurationDialog
from . import player
class configuration(object):
def __init__(self):
self.view = configurationDialog(_("Settings"))
self.create_config()
self.view.get_response()
self.save()
def create_config(self):
self.output_devices = player.player.get_output_devices()
self.view.create_general(output_devices=[i["name"] for i in self.output_devices])
current_output_device = config.app["main"]["output_device"]
for i in self.output_devices:
# here we must compare against the str version of the vlc's device identifier.
if str(i["id"]) == current_output_device:
self.view.set_value("general", "output_device", i["name"])
break
self.view.realize()
extractors = get_extractors(import_all=True)
for i in extractors:
if hasattr(i, "settings"):
panel = getattr(i, "settings")(self.view.notebook)
self.view.notebook.InsertSubPage(1, panel, panel.name)
panel.load()
if hasattr(panel, "on_enabled"):
panel.on_enabled()
def save(self):
selected_output_device = self.view.get_value("general", "output_device")
selected_device_id = None
for i in self.output_devices:
# Vlc returns everything as bytes object whereas WX works with string objects, so I need to convert the wx returned string to bytes before
# Otherwise the comparison will be false.
# toDo: Check if utf-8 would be enough or we'd have to use the fylesystem encode for handling this.
if i["name"] == bytes(selected_output_device, "utf-8"):
selected_device_id = i["id"]
break
if config.app["main"]["output_device"] != selected_device_id:
config.app["main"]["output_device"] = selected_device_id
player.player.set_output_device(config.app["main"]["output_device"])
for i in range(0, self.view.notebook.GetPageCount()):
page = self.view.notebook.GetPage(i)
if hasattr(page, "save"):
page.save()
config.app.write()

View File

@@ -13,17 +13,11 @@ from pubsub import pub
from issueReporter import issueReporter
from wxUI import mainWindow, menus
from update import updater
from . import player
from utils import get_extractors
from . import player, configuration
log = logging.getLogger("controller.main")
def get_extractors():
""" Function for importing everything wich is located in the extractors package and has a class named interface."""
import extractors
module_type = types.ModuleType
classes = [m.interface for m in extractors.__dict__.values() if type(m) == module_type and hasattr(m, 'interface')]
return sorted(classes, key=lambda c: c.name)
class Controller(object):
def __init__(self):
@@ -32,7 +26,7 @@ class Controller(object):
# Setting up the player object
player.setup()
# Get main window
self.window = mainWindow.mainWindow()
self.window = mainWindow.mainWindow(extractors=[i.interface.name for i in get_extractors()])
log.debug("Main window created")
self.window.change_status(_(u"Ready"))
# Here we will save results for searches as song objects.
@@ -67,6 +61,7 @@ class Controller(object):
widgetUtils.connect_event(self.window.list, widgetUtils.LISTBOX_ITEM_ACTIVATED, self.on_activated)
widgetUtils.connect_event(self.window.list, widgetUtils.KEYPRESS, self.on_keypress)
widgetUtils.connect_event(self.window, widgetUtils.MENU, self.on_play, menuitem=self.window.player_play)
widgetUtils.connect_event(self.window, widgetUtils.MENU, self.on_settings, menuitem=self.window.settings)
widgetUtils.connect_event(self.window, widgetUtils.MENU, self.on_next, menuitem=self.window.player_next)
widgetUtils.connect_event(self.window, widgetUtils.MENU, self.on_previous, menuitem=self.window.player_previous)
widgetUtils.connect_event(self.window, widgetUtils.MENU, self.on_stop, menuitem=self.window.player_stop)
@@ -93,10 +88,24 @@ class Controller(object):
pub.subscribe(self.change_status, "change_status")
pub.subscribe(self.on_download_finished, "download_finished")
pub.subscribe(self.on_notify, "notify")
pub.subscribe(self.on_update_progress, "update-progress")
# Event functions. These functions will call other functions in a thread and are bound to widget events.
def on_update_progress(self, value):
wx.CallAfter(self.window.progressbar.SetValue, value)
def on_settings(self, *args, **kwargs):
settings = configuration.configuration()
self.reload_extractors()
def on_search(self, *args, **kwargs):
utils.call_threaded(self.search)
text = self.window.get_text()
if text == "":
return
extractor = self.window.extractor.GetValue()
self.change_status(_(u"Searching {0}... ").format(text))
utils.call_threaded(self.search, text=text, extractor=extractor)
def on_activated(self, *args, **kwargs):
self.on_play()
@@ -179,14 +188,14 @@ class Controller(object):
def on_download(self, *args, **kwargs):
item = self.results[self.window.get_item()]
log.debug("Starting requested download: {0} (using extractor: {1})".format(item.title, self.extractor.name))
f = "{0}.mp3".format(item.format_track())
f = "{item_name}.{item_extension}".format(item_name=item.format_track(), item_extension=item.extractor.get_file_format())
if item.download_url == "":
item.get_download_url()
path = self.window.get_destination_path(f)
if path != None:
log.debug("User has requested the following path: {0}".format(path,))
if self.extractor.needs_transcode == True: # Send download to vlc based transcoder
utils.call_threaded(player.player.transcode_audio, item, path)
if self.extractor.transcoder_enabled() == True: # Send download to vlc based transcoder
utils.call_threaded(player.player.transcode_audio, item, path, _format=item.extractor.get_file_format())
else:
log.debug("downloading %s URL to %s filename" % (item.download_url, path,))
utils.call_threaded(utils.download_file, item.download_url, path)
@@ -238,25 +247,25 @@ class Controller(object):
self.window.notify(title, message)
# real functions. These functions really are doing the work.
def search(self, *args, **kwargs):
text = self.window.get_text()
if text == "":
return
extractor = self.window.extractor.GetValue()
self.change_status(_(u"Searching {0}... ").format(text))
def search(self, text, extractor, *args, **kwargs):
extractors = get_extractors()
for i in extractors:
if extractor == i.name:
self.extractor = i()
if extractor == i.interface.name:
self.extractor = i.interface()
break
log.debug("Started search for {0} (selected extractor: {1})".format(text, self.extractor.name))
self.window.list.Clear()
wx.CallAfter(self.window.list.Clear)
self.extractor.search(text)
self.results = self.extractor.results
for i in self.results:
self.window.list.Append(i.format_track())
wx.CallAfter(self.window.list.Append, i.format_track())
if len(self.results) == 0:
self.change_status(_(u"No results found. "))
wx.CallAfter(self.change_status, _(u"No results found. "))
else:
self.change_status(u"")
wx.CallAfter(self.window.list.SetFocus)
wx.CallAfter(self.change_status, u"")
wx.CallAfter(self.window.list.SetFocus)
def reload_extractors(self):
extractors = [i.interface.name for i in get_extractors()]
self.window.extractor.SetItems(extractors)
self.window.extractor.SetValue(extractors[0])

View File

@@ -5,6 +5,7 @@ import random
import vlc
import logging
import config
import time
from pubsub import pub
from utils import call_threaded
@@ -33,6 +34,27 @@ class audioPlayer(object):
self.event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, self.end_callback)
self.event_manager.event_attach(vlc.EventType.MediaPlayerEncounteredError, self.playback_error)
log.debug("Bound media playback events.")
# configure output device
self.set_output_device(config.app["main"]["output_device"])
def get_output_devices(self):
""" Retrieve enabled output devices so we can switch or use those later. """
log.debug("Retrieving output devices...")
devices = []
mods = self.player.audio_output_device_enum()
if mods:
mod = mods
while mod:
mod = mod.contents
devices.append(dict(id=mod.device, name=mod.description))
mod = mod.next
vlc.libvlc_audio_output_device_list_release(mods)
return devices
def set_output_device(self, device_id):
""" Set Output device to be ued in LibVLC"""
log.debug("Setting output audio device to {device}...".format(device=device_id,))
self.player.audio_output_device_set(None, device_id)
def play(self, item):
self.stopped = True
@@ -110,7 +132,7 @@ class audioPlayer(object):
#https://github.com/ZeBobo5/Vlc.DotNet/issues/4
call_threaded(self.next)
def transcode_audio(self, item, path):
def transcode_audio(self, item, path, _format="mp3", bitrate=320):
""" Converts given item to mp3. This method will be available when needed automatically."""
if item.download_url == "":
item.get_download_url()
@@ -118,7 +140,7 @@ class audioPlayer(object):
temporary_filename = "chunk_{0}".format(random.randint(0,2000000))
temporary_path = os.path.join(os.path.dirname(path), temporary_filename)
# Let's get a new VLC instance for transcoding this file.
transcoding_instance = vlc.Instance(*["--sout=#transcode{acodec=mp3,ab=192}:file{mux=raw,dst=\"%s\"}"% (temporary_path,)])
transcoding_instance = vlc.Instance(*["--sout=#transcode{acodec=%s,ab=%d}:file{mux=raw,dst=\"%s\"}"% (_format, bitrate, temporary_path,)])
transcoder = transcoding_instance.media_player_new()
transcoder.set_mrl(item.download_url)
pub.sendMessage("change_status", status=_(u"Downloading {0}.").format(item.title,))
@@ -127,6 +149,7 @@ class audioPlayer(object):
while True:
state = media.get_state()
pub.sendMessage("change_status", status=_("Downloading {0} ({1}%).").format(item.title, int(transcoder.get_position()*100)))
pub.sendMessage("update-progress", value=int(transcoder.get_position()*100))
if str(state) == 'State.Ended':
break
elif str(state) == 'state.error':
@@ -142,4 +165,5 @@ class audioPlayer(object):
def __del__(self):
self.event_manager.event_detach(vlc.EventType.MediaPlayerEndReached)
self.event_manager.event_detach(vlc.EventType.MediaPlayerEncounteredError, self.playback_error)
if hasattr(self, "event_manager"):
self.event_manager.event_detach(vlc.EventType.MediaPlayerEncounteredError, self.playback_error)

View File

@@ -1,3 +1,3 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from . import mailru, youtube, zaycev
from . import youtube, zaycev, tidal

70
src/extractors/base.py Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
""" Base components useful for all other extractors. """
import logging
import wx
import config
log = logging.getLogger("extractors.config")
class baseInterface(object):
name = "base"
enabled = False
needs_transcode = False
results = []
def __init__(self):
super(baseInterface, self).__init__()
log.debug("started extraction service for {0}".format(self.name,))
def search(self, text, *args, **kwargs):
raise NotImplementedError()
def get_download_url(self, url):
raise NotImplementedError()
def format_track(self, item):
raise NotImplementedError()
def get_file_format(self):
return "mp3"
def transcoder_enabled(self):
return False
class song(object):
""" Represents a song in all services. Data will be filled by the service itself"""
def __init__(self, extractor):
self.extractor = extractor
self.bitrate = 0
self.title = ""
self.artist = ""
self.duration = ""
self.size = 0
self.url = ""
self.download_url = ""
self.info = None
def format_track(self):
return self.extractor.format_track(self)
def get_download_url(self):
self.download_url = self.extractor.get_download_url(self.url)
class baseSettings(wx.Panel):
config_section = "base"
def __init__(self, *args, **kwargs):
super(baseSettings, self).__init__(*args, **kwargs)
self.map = []
def save(self):
for i in self.map:
config.app["services"][self.config_section][i[0]] = i[1].GetValue()
def load(self):
for i in self.map:
if i[0] in config.app["services"][self.config_section]:
i[1].SetValue(config.app["services"][self.config_section][i[0]])
else:
log.error("No key available: {key} on extractor {extractor}".format(key=i[0], extractor=self.config_section))

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals # at top of module
class song(object):
""" Represents a song in all services. Data will be filled by the service itself"""
def __init__(self, extractor):
self.extractor = extractor
self.bitrate = 0
self.title = ""
self.artist = ""
self.duration = ""
self.size = 0
self.url = ""
self.download_url = ""
def format_track(self):
return self.extractor.format_track(self)
def get_download_url(self):
self.download_url = self.extractor.get_download_url(self.url)

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals # at top of module
try:
import urllib.parse as urlparse
except ImportError:
import urllib as urlparse
import requests
import youtube_dl
import logging
from bs4 import BeautifulSoup
from . import baseFile
log = logging.getLogger("extractors.mail.ru")
class interface(object):
name = "mail.ru"
def __init__(self):
self.results = []
self.needs_transcode = False
log.debug("Started extraction service for mail.ru music")
def search(self, text, page=1):
if text == "" or text == None:
raise ValueError("Text must be passed and should not be blank.")
site = 'https://my.mail.ru/music/search/%s' % (text)
log.debug("Retrieving data from {0}...".format(site,))
r = requests.get(site)
soup = BeautifulSoup(r.text, 'html.parser')
search_results = soup.find_all("div", {"class": "songs-table__row__col songs-table__row__col--title title songs-table__row__col--title-hq-similar resize"})
self.results = []
for search in search_results:
data = search.find_all("a")
s = baseFile.song(self)
s.title = data[0].text.replace("\n", "").replace("\t", "")
# s.artist = data[1].text.replace("\n", "").replace("\t", "")
# print(data)
s.url = u"https://my.mail.ru"+urlparse.quote(data[0].__dict__["attrs"]["href"].encode("utf-8"))
self.results.append(s)
log.debug("{0} results found.".format(len(self.results)))
def get_download_url(self, url):
log.debug("Getting download URL for {0}".format(url,))
ydl = youtube_dl.YoutubeDL({'quiet': True, 'no_warnings': True, 'logger': log, 'format': 'bestaudio/best', 'outtmpl': u'%(id)s%(ext)s'})
with ydl:
result = ydl.extract_info(url, download=False)
if 'entries' in result:
video = result['entries'][0]
else:
video = result
log.debug("Download URL: {0}".format(video["url"],))
return video["url"]
def format_track(self, item):
return item.title

168
src/extractors/tidal.py Normal file
View File

@@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
import logging
import webbrowser
import wx
import tidalapi
import config
from update.utils import seconds_to_string
from .import base
log = logging.getLogger("extractors.tidal.com")
class interface(base.baseInterface):
name = "tidal"
enabled = config.app["services"]["tidal"].get("enabled")
# This should not be enabled if credentials are not in config.
if config.app["services"]["tidal"]["username"] == "" or config.app["services"]["tidal"]["password"] == "":
enabled = False
def __init__(self):
super(interface, self).__init__()
self.setup()
def setup(self):
# Assign quality or switch to high if not specified/not found.
if hasattr(tidalapi.Quality, config.app["services"]["tidal"]["quality"]):
quality = getattr(tidalapi.Quality, config.app["services"]["tidal"]["quality"])
else:
quality = tidalapi.Quality.high
_config = tidalapi.Config(quality=quality)
username = config.app["services"]["tidal"]["username"]
password = config.app["services"]["tidal"]["password"]
log.debug("Using quality: %s" % (quality,))
self.session = tidalapi.Session(config=_config)
self.session.login(username=username, password=password)
def get_file_format(self):
if config.app["services"]["tidal"]["quality"] == "lossless":
self.file_extension = "flac"
else:
self.file_extension = "mp3"
return self.file_extension
def transcoder_enabled(self):
if config.app["services"]["tidal"]["quality"] == "lossless":
return False
else:
return True
def search(self, text, page=1):
if text == "" or text == None:
raise ValueError("Text must be passed and should not be blank.")
log.debug("Retrieving data from Tidal...")
fieldtypes = ["artist", "album", "playlist"]
field = "track"
for i in fieldtypes:
if text.startswith(i+"://"):
field = i
text = text.replace(i+"://", "")
log.debug("Searching for %s..." % (field))
search_response = self.session.search(value=text, field=field)
self.results = []
if field == "track":
data = search_response.tracks
elif field == "artist":
data = []
artist = search_response.artists[0].id
albums = self.session.get_artist_albums(artist)
for album in albums:
tracks = self.session.get_album_tracks(album.id)
for track in tracks:
data.append(track)
compilations = self.session.get_artist_albums_other(artist)
for album in compilations:
tracks = self.session.get_album_tracks(album.id)
for track in tracks:
data.append(track)
singles = self.session.get_artist_albums_ep_singles(artist)
for album in singles:
tracks = self.session.get_album_tracks(album.id)
for track in tracks:
data.append(track)
for search_result in data:
s = base.song(self)
s.title = search_result.name
s.artist = search_result.artist.name
s.duration = seconds_to_string(search_result.duration)
s.url = search_result.id
s.info = search_result
self.results.append(s)
log.debug("{0} results found.".format(len(self.results)))
def get_download_url(self, url):
url = self.session.get_media_url(url)
if url.startswith("https://") or url.startswith("http://") == False:
url = "rtmp://"+url
return url
def format_track(self, item):
return "{title}. {artist}. {duration}".format(title=item.title, duration=item.duration, artist=item.artist)
class settings(base.baseSettings):
name = _("Tidal")
config_section = "tidal"
def get_quality_list(self):
results = dict(low=_("Low"), high=_("High"), lossless=_("Lossless"))
return results
def get_quality_value(self, *args, **kwargs):
q = self.get_quality_list()
for i in q.keys():
if q.get(i) == self.quality.GetStringSelection():
return i
def set_quality_value(self, value, *args, **kwargs):
q = self.get_quality_list()
for i in q.keys():
if i == value:
self.quality.SetStringSelection(q.get(i))
break
def __init__(self, parent):
super(settings, self).__init__(parent=parent)
sizer = wx.BoxSizer(wx.VERTICAL)
self.enabled = wx.CheckBox(self, wx.NewId(), _("Enable this service"))
self.enabled.Bind(wx.EVT_CHECKBOX, self.on_enabled)
self.map.append(("enabled", self.enabled))
sizer.Add(self.enabled, 0, wx.ALL, 5)
username = wx.StaticText(self, wx.NewId(), _("Tidal username or email address"))
self.username = wx.TextCtrl(self, wx.NewId())
usernamebox = wx.BoxSizer(wx.HORIZONTAL)
usernamebox.Add(username, 0, wx.ALL, 5)
usernamebox.Add(self.username, 0, wx.ALL, 5)
sizer.Add(usernamebox, 0, wx.ALL, 5)
self.map.append(("username", self.username))
password = wx.StaticText(self, wx.NewId(), _("Password"))
self.password = wx.TextCtrl(self, wx.NewId(), style=wx.TE_PASSWORD)
passwordbox = wx.BoxSizer(wx.HORIZONTAL)
passwordbox.Add(password, 0, wx.ALL, 5)
passwordbox.Add(self.password, 0, wx.ALL, 5)
sizer.Add(passwordbox, 0, wx.ALL, 5)
self.map.append(("password", self.password))
self.get_account = wx.Button(self, wx.NewId(), _("You can subscribe for a tidal account here"))
self.get_account.Bind(wx.EVT_BUTTON, self.on_get_account)
sizer.Add(self.get_account, 0, wx.ALL, 5)
quality = wx.StaticText(self, wx.NewId(), _("Audio quality"))
self.quality = wx.ComboBox(self, wx.NewId(), choices=[i for i in self.get_quality_list().values()], value=_("High"), style=wx.CB_READONLY)
qualitybox = wx.BoxSizer(wx.HORIZONTAL)
qualitybox.Add(quality, 0, wx.ALL, 5)
qualitybox.Add(self.quality, 0, wx.ALL, 5)
sizer.Add(qualitybox, 0, wx.ALL, 5)
# Monkeypatch for getting the right quality value here.
self.quality.GetValue = self.get_quality_value
self.quality.SetValue = self.set_quality_value
self.map.append(("quality", self.quality))
self.SetSizer(sizer)
def on_enabled(self, *args, **kwargs):
for i in self.map:
if i[1] != self.enabled:
if self.enabled.GetValue() == True:
i[1].Enable(True)
else:
i[1].Enable(False)
def on_get_account(self, *args, **kwargs):
webbrowser.open_new_tab("https://tidal.com")

View File

@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals # at top of module
import isodate
import youtube_dl
import logging
import wx
import config
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from .import baseFile
from update.utils import seconds_to_string
from .import base
DEVELOPER_KEY = "AIzaSyCU_hvZJEjLlAGAnlscquKEkE8l0lVOfn0"
YOUTUBE_API_SERVICE_NAME = "youtube"
@@ -14,13 +15,9 @@ YOUTUBE_API_VERSION = "v3"
log = logging.getLogger("extractors.youtube.com")
class interface(object):
name = "youtube"
def __init__(self):
self.results = []
self.needs_transcode = True
log.debug("started extraction service for {0}".format(self.name,))
class interface(base.baseInterface):
name = "YouTube"
enabled = config.app["services"]["youtube"].get("enabled")
def search(self, text, page=1):
if text == "" or text == None:
@@ -28,7 +25,7 @@ class interface(object):
if text.startswith("https") or text.startswith("http"):
return self.search_from_url(text)
type = "video"
max_results = 20
max_results = config.app["services"]["youtube"]["max_results"]
log.debug("Retrieving data from Youtube...")
youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=DEVELOPER_KEY)
search_response = youtube.search().list(q=text, part="id,snippet", maxResults=max_results, type=type).execute()
@@ -36,7 +33,7 @@ class interface(object):
ids = []
for search_result in search_response.get("items", []):
if search_result["id"]["kind"] == "youtube#video":
s = baseFile.song(self)
s = base.song(self)
s.title = search_result["snippet"]["title"]
ids.append(search_result["id"]["videoId"])
s.url = "https://www.youtube.com/watch?v="+search_result["id"]["videoId"]
@@ -50,7 +47,7 @@ class interface(object):
log.debug("Getting download URL for {0}".format(url,))
if "playlist?list=" in url:
return self.search_from_playlist(url)
ydl = youtube_dl.YoutubeDL({'quiet': True, 'no_warnings': True, 'logger': log, 'format': 'bestaudio/best', 'outtmpl': u'%(id)s%(ext)s'})
ydl = youtube_dl.YoutubeDL({'quiet': True, 'no_warnings': True, 'logger': log, 'prefer-free-formats': True, 'format': 'bestaudio', 'outtmpl': u'%(id)s%(ext)s'})
with ydl:
result = ydl.extract_info(url, download=False)
if 'entries' in result:
@@ -95,8 +92,43 @@ class interface(object):
video = result['entries'][0]
else:
video = result
log.debug("Download URL: {0}".format(video["url"],))
return video["url"]
# From here we should extract the first format so it will contain audio only.
log.debug("Download URL: {0}".format(video["formats"][0]["url"],))
return video["formats"][0]["url"]
def format_track(self, item):
return "{0} {1}".format(item.title, item.duration)
return "{0} {1}".format(item.title, item.duration)
def transcoder_enabled(self):
return config.app["services"]["youtube"]["transcode"]
class settings(base.baseSettings):
name = _("Youtube Settings")
config_section = "youtube"
def __init__(self, parent):
super(settings, self).__init__(parent=parent)
sizer = wx.BoxSizer(wx.VERTICAL)
self.enabled = wx.CheckBox(self, wx.NewId(), _("Enable this service"))
self.enabled.Bind(wx.EVT_CHECKBOX, self.on_enabled)
self.map.append(("enabled", self.enabled))
sizer.Add(self.enabled, 0, wx.ALL, 5)
max_results_label = wx.StaticText(self, wx.NewId(), _("Max results per page"))
self.max_results = wx.SpinCtrl(self, wx.NewId())
self.max_results.SetRange(1, 50)
max_results_sizer = wx.BoxSizer(wx.HORIZONTAL)
max_results_sizer.Add(max_results_label, 0, wx.ALL, 5)
max_results_sizer.Add(self.max_results, 0, wx.ALL, 5)
self.map.append(("max_results", self.max_results))
# self.transcode = wx.CheckBox(self, wx.NewId(), _("Enable transcode when downloading"))
# self.map.append(("transcode", self.transcode))
# sizer.Add(self.transcode, 0, wx.ALL, 5)
self.SetSizer(sizer)
def on_enabled(self, *args, **kwargs):
for i in self.map:
if i[1] != self.enabled:
if self.enabled.GetValue() == True:
i[1].Enable(True)
else:
i[1].Enable(False)

View File

@@ -1,22 +1,19 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals # at top of module
import re
import json
import requests
import logging
import wx
import config
from bs4 import BeautifulSoup
from . import baseFile
from . import base
log = logging.getLogger("extractors.zaycev.net")
class interface(object):
class interface(base.baseInterface):
name = "zaycev.net"
def __init__(self):
self.results = []
self.needs_transcode = False
log.debug("Started extraction service for zaycev.net")
enabled = config.app["services"]["zaycev"].get("enabled")
def search(self, text, page=1):
if text == "" or text == None:
@@ -31,7 +28,7 @@ class interface(object):
# The easiest method to get artist and song names is to fetch links. There are only two links per result here.
data = i.find_all("a")
# from here, data[0] contains artist info and data[1] contains info of the retrieved song.
s = baseFile.song(self)
s = base.song(self)
s.title = data[1].text
s.artist = data[0].text
s.url = "http://zaycev.net%s" % (data[1].attrs["href"])
@@ -49,4 +46,16 @@ class interface(object):
return data["url"]
def format_track(self, item):
return "{0}. {1}. {2}".format(item.title, item.duration, item.size)
return "{0}. {1}. {2}".format(item.title, item.duration, item.size)
class settings(base.baseSettings):
name = _("zaycev.net")
config_section = "zaycev"
def __init__(self, parent):
super(settings, self).__init__(parent=parent)
sizer = wx.BoxSizer(wx.VERTICAL)
self.enabled = wx.CheckBox(self, wx.NewId(), _("Enable this service (works only in the Russian Federation)"))
self.map.append(("enabled", self.enabled))
sizer.Add(self.enabled, 0, wx.ALL, 5)
self.SetSizer(sizer)

View File

@@ -5,7 +5,7 @@ CRCCheck on
ManifestSupportedOS all
XPStyle on
Name "MusicDL"
OutFile "music_dl_0.3_setup.exe"
OutFile "music_dl_0.6_setup.exe"
InstallDir "$PROGRAMFILES\musicDL"
InstallDirRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "InstallLocation"
RequestExecutionLevel admin
@@ -13,11 +13,11 @@ SetCompress auto
SetCompressor /solid lzma
SetDatablockOptimize on
VIAddVersionKey ProductName "MusicDL"
VIAddVersionKey LegalCopyright "Copyright 2018 Manuel Cortéz."
VIAddVersionKey ProductVersion "0.3"
VIAddVersionKey FileVersion "0.3"
VIProductVersion "0.3.0.0"
VIFileVersion "0.3.0.0"
VIAddVersionKey LegalCopyright "Copyright 2019 Manuel Cortez."
VIAddVersionKey ProductVersion "0.6"
VIAddVersionKey FileVersion "0.6"
VIProductVersion "0.6.0.0"
VIFileVersion "0.6.0.0"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
var StartMenuFolder
@@ -49,7 +49,7 @@ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "InstallLocation" $INSTDIR
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall" "Publisher" "Manuel Cortéz"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "DisplayVersion" "0.3"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "DisplayVersion" "0.6"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "URLInfoAbout" "https://manuelcortez.net/music_dl"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "VersionMajor" 0
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\musicDL" "VersionMinor" 1

View File

@@ -5,19 +5,19 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2018-02-28 15:02-0600\n"
"PO-Revision-Date: 2018-03-17 16:25-0600\n"
"Last-Translator: \n"
"POT-Creation-Date: 2019-06-24 13:05-0500\n"
"PO-Revision-Date: 2019-06-24 13:18-0500\n"
"Last-Translator: Manuel Cortez <manuel@manuelcortez.net>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 2.0.2\n"
"X-Generator: Poedit 2.0.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ../src\application.py:7
#: ../src\application.py:9
msgid ""
" Is an application that will allow you to download music from popular sites "
"such as youtube, zaycev.net."
@@ -25,53 +25,240 @@ msgstr ""
" Es una aplicación que te permite descargar música de sitios populares como "
"YouTube y zaycev.net."
#: ../src\application.py:12
#: ../src\application.py:14
msgid "Manuel Cortez (Spanish)"
msgstr "Manuel Cortez (Español)"
#: ../src\controller\mainController.py:27
#: ../src\application.py:14
msgid "Valeria K (Russian)"
msgstr "Valeria K (Ruso)"
#: ../src\controller\configuration.py:10 ../src\wxUI\mainWindow.py:15
msgid "Settings"
msgstr "Preferencias"
#: ../src\controller\mainController.py:31
msgid "Ready"
msgstr "Listo"
#: ../src\controller\mainController.py:42
#: ../src\controller\mainController.py:47
msgid "Showing {0} results."
msgstr "Mostrando {0} resultados"
#: ../src\controller\mainController.py:46
#: ../src\controller\mainController.py:51
msgid "Shuffle on"
msgstr "Modo aleatorio activo"
#: ../src\controller\mainController.py:105
#: ../src\controller\mainController.py:125 ../src\wxUI\mainWindow.py:13
#: ../src\wxUI\mainWindow.py:61
msgid "Play"
msgstr "Reproducir"
#: ../src\controller\mainController.py:108
#: ../src\controller\mainController.py:120
msgid "Pause"
msgstr "pausa"
#: ../src\controller\mainController.py:206
#: ../src\controller\mainController.py:107
msgid "Searching {0}... "
msgstr "Buscando {0}..."
#: ../src\controller\player.py:42
#: ../src\controller\mainController.py:141
#: ../src\controller\mainController.py:161 ../src\wxUI\mainWindow.py:18
#: ../src\wxUI\mainWindow.py:68
msgid "Play"
msgstr "Reproducir"
#: ../src\controller\mainController.py:144
#: ../src\controller\mainController.py:156
msgid "Pause"
msgstr "pausa"
#: ../src\controller\mainController.py:243
msgid "File downloaded: {0}"
msgstr "Archivo descargado: {0}"
#: ../src\controller\mainController.py:263
msgid "No results found. "
msgstr "No se han encontrado resultados."
#: ../src\controller\player.py:70
msgid "Error playing {0}. {1}."
msgstr "Error reproduciendo {0}. {1}."
#: ../src\controller\player.py:48
#: ../src\controller\player.py:76
msgid "Playing {0}."
msgstr "Reproduciendo {0}."
#: ../src\controller\player.py:116 ../src\utils.py:53
#: ../src\controller\player.py:146 ../src\utils.py:58
msgid "Downloading {0}."
msgstr "Descargando {0}."
#: ../src\controller\player.py:121 ../src\utils.py:63
#: ../src\controller\player.py:151 ../src\utils.py:69
msgid "Downloading {0} ({1}%)."
msgstr "Descargando {0} ({1}%)."
#: ../src\controller\player.py:164
msgid "There was an error while trying to access the file you have requested."
msgstr "Ocurrió un error al intentar acceder al fichero solicitado."
#: ../src\controller\player.py:164 ../src\issueReporter\wx_ui.py:94
#: ../src\issueReporter\wx_ui.py:97
msgid "Error"
msgstr "Error"
#: ../src\extractors\tidal.py:102
msgid "Tidal"
msgstr "Tidal"
#: ../src\extractors\tidal.py:106
msgid "Lossless"
msgstr "Calidad más alta (FLAC)"
#: ../src\extractors\tidal.py:106
msgid "Low"
msgstr "Baja"
#: ../src\extractors\tidal.py:106 ../src\extractors\tidal.py:141
msgid "High"
msgstr "Alta"
#: ../src\extractors\tidal.py:118 ../src\extractors\youtube.py:112
msgid "Enable this service"
msgstr "Activar servicio"
#: ../src\extractors\tidal.py:122
msgid "Tidal username or email address"
msgstr "Correo o nombre de usuario de Tidal"
#: ../src\extractors\tidal.py:130
msgid "Password"
msgstr "Contraseña"
#: ../src\extractors\tidal.py:137
msgid "You can subscribe for a tidal account here"
msgstr "Puedes obtener tu cuenta de Tidal aquí"
#: ../src\extractors\tidal.py:140
msgid "Audio quality"
msgstr "Calidad de audio"
#: ../src\extractors\youtube.py:106
msgid "Youtube Settings"
msgstr "Opciones de Youtube"
#: ../src\extractors\youtube.py:116
msgid "Max results per page"
msgstr "Resultados máximos a cargar por cada búsqueda"
#: ../src\extractors\youtube.py:123
msgid "Enable transcode when downloading"
msgstr "Activar transcodificación al descargar"
#: ../src\extractors\zaycev.py:52
msgid "zaycev.net"
msgstr "zaycev.net"
#: ../src\extractors\zaycev.py:58
msgid "Enable this service (works only in the Russian Federation)"
msgstr "Activar servicio (funciona solo en la federación de Rusia)"
#: ../src\issueReporter\wx_ui.py:26 ../src\wxUI\mainWindow.py:31
msgid "Report an error"
msgstr "Reportar un error"
#: ../src\issueReporter\wx_ui.py:30
msgid ""
"Briefly describe what happened. You will be able to thoroughly explain it "
"later"
msgstr ""
"Describe brevemente lo que ha ocurrido. Más adelante podrás añadir más "
"detalles."
#: ../src\issueReporter\wx_ui.py:40
msgid "First Name"
msgstr "Nombre (s)"
#: ../src\issueReporter\wx_ui.py:50
msgid "Last Name"
msgstr "Apellido (s)"
#: ../src\issueReporter\wx_ui.py:60
msgid "Email address (Will not be public)"
msgstr "Dirección de correo electrónico (No será publicada)"
#: ../src\issueReporter\wx_ui.py:70
msgid "Here, you can describe the bug in detail"
msgstr "Aquí puedes describir el problema con más detalles."
#: ../src\issueReporter\wx_ui.py:80
msgid ""
"I know that the {0} bug system will get my email address to contact me and "
"fix the bug quickly"
msgstr ""
"Estoy enterado que el sistema de reporte de errores de {0} podrá utilizar mi "
"correo electrónico para contactarme en caso de necesitar más detalles para "
"arreglar el problema rápidamente."
#: ../src\issueReporter\wx_ui.py:83
msgid "Send report"
msgstr "Enviar reporte"
#: ../src\issueReporter\wx_ui.py:85
msgid "Cancel"
msgstr "Cancelar"
#: ../src\issueReporter\wx_ui.py:94
msgid ""
"You must fill out the following fields: first name, last name, email address "
"and issue information."
msgstr ""
"Debes llenar los siguientes campos: Nombre, apellido, dirección de correo "
"electrónico y la información del problema."
#: ../src\issueReporter\wx_ui.py:97
msgid ""
"You need to mark the checkbox to provide us your email address to contact "
"you if it is necessary."
msgstr ""
"Debes marcar la casilla para proporcionarnos tu correo electrónico y poder "
"contactarte si es necesario."
#: ../src\issueReporter\wx_ui.py:100
msgid ""
"Thanks for reporting this bug! In future versions, you may be able to find "
"it in the changes list. You have received an email with more information "
"regarding your report. You've reported the bug number %i"
msgstr ""
"¡Gracias por reportar este error! Esperamos que puedas encontrar este "
"problema resuelto en la lista de cambios de una versión futura. Has recibido "
"un correo electrónico con más información sobre tu reporte. Has reportado el "
"error número %i"
#: ../src\issueReporter\wx_ui.py:100
msgid "reported"
msgstr "Reportado"
#: ../src\issueReporter\wx_ui.py:104
msgid "Error while reporting"
msgstr "Error al intentar reportar"
#: ../src\issueReporter\wx_ui.py:104
msgid ""
"Something unexpected occurred while trying to report the bug. Please, try "
"again later"
msgstr ""
"Algo inesperado ha ocurrido al intentar realizar el reporte de error. Por "
"favor, inténtalo nuevamente más tarde."
#: ../src\issueReporter\wx_ui.py:108
msgid "Please wait while your report is being send."
msgstr "Por favor, espera mientras tu reporte es enviado."
#: ../src\issueReporter\wx_ui.py:108
msgid "Sending report..."
msgstr "Enviando reporte..."
#: ../src\test\test_i18n.py:21
msgid "This is a string with no special characters."
msgstr ""
#: ../src\test\test_i18n.py:24
msgid ""
"\\320\\237\\321\\200\\320\\270\\320\\262\\320\\265\\321\\202 "
"\\320\\262\\321\\201\\320\\265\\320\\274"
msgstr ""
#: ../src\update\utils.py:27
msgid "%d day, "
msgstr "%d día, "
@@ -104,11 +291,11 @@ msgstr "%s segundo"
msgid "%s seconds"
msgstr "%s segundos"
#: ../src\update\wxUpdater.py:9
#: ../src\update\wxUpdater.py:10
msgid "New version for %s"
msgstr "Nueva versión de %s"
#: ../src\update\wxUpdater.py:9
#: ../src\update\wxUpdater.py:10
msgid ""
"There's a new %s version available. Would you like to download it now?\n"
"\n"
@@ -124,23 +311,23 @@ msgstr ""
"Novedades:\n"
"%s"
#: ../src\update\wxUpdater.py:16
#: ../src\update\wxUpdater.py:17
msgid "Download in Progress"
msgstr "Descarga en progreso"
#: ../src\update\wxUpdater.py:16
#: ../src\update\wxUpdater.py:17
msgid "Downloading the new version..."
msgstr "Descargando la nueva versión..."
#: ../src\update\wxUpdater.py:26
#: ../src\update\wxUpdater.py:27
msgid "Updating... %s of %s"
msgstr "Actualizando... %s de %s"
#: ../src\update\wxUpdater.py:29
#: ../src\update\wxUpdater.py:30
msgid "Done!"
msgstr "¡Hecho!"
#: ../src\update\wxUpdater.py:29
#: ../src\update\wxUpdater.py:30
msgid ""
"The update has been downloaded and installed successfully. Press OK to "
"continue."
@@ -148,87 +335,115 @@ msgstr ""
"La actualización ha sido descargada e instalada satisfactoriamente. Pulsa "
"aceptar para continuar."
#: ../src\wxUI\mainWindow.py:14 ../src\wxUI\mainWindow.py:62
#: ../src\wxUI\configuration.py:9
msgid "Output device"
msgstr "Dispositivo de salida"
#: ../src\wxUI\configuration.py:27
msgid "General"
msgstr "General"
#: ../src\wxUI\configuration.py:31
msgid "Services"
msgstr "Servicios"
#: ../src\wxUI\configuration.py:34
msgid "Save"
msgstr "Guardar"
#: ../src\wxUI\configuration.py:36
msgid "Close"
msgstr "Cerrar"
#: ../src\wxUI\mainWindow.py:16
msgid "Application"
msgstr "Aplicación"
#: ../src\wxUI\mainWindow.py:19 ../src\wxUI\mainWindow.py:69
msgid "Stop"
msgstr "Detener"
#: ../src\wxUI\mainWindow.py:15 ../src\wxUI\mainWindow.py:60
#: ../src\wxUI\mainWindow.py:20 ../src\wxUI\mainWindow.py:67
msgid "Previous"
msgstr "Anterior"
#: ../src\wxUI\mainWindow.py:16 ../src\wxUI\mainWindow.py:63
#: ../src\wxUI\mainWindow.py:21 ../src\wxUI\mainWindow.py:70
msgid "Next"
msgstr "Siguiente"
#: ../src\wxUI\mainWindow.py:17
#: ../src\wxUI\mainWindow.py:22
msgid "Shuffle"
msgstr "Aleatorio"
#: ../src\wxUI\mainWindow.py:18
#: ../src\wxUI\mainWindow.py:23
msgid "Volume down"
msgstr "Bajar volumen"
#: ../src\wxUI\mainWindow.py:19
#: ../src\wxUI\mainWindow.py:24
msgid "Volume up"
msgstr "Subir volumen"
#: ../src\wxUI\mainWindow.py:20
#: ../src\wxUI\mainWindow.py:25
msgid "Mute"
msgstr "Silenciar"
#: ../src\wxUI\mainWindow.py:22
#: ../src\wxUI\mainWindow.py:27
msgid "About {0}"
msgstr "Sobre {0}"
#: ../src\wxUI\mainWindow.py:23
#: ../src\wxUI\mainWindow.py:28
msgid "Check for updates"
msgstr "Comprobar actualizaciones"
#: ../src\wxUI\mainWindow.py:24
#: ../src\wxUI\mainWindow.py:29
msgid "What's new in this version?"
msgstr "¿qué hay de nuevo?"
#: ../src\wxUI\mainWindow.py:30
msgid "Visit website"
msgstr "Visitar sitio web"
#: ../src\wxUI\mainWindow.py:25
#: ../src\wxUI\mainWindow.py:32
msgid "Player"
msgstr "Reproductor"
#: ../src\wxUI\mainWindow.py:26
#: ../src\wxUI\mainWindow.py:33
msgid "Help"
msgstr "Ayuda"
#: ../src\wxUI\mainWindow.py:36
#: ../src\wxUI\mainWindow.py:43
msgid "search"
msgstr "Buscar"
#: ../src\wxUI\mainWindow.py:41
#: ../src\wxUI\mainWindow.py:48
msgid "Search in"
msgstr "Buscar en"
#: ../src\wxUI\mainWindow.py:44
#: ../src\wxUI\mainWindow.py:51
msgid "Search"
msgstr "Buscar"
#: ../src\wxUI\mainWindow.py:48
#: ../src\wxUI\mainWindow.py:55
msgid "Results"
msgstr "Resultados"
#: ../src\wxUI\mainWindow.py:54
#: ../src\wxUI\mainWindow.py:61
msgid "Position"
msgstr "Posición"
#: ../src\wxUI\mainWindow.py:57
#: ../src\wxUI\mainWindow.py:64
msgid "Volume"
msgstr "Volumen"
#: ../src\wxUI\mainWindow.py:99
#: ../src\wxUI\mainWindow.py:114
msgid "Audio Files(*.mp3)|*.mp3"
msgstr "Archivos de audio (*.mp3)|*.mp3"
#: ../src\wxUI\mainWindow.py:99
#: ../src\wxUI\mainWindow.py:114
msgid "Save this file"
msgstr "Guardar archivo"
#: ../src\wxUI\menus.py:7
#: ../src\wxUI\menus.py:8
msgid "Play/Pause"
msgstr "Reproducir/pausar"

Binary file not shown.

View File

@@ -12,7 +12,7 @@ import sys
storage.setup()
# Let's import config module here as it is dependent on storage being setup.
import config
logging.basicConfig(filename=os.path.join(storage.data_directory, "info.log"), level=logging.DEBUG, filemode="w")
logging.basicConfig(handlers=[logging.FileHandler(os.path.join(storage.data_directory, "info.log"), "w", "utf-8")], level=logging.DEBUG)
# Let's mute the google discovery_cache logger as we won't use it and we'll avoid some tracebacks.
glog = logging.getLogger("googleapiclient.discovery_cache")
glog.setLevel(logging.CRITICAL)

View File

@@ -5,8 +5,13 @@ import sys
import unittest
import re
import i18n
import storage
import config
storage.setup()
config.setup()
i18n.setup()
import extractors
from extractors import baseFile
from extractors import base
# Pytohn 2/3 compat
if sys.version[0] == "2":
@@ -30,7 +35,7 @@ class extractorsTestCase(unittest.TestCase):
self.assertIsInstance(len(extractor_instance.results), int)
# Take and test validity of the first item.
item = extractor_instance.results[0]
self.assertIsInstance(item, baseFile.song)
self.assertIsInstance(item, base.song)
self.assertIsInstance(item.title, strtype)
self.assertNotEqual(item.title, "")
if extractor_name == "youtube": # Duration is only available for youtube.

View File

@@ -3,6 +3,9 @@ import os
import requests
import threading
import logging
import types
import extractors
from importlib import reload
from pubsub import pub
log = logging.getLogger("utils")
@@ -12,6 +15,7 @@ def call_threaded(func, *args, **kwargs):
def new_func(*a, **k):
func(*a, **k)
thread = threading.Thread(target=new_func, args=args, kwargs=kwargs)
thread.daemon = True
thread.start()
return thread
@@ -64,6 +68,20 @@ def download_file(url, local_filename):
done = int(100 * dl / total_length)
msg = _(u"Downloading {0} ({1}%).").format(os.path.basename(local_filename), done)
pub.sendMessage("change_status", status=msg)
pub.sendMessage("update-progress", value=done)
pub.sendMessage("download_finished", file=os.path.basename(local_filename))
log.debug("Download finished successfully")
return local_filename
def get_extractors(import_all=False):
""" Function for importing everything wich is located in the extractors package and has a class named interface."""
module_type = types.ModuleType
# first of all, import all classes for the package so we can reload everything if they have changes in config.
_classes = [m for m in extractors.__dict__.values() if type(m) == module_type and hasattr(m, 'interface')]
for cls in _classes:
reload(cls)
if not import_all:
classes = [m for m in extractors.__dict__.values() if type(m) == module_type and hasattr(m, 'interface') and m.interface.enabled != False]
else:
classes = [m for m in extractors.__dict__.values() if type(m) == module_type and hasattr(m, 'interface')]
return classes#sorted(classes, key=lambda c: c.name)

51
src/wxUI/configuration.py Normal file
View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import wx
import widgetUtils
class general(wx.Panel, widgetUtils.BaseDialog):
def __init__(self, panel, output_devices=[]):
super(general, self).__init__(panel)
sizer = wx.BoxSizer(wx.VERTICAL)
output_device_label = wx.StaticText(self, wx.NewId(), _("Output device"))
self.output_device = wx.ComboBox(self, wx.NewId(), choices=output_devices, value=output_devices[0], style=wx.CB_READONLY)
output_device_box = wx.BoxSizer(wx.HORIZONTAL)
output_device_box.Add(output_device_label, 0, wx.ALL, 5)
output_device_box.Add(self.output_device, 0, wx.ALL, 5)
sizer.Add(output_device_box, 0, wx.ALL, 5)
self.SetSizer(sizer)
class configurationDialog(widgetUtils.BaseDialog):
def __init__(self, title):
super(configurationDialog, self).__init__(None, -1, title=title)
self.panel = wx.Panel(self)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.notebook = wx.Treebook(self.panel)
def create_general(self, output_devices=[]):
self.general = general(self.notebook, output_devices=output_devices)
self.notebook.AddPage(self.general, _("General"))
self.general.SetFocus()
def realize(self):
self.notebook.AddPage(wx.Panel(self.notebook, wx.NewId()), _("Services"))
self.sizer.Add(self.notebook, 0, wx.ALL, 5)
ok_cancel_box = wx.BoxSizer(wx.HORIZONTAL)
ok = wx.Button(self.panel, wx.ID_OK, _("Save"))
ok.SetDefault()
cancel = wx.Button(self.panel, wx.ID_CANCEL, _("Close"))
self.SetEscapeId(cancel.GetId())
ok_cancel_box.Add(ok, 0, wx.ALL, 5)
ok_cancel_box.Add(cancel, 0, wx.ALL, 5)
self.sizer.Add(ok_cancel_box, 0, wx.ALL, 5)
self.panel.SetSizer(self.sizer)
self.SetClientSize(self.sizer.CalcMin())
def get_value(self, panel, key):
p = getattr(self, panel)
return getattr(p, key).GetValue()
def set_value(self, panel, key, value):
p = getattr(self, panel)
control = getattr(p, key)
getattr(control, "SetValue")(value)

View File

@@ -11,8 +11,9 @@ import widgetUtils
class mainWindow(wx.Frame):
def makeMenu(self):
mb = wx.MenuBar()
# app_ = wx.Menu()
# mb.Append(app_, _(u"Application"))
app_ = wx.Menu()
self.settings = app_.Append(wx.NewId(), _("Settings"))
mb.Append(app_, _("Application"))
player = wx.Menu()
self.player_play = player.Append(wx.NewId(), _(u"Play"))
self.player_stop = player.Append(wx.NewId(), _(u"Stop"))
@@ -32,7 +33,7 @@ class mainWindow(wx.Frame):
mb.Append(help_, _(u"Help"))
self.SetMenuBar(mb)
def __init__(self):
def __init__(self, extractors=[]):
super(mainWindow, self).__init__(parent=None, id=wx.NewId(), title=application.name)
self.Maximize(True)
self.makeMenu()
@@ -45,7 +46,7 @@ class mainWindow(wx.Frame):
box.Add(lbl2, 0, wx.GROW)
box.Add(self.text, 1, wx.GROW)
box.Add(wx.StaticText(self.panel, wx.NewId(), _(u"Search in")), 0, wx.GROW)
self.extractor = wx.ComboBox(self.panel, wx.NewId(), choices=["youtube", "mail.ru", "zaycev.net"], value="youtube", style=wx.CB_READONLY)
self.extractor = wx.ComboBox(self.panel, wx.NewId(), choices=extractors, value=extractors[0], style=wx.CB_READONLY)
box.Add(self.extractor, 1, wx.GROW)
self.search = wx.Button(self.panel, wx.NewId(), _(u"Search"))
self.search.SetDefault()
@@ -73,6 +74,8 @@ class mainWindow(wx.Frame):
box2.Add(self.next)
self.sizer.Add(box1, 0, wx.GROW)
self.sizer.Add(box2, 1, wx.GROW)
self.progressbar = wx.Gauge(self.panel, wx.NewId(), range=100, style=wx.GA_HORIZONTAL)
self.sizer.Add(self.progressbar, 0, wx.ALL, 5)
self.panel.SetSizerAndFit(self.sizer)
# self.SetClientSize(self.sizer.CalcMin())
# self.Layout()