Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
a65d6a82c0 | |||
659e436dc4 | |||
ab0fc159f1 | |||
18e90b7502 | |||
c8e83d2011 | |||
4254f444db | |||
7756d71b32 | |||
bb97d017b5 | |||
64d076ce44 | |||
d33205e84e | |||
bb411e7bbc | |||
a80bfd53c1 | |||
75131a6fe6 | |||
d480e06ee3 | |||
1fcdd51358 | |||
b254a4eb1b | |||
93b066804b | |||
edc46ee824 | |||
7786a31c2c | |||
ffa02088ad | |||
22b3b31895 | |||
2e67a1ae63 | |||
52265c4f3e | |||
b105dd649d | |||
ad5569f26f | |||
b090d7f896 | |||
0447974029 | |||
26f2da1e6d | |||
a8d6fa84b4 | |||
93d21868e6 | |||
a04dd9c11b | |||
daf1610054 | |||
76b06090e6 | |||
f080977e23 | |||
cb5b0707bb | |||
4e5941cdf4 | |||
6a16a66b5e | |||
dd23ce6adf | |||
99d02c97f0 | |||
d0491d8dd0 | |||
efeb0fbec6 |
22
changes.md
22
changes.md
@@ -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
|
||||
|
||||
|
@@ -9,4 +9,6 @@ youtube-dl
|
||||
pyinstaller
|
||||
isodate
|
||||
configobj
|
||||
winpaths
|
||||
winpaths
|
||||
tidalapi
|
||||
mutagen
|
@@ -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 ""
|
||||
|
||||
|
@@ -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)
|
@@ -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"
|
||||
|
@@ -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)
|
||||
|
52
src/controller/configuration.py
Normal file
52
src/controller/configuration.py
Normal 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()
|
@@ -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])
|
@@ -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)
|
@@ -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
70
src/extractors/base.py
Normal 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))
|
@@ -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)
|
@@ -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
168
src/extractors/tidal.py
Normal 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")
|
@@ -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)
|
@@ -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)
|
@@ -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
|
||||
|
Binary file not shown.
@@ -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"
|
||||
|
||||
|
BIN
src/locales/es/LC_MESSAGES/wxstd.mo
Normal file
BIN
src/locales/es/LC_MESSAGES/wxstd.mo
Normal file
Binary file not shown.
@@ -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)
|
||||
|
@@ -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.
|
||||
|
18
src/utils.py
18
src/utils.py
@@ -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
51
src/wxUI/configuration.py
Normal 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)
|
@@ -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()
|
||||
|
Reference in New Issue
Block a user