Merge master branch and adapt it to qt5
This commit is contained in:
commit
e986ab5a4b
80
3rdparty/qocoa/qsearchfield_mac.mm
vendored
80
3rdparty/qocoa/qsearchfield_mac.mm
vendored
@ -30,6 +30,7 @@ THE SOFTWARE.
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QKeyEvent>
|
||||
|
||||
class QSearchFieldPrivate : public QObject
|
||||
{
|
||||
@ -51,8 +52,19 @@ public:
|
||||
|
||||
void returnPressed()
|
||||
{
|
||||
if (qSearchField)
|
||||
if (qSearchField) {
|
||||
emit qSearchField->returnPressed();
|
||||
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
|
||||
QApplication::postEvent(qSearchField, event);
|
||||
}
|
||||
}
|
||||
|
||||
void escapePressed()
|
||||
{
|
||||
if (qSearchField) {
|
||||
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
|
||||
QApplication::postEvent(qSearchField, event);
|
||||
}
|
||||
}
|
||||
|
||||
QPointer<QSearchField> qSearchField;
|
||||
@ -77,12 +89,16 @@ public:
|
||||
|
||||
-(void)controlTextDidEndEditing:(NSNotification*)notification {
|
||||
// No Q_ASSERT here as it is called on destruction.
|
||||
if (pimpl)
|
||||
pimpl->textDidEndEditing();
|
||||
if (!pimpl) return;
|
||||
|
||||
pimpl->textDidEndEditing();
|
||||
|
||||
if ([[[notification userInfo] objectForKey:@"NSTextMovement"] intValue] == NSReturnTextMovement)
|
||||
pimpl->returnPressed();
|
||||
else if ([[[notification userInfo] objectForKey:@"NSTextMovement"] intValue] == NSOtherTextMovement)
|
||||
pimpl->escapePressed();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface QocoaSearchField : NSSearchField
|
||||
@ -91,32 +107,40 @@ public:
|
||||
|
||||
@implementation QocoaSearchField
|
||||
-(BOOL)performKeyEquivalent:(NSEvent*)event {
|
||||
if ([event type] == NSKeyDown && [event modifierFlags] & NSCommandKeyMask)
|
||||
{
|
||||
QString keyString = toQString([event characters]);
|
||||
if (keyString == "a") // Cmd+a
|
||||
|
||||
// First, check if we have the focus.
|
||||
// If no, it probably means this event isn't for us.
|
||||
NSResponder* firstResponder = [[NSApp keyWindow] firstResponder];
|
||||
if ([firstResponder isKindOfClass:[NSText class]] &&
|
||||
[(NSText*)firstResponder delegate] == self) {
|
||||
|
||||
if ([event type] == NSKeyDown && [event modifierFlags] & NSCommandKeyMask)
|
||||
{
|
||||
[self performSelector:@selector(selectText:)];
|
||||
return YES;
|
||||
}
|
||||
else if (keyString == "c") // Cmd+c
|
||||
{
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
clipboard->setText(toQString([self stringValue]));
|
||||
return YES;
|
||||
}
|
||||
else if (keyString == "v") // Cmd+v
|
||||
{
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
[self setStringValue:fromQString(clipboard->text())];
|
||||
return YES;
|
||||
}
|
||||
else if (keyString == "x") // Cmd+x
|
||||
{
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
clipboard->setText(toQString([self stringValue]));
|
||||
[self setStringValue:@""];
|
||||
return YES;
|
||||
QString keyString = toQString([event characters]);
|
||||
if (keyString == "a") // Cmd+a
|
||||
{
|
||||
[self performSelector:@selector(selectText:)];
|
||||
return YES;
|
||||
}
|
||||
else if (keyString == "c") // Cmd+c
|
||||
{
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
clipboard->setText(toQString([self stringValue]));
|
||||
return YES;
|
||||
}
|
||||
else if (keyString == "v") // Cmd+v
|
||||
{
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
[self setStringValue:fromQString(clipboard->text())];
|
||||
return YES;
|
||||
}
|
||||
else if (keyString == "x") // Cmd+x
|
||||
{
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
clipboard->setText(toQString([self stringValue]));
|
||||
[self setStringValue:@""];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,10 +64,10 @@ find_package(Protobuf REQUIRED)
|
||||
find_package(FFTW3)
|
||||
|
||||
pkg_check_modules(CDIO libcdio)
|
||||
pkg_check_modules(CHROMAPRINT libchromaprint)
|
||||
pkg_check_modules(CHROMAPRINT REQUIRED libchromaprint)
|
||||
pkg_check_modules(GIO gio-2.0)
|
||||
pkg_check_modules(GLIB glib-2.0)
|
||||
pkg_check_modules(GOBJECT gobject-2.0)
|
||||
pkg_check_modules(GLIB REQUIRED glib-2.0)
|
||||
pkg_check_modules(GOBJECT REQUIRED gobject-2.0)
|
||||
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
|
||||
pkg_check_modules(GSTREAMER_APP REQUIRED gstreamer-app-1.0)
|
||||
pkg_check_modules(GSTREAMER_AUDIO REQUIRED gstreamer-audio-1.0)
|
||||
@ -219,6 +219,11 @@ optional_component(SEAFILE ON "Seafile support"
|
||||
DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999"
|
||||
)
|
||||
|
||||
optional_component(AMAZON_CLOUD_DRIVE ON "Amazon Cloud Drive support"
|
||||
DEPENDS "Google sparsehash" SPARSEHASH_INCLUDE_DIRS
|
||||
DEPENDS "Taglib 1.8" "TAGLIB_VERSION VERSION_GREATER 1.7.999"
|
||||
)
|
||||
|
||||
optional_component(AUDIOCD ON "Devices: Audio CD support"
|
||||
DEPENDS "libcdio" CDIO_FOUND
|
||||
)
|
||||
|
20
Changelog
20
Changelog
@ -47,6 +47,11 @@ Next release:
|
||||
* Persistent cache for pixmaps. Huge improvement of the performance when
|
||||
scrolling the library for example
|
||||
* Add AppData file for Clementine (for GNOME and KDE Software Centers)
|
||||
* Add iPod-like behaviour to previous button
|
||||
* Add "no song details" now playing widget option
|
||||
* Ability to add tracks to Spotify starred playlist by drag and drop
|
||||
* Add HipHop and Kuduro equalizers
|
||||
* Add AZLyrics lyric provider
|
||||
|
||||
Bugfixes:
|
||||
* Fix crash when click on a SoundCloud entry in internet tab
|
||||
@ -86,6 +91,16 @@ Next release:
|
||||
* Fix socket leak in moodbar
|
||||
* Fix memory leak in tagreader
|
||||
* Remove Ubuntu One support
|
||||
* Remove Discogs support
|
||||
* Fix crash when trying to fingerprint but missing a plugin
|
||||
* Fix infinite scan with Subsonic when the library is empty
|
||||
* Fix shortcut/media keys issues on Mac
|
||||
* Fix compilation issues on Yosemite
|
||||
* Fix performer tag for mpeg
|
||||
* Fix parsing issues with "innovative" datetime formats
|
||||
* Fix laggy interface on Mac
|
||||
* Fix crash in GrooveShark
|
||||
* Fix playback breaks in Spotify
|
||||
|
||||
Build system changes:
|
||||
* Update to gstreamer 1.0
|
||||
@ -93,6 +108,11 @@ Next release:
|
||||
* Use the system's sha2 library if it's available
|
||||
* (Windows) Add libgmp-10.dll which is required by libgiognutls.dll
|
||||
* (Fedora) Don't depend on libplist or usbmuxd
|
||||
* Remove libindicate-qt
|
||||
* (Debian/Ubuntu) Remove internal copy of chromaprint and add it as
|
||||
dependency
|
||||
* (Debian/Ubuntu) Add libmygpo-qt-dev (=> 1.0.7)
|
||||
* Remove internal copy of libechonest and add it as dependency
|
||||
|
||||
|
||||
|
||||
|
22
README.md
22
README.md
@ -8,6 +8,28 @@ Clementine is a modern music player and library organizer for Windows, Linux and
|
||||
- Buildbot: http://buildbot.clementine-player.org/grid
|
||||
- Latest developer builds: http://builds.clementine-player.org/
|
||||
|
||||
Opening an issue
|
||||
----------------
|
||||
### Ask for a new feature
|
||||
|
||||
Please:
|
||||
|
||||
* Check if the new feature is not already implemented (Changelog)
|
||||
* Check if another person didn't already open an issue
|
||||
* If there is already an opened issue there is no need to comment "+1", it won't help. Instead, you can subscribe to the issue to be notified of anything new about it
|
||||
|
||||
### Report a bug
|
||||
|
||||
Please:
|
||||
|
||||
* Try the latest developer build (http://builds.clementine-player.org/) to see if the bug is still present (**Attention**, those builds aren't stable so they might not work well and could sometimes break things like user settings). If it works like a charm even though you see an open issue, please comment on it and explain that the issue has been fixed
|
||||
* Check if another person has already opened the same issue to avoid duplicates
|
||||
* If there already is an open issue you could comment on it to add precisions about the problem or confirm it
|
||||
* In case there isn't, you can open a new issue with an explicit title and as much information as possible (OS, Clementine version, how to reproduce the problem...)
|
||||
* Please use http://pastebin.com/ for logs/debug
|
||||
|
||||
If there are no answers, it doesn't mean we don't care about your feature request/bug. It just means we can't reproduce the bug or haven't had time to implement it :o)
|
||||
|
||||
Compiling from source
|
||||
---------------------
|
||||
|
||||
|
@ -310,6 +310,7 @@
|
||||
<file>playstore/uk_generic_rgb_wo_45.png</file>
|
||||
<file>playstore/vi_generic_rgb_wo_45.png</file>
|
||||
<file>providers/amazon.png</file>
|
||||
<file>providers/amazonclouddrive.png</file>
|
||||
<file>providers/aol.png</file>
|
||||
<file>providers/bbc.png</file>
|
||||
<file>providers/box.png</file>
|
||||
@ -385,6 +386,8 @@
|
||||
<file>schema/schema-45.sql</file>
|
||||
<file>schema/schema-46.sql</file>
|
||||
<file>schema/schema-47.sql</file>
|
||||
<file>schema/schema-48.sql</file>
|
||||
<file>schema/schema-49.sql</file>
|
||||
<file>schema/schema-4.sql</file>
|
||||
<file>schema/schema-5.sql</file>
|
||||
<file>schema/schema-6.sql</file>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lyricproviders>
|
||||
<provider name="azlyrics.com" title="{artist} LYRICS - {title}" charset="utf-8" url="http://www.azlyrics.com/lyrics/{artist}/{title}.html">
|
||||
<urlFormat replace=" ._@,;&\/'"-" with=""/>
|
||||
<urlFormat replace=" ._@,;&\/()'"-" with=""/>
|
||||
<extract>
|
||||
<item begin="<!-- END OF RINGTONE 1 -->" end="<!-- RINGTONE 2 -->"/>
|
||||
<item begin="<!-- start of lyrics -->" end="<!-- end of lyrics -->"/>
|
||||
</extract>
|
||||
<exclude>
|
||||
<item tag="<B>"/>
|
||||
|
BIN
data/providers/amazonclouddrive.png
Normal file
BIN
data/providers/amazonclouddrive.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
50
data/schema/schema-48.sql
Normal file
50
data/schema/schema-48.sql
Normal file
@ -0,0 +1,50 @@
|
||||
CREATE TABLE amazon_cloud_drive_songs(
|
||||
title TEXT,
|
||||
album TEXT,
|
||||
artist TEXT,
|
||||
albumartist TEXT,
|
||||
composer TEXT,
|
||||
track INTEGER,
|
||||
disc INTEGER,
|
||||
bpm REAL,
|
||||
year INTEGER,
|
||||
genre TEXT,
|
||||
comment TEXT,
|
||||
compilation INTEGER,
|
||||
|
||||
length INTEGER,
|
||||
bitrate INTEGER,
|
||||
samplerate INTEGER,
|
||||
|
||||
directory INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
mtime INTEGER NOT NULL,
|
||||
ctime INTEGER NOT NULL,
|
||||
filesize INTEGER NOT NULL,
|
||||
sampler INTEGER NOT NULL DEFAULT 0,
|
||||
art_automatic TEXT,
|
||||
art_manual TEXT,
|
||||
filetype INTEGER NOT NULL DEFAULT 0,
|
||||
playcount INTEGER NOT NULL DEFAULT 0,
|
||||
lastplayed INTEGER,
|
||||
rating INTEGER,
|
||||
forced_compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||
forced_compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||
effective_compilation NOT NULL DEFAULT 0,
|
||||
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||
score INTEGER NOT NULL DEFAULT 0,
|
||||
beginning INTEGER NOT NULL DEFAULT 0,
|
||||
cue_path TEXT,
|
||||
unavailable INTEGER DEFAULT 0,
|
||||
effective_albumartist TEXT,
|
||||
etag TEXT,
|
||||
performer TEXT,
|
||||
grouping TEXT
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE amazon_cloud_drive_songs_fts USING fts3 (
|
||||
ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment,
|
||||
tokenize=unicode
|
||||
);
|
||||
|
||||
UPDATE schema_version SET version=48;
|
3
data/schema/schema-49.sql
Normal file
3
data/schema/schema-49.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE %allsongstables ADD COLUMN lyrics TEXT;
|
||||
|
||||
UPDATE schema_version SET version=49;
|
2
debian/copyright
vendored
2
debian/copyright
vendored
@ -46,9 +46,9 @@ Copyright: 2004, Melchior FRANZ <mfranz@kde.org>
|
||||
License: GPL-2+
|
||||
|
||||
Files: ext/libclementine-common/core/arraysize.h
|
||||
ext/libclementine-common/core/scoped_nsautorelease_pool.*
|
||||
src/core/scoped_nsobject.h
|
||||
src/core/scoped_cftyperef.h
|
||||
src/core/scoped_nsautorelease_pool.*
|
||||
Copyright: 2011, The Chromium Authors
|
||||
License: BSD-Google
|
||||
|
||||
|
22
dist/windows/clementine.nsi.in
vendored
22
dist/windows/clementine.nsi.in
vendored
@ -111,10 +111,32 @@ ShowUnInstDetails show
|
||||
@NORMAL@RequestExecutionLevel admin
|
||||
@PORTABLE@RequestExecutionLevel user
|
||||
|
||||
; Check for previous installation, and call the uninstaller if any
|
||||
Function CheckPreviousInstall
|
||||
|
||||
ReadRegStr $R0 ${PRODUCT_UNINST_ROOT_KEY} ${PRODUCT_UNINST_KEY} "UninstallString"
|
||||
StrCmp $R0 "" done
|
||||
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \
|
||||
"${PRODUCT_NAME} is already installed. $\n$\nClick `OK` to remove the \
|
||||
previous version or `Cancel` to cancel this upgrade." \
|
||||
IDOK uninst
|
||||
Abort
|
||||
; Run the uninstaller
|
||||
uninst:
|
||||
ClearErrors
|
||||
ExecWait '$R0 _?=$INSTDIR' ; Do not copy the uninstaller to a temp file
|
||||
|
||||
done:
|
||||
|
||||
FunctionEnd
|
||||
|
||||
Function .onInit
|
||||
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
|
||||
Call CheckPreviousInstall
|
||||
|
||||
FunctionEnd
|
||||
|
||||
Function RunClementine
|
||||
|
@ -4,11 +4,12 @@
|
||||
#import <Foundation/NSFileManager.h>
|
||||
#import <Foundation/NSPathUtilities.h>
|
||||
|
||||
#import "core/scoped_nsautorelease_pool.h"
|
||||
|
||||
namespace utilities {
|
||||
|
||||
QString GetUserDataDirectory() {
|
||||
NSAutoreleasePool* pool = [NSAutoreleasePool alloc];
|
||||
[pool init];
|
||||
ScopedNSAutoreleasePool pool;
|
||||
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
|
||||
NSUserDomainMask, YES);
|
||||
@ -19,12 +20,11 @@ QString GetUserDataDirectory() {
|
||||
} else {
|
||||
ret = "~/Library/Caches";
|
||||
}
|
||||
[pool drain];
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString GetSettingsDirectory() {
|
||||
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
|
||||
ScopedNSAutoreleasePool pool;
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(
|
||||
NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
||||
NSString* ret;
|
||||
@ -42,7 +42,7 @@ QString GetSettingsDirectory() {
|
||||
error:nil];
|
||||
|
||||
QString path = QString::fromUtf8([ret UTF8String]);
|
||||
[pool drain];
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace utilities
|
||||
|
@ -942,15 +942,15 @@ void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove this from the pending list now
|
||||
pending_playback_requests_.removeAll(req);
|
||||
|
||||
// Load the track
|
||||
sp_error error = sp_session_player_load(session_, req.track_);
|
||||
if (error != SP_ERROR_OK) {
|
||||
SendPlaybackError("Spotify playback error: " +
|
||||
QString::fromUtf8(sp_error_message(error)));
|
||||
sp_link_release(req.link_);
|
||||
|
||||
// Remove this from the pending list now
|
||||
pending_playback_requests_.removeAll(req);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -965,6 +965,9 @@ void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
|
||||
sp_session_player_play(session_, true);
|
||||
|
||||
sp_link_release(req.link_);
|
||||
|
||||
// Remove this from the pending list now
|
||||
pending_playback_requests_.removeAll(req);
|
||||
}
|
||||
|
||||
void SpotifyClient::SendPlaybackError(const QString& error) {
|
||||
|
@ -21,6 +21,10 @@ set(HEADERS
|
||||
core/workerpool.h
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
list(APPEND SOURCES core/scoped_nsautorelease_pool.mm)
|
||||
endif(APPLE)
|
||||
|
||||
qt5_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
add_library(libclementine-common STATIC
|
||||
|
@ -143,6 +143,7 @@ void TagReader::ReadFile(const QString& filename,
|
||||
|
||||
QString disc;
|
||||
QString compilation;
|
||||
QString lyrics;
|
||||
|
||||
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same
|
||||
// way;
|
||||
@ -174,6 +175,10 @@ void TagReader::ReadFile(const QString& filename,
|
||||
Decode(map["TIT1"].front()->toString(), nullptr,
|
||||
song->mutable_grouping());
|
||||
|
||||
if (!map["TOPE"].isEmpty()) // original artist/performer
|
||||
Decode(map["TOPE"].front()->toString(), nullptr,
|
||||
song->mutable_performer());
|
||||
|
||||
// Skip TPE1 (which is the artist) here because we already fetched it
|
||||
|
||||
if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
|
||||
@ -184,6 +189,12 @@ void TagReader::ReadFile(const QString& filename,
|
||||
compilation =
|
||||
TStringToQString(map["TCMP"].front()->toString()).trimmed();
|
||||
|
||||
if (!map["USLT"].isEmpty()) {
|
||||
lyrics = TStringToQString((map["USLT"].front())->toString()).trimmed();
|
||||
qLog(Debug) << "Read ULST lyrics " << lyrics;
|
||||
} else if (!map["SYLT"].isEmpty())
|
||||
lyrics = TStringToQString((map["SYLT"].front())->toString()).trimmed();
|
||||
|
||||
if (!map["APIC"].isEmpty()) song->set_art_automatic(kEmbeddedCover);
|
||||
|
||||
// Find a suitable comment tag. For now we ignore iTunNORM comments.
|
||||
@ -365,6 +376,8 @@ void TagReader::ReadFile(const QString& filename,
|
||||
song->set_compilation(compilation.toInt() == 1);
|
||||
}
|
||||
|
||||
if (!lyrics.isEmpty()) song->set_lyrics(lyrics.toStdString());
|
||||
|
||||
if (fileref->audioProperties()) {
|
||||
song->set_bitrate(fileref->audioProperties()->bitrate());
|
||||
song->set_samplerate(fileref->audioProperties()->sampleRate());
|
||||
@ -612,6 +625,8 @@ bool TagReader::SaveFile(const QString& filename,
|
||||
tag);
|
||||
SetTextFrame("TCOM", song.composer(), tag);
|
||||
SetTextFrame("TIT1", song.grouping(), tag);
|
||||
SetTextFrame("TOPE", song.performer(), tag);
|
||||
SetTextFrame("USLT", song.lyrics(), tag);
|
||||
// Skip TPE1 (which is the artist) here because we already set it
|
||||
SetTextFrame("TPE2", song.albumartist(), tag);
|
||||
SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag);
|
||||
|
@ -51,6 +51,7 @@ message SongMetadata {
|
||||
optional string etag = 30;
|
||||
optional string performer = 31;
|
||||
optional string grouping = 32;
|
||||
optional string lyrics = 33;
|
||||
}
|
||||
|
||||
message ReadFileRequest {
|
||||
|
@ -106,6 +106,7 @@ set(SOURCES
|
||||
core/stylesheetloader.cpp
|
||||
core/tagreaderclient.cpp
|
||||
core/taskmanager.cpp
|
||||
core/thread.cpp
|
||||
core/urlhandler.cpp
|
||||
core/utilities.cpp
|
||||
|
||||
@ -312,6 +313,7 @@ set(SOURCES
|
||||
songinfo/songkickconcerts.cpp
|
||||
songinfo/songkickconcertwidget.cpp
|
||||
songinfo/songplaystats.cpp
|
||||
songinfo/taglyricsinfoprovider.cpp
|
||||
songinfo/ultimatelyricslyric.cpp
|
||||
songinfo/ultimatelyricsprovider.cpp
|
||||
songinfo/ultimatelyricsreader.cpp
|
||||
@ -604,6 +606,7 @@ set(HEADERS
|
||||
songinfo/songkickconcerts.h
|
||||
songinfo/songkickconcertwidget.h
|
||||
songinfo/songplaystats.h
|
||||
songinfo/taglyricsinfoprovider.h
|
||||
songinfo/ultimatelyricslyric.h
|
||||
songinfo/ultimatelyricsprovider.h
|
||||
songinfo/ultimatelyricsreader.h
|
||||
@ -863,7 +866,6 @@ optional_source(APPLE
|
||||
core/macfslistener.mm
|
||||
core/macglobalshortcutbackend.mm
|
||||
core/mac_startup.mm
|
||||
core/scoped_nsautorelease_pool.mm
|
||||
devices/macdevicelister.mm
|
||||
engines/osxdevicefinder.cpp
|
||||
networkremote/bonjour.mm
|
||||
@ -1035,14 +1037,16 @@ optional_source(HAVE_AUDIOCD
|
||||
devices/cddadevice.cpp
|
||||
devices/cddalister.cpp
|
||||
devices/cddasongloader.cpp
|
||||
ui/ripcd.cpp
|
||||
ripper/ripcddialog.cpp
|
||||
ripper/ripper.cpp
|
||||
HEADERS
|
||||
devices/cddadevice.h
|
||||
devices/cddalister.h
|
||||
devices/cddasongloader.h
|
||||
ui/ripcd.h
|
||||
ripper/ripcddialog.h
|
||||
ripper/ripper.h
|
||||
UI
|
||||
ui/ripcd.ui
|
||||
ripper/ripcddialog.ui
|
||||
)
|
||||
|
||||
# mtp device
|
||||
@ -1176,6 +1180,20 @@ optional_source(HAVE_SEAFILE
|
||||
internet/seafile/seafilesettingspage.ui
|
||||
)
|
||||
|
||||
# Amazon Cloud Drive support
|
||||
optional_source(HAVE_AMAZON_CLOUD_DRIVE
|
||||
SOURCES
|
||||
internet/amazon/amazonclouddrive.cpp
|
||||
internet/amazon/amazonsettingspage.cpp
|
||||
internet/amazon/amazonurlhandler.cpp
|
||||
HEADERS
|
||||
internet/amazon/amazonclouddrive.h
|
||||
internet/amazon/amazonsettingspage.h
|
||||
internet/amazon/amazonurlhandler.h
|
||||
UI
|
||||
internet/amazon/amazonsettingspage.ui
|
||||
)
|
||||
|
||||
|
||||
# Pulse audio integration
|
||||
optional_source(HAVE_LIBPULSE
|
||||
|
@ -21,6 +21,7 @@
|
||||
#define CMAKE_EXECUTABLE_SUFFIX "${CMAKE_EXECUTABLE_SUFFIX}"
|
||||
|
||||
#cmakedefine ENABLE_VISUALISATIONS
|
||||
#cmakedefine HAVE_AMAZON_CLOUD_DRIVE
|
||||
#cmakedefine HAVE_AUDIOCD
|
||||
#cmakedefine HAVE_BOX
|
||||
#cmakedefine HAVE_BREAKPAD
|
||||
|
@ -47,7 +47,7 @@
|
||||
#include <QVariant>
|
||||
|
||||
const char* Database::kDatabaseFilename = "clementine.db";
|
||||
const int Database::kSchemaVersion = 47;
|
||||
const int Database::kSchemaVersion = 49;
|
||||
const char* Database::kMagicAllSongsTables = "%allsongstables";
|
||||
|
||||
int Database::sNextConnectionId = 1;
|
||||
|
@ -29,7 +29,7 @@
|
||||
class PlatformInterface;
|
||||
@class SPMediaKeyTap;
|
||||
|
||||
@interface AppDelegate : NSObject<NSApplicationDelegate> {
|
||||
@interface AppDelegate : NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate> {
|
||||
PlatformInterface* application_handler_;
|
||||
NSMenu* dock_menu_;
|
||||
MacGlobalShortcutBackend* shortcut_handler_;
|
||||
|
@ -250,13 +250,8 @@ static BreakpadRef InitBreakpad() {
|
||||
[delegate_ setShortcutHandler:shortcut_handler_];
|
||||
[self setDelegate:delegate_];
|
||||
|
||||
Class notification_center_class =
|
||||
NSClassFromString(@"NSUserNotificationCenter");
|
||||
if (notification_center_class) {
|
||||
id notification_center =
|
||||
[notification_center_class defaultUserNotificationCenter];
|
||||
[notification_center setDelegate:delegate_];
|
||||
}
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter]
|
||||
setDelegate:delegate_];
|
||||
}
|
||||
|
||||
- (void)sendEvent:(NSEvent*)event {
|
||||
@ -515,10 +510,6 @@ void DumpDictionary(CFDictionaryRef dict) {
|
||||
static const NSUInteger kFullScreenPrimary = 1 << 7;
|
||||
|
||||
void EnableFullScreen(const QWidget& main_window) {
|
||||
if (QSysInfo::MacintoshVersion == QSysInfo::MV_SNOWLEOPARD) {
|
||||
return; // Unsupported on 10.6
|
||||
}
|
||||
|
||||
NSView* view = reinterpret_cast<NSView*>(main_window.winId());
|
||||
NSWindow* window = [view window];
|
||||
[window setCollectionBehavior:kFullScreenPrimary];
|
||||
@ -526,10 +517,7 @@ void EnableFullScreen(const QWidget& main_window) {
|
||||
|
||||
float GetDevicePixelRatio(QWidget* widget) {
|
||||
NSView* view = reinterpret_cast<NSView*>(widget->winId());
|
||||
if ([[view window] respondsToSelector:@selector(backingScaleFactor)]) {
|
||||
return [[view window] backingScaleFactor];
|
||||
}
|
||||
return 1.0f;
|
||||
return [[view window] backingScaleFactor];
|
||||
}
|
||||
|
||||
} // namespace mac
|
||||
|
@ -89,15 +89,11 @@ bool MacGlobalShortcutBackend::DoRegister() {
|
||||
// Always enable media keys.
|
||||
mac::SetShortcutHandler(this);
|
||||
|
||||
if (AXAPIEnabled()) {
|
||||
for (const GlobalShortcuts::Shortcut& shortcut :
|
||||
manager_->shortcuts().values()) {
|
||||
shortcuts_[shortcut.action->shortcut()] = shortcut.action;
|
||||
}
|
||||
return p_->Register();
|
||||
for (const GlobalShortcuts::Shortcut& shortcut :
|
||||
manager_->shortcuts().values()) {
|
||||
shortcuts_[shortcut.action->shortcut()] = shortcut.action;
|
||||
}
|
||||
|
||||
return false;
|
||||
return p_->Register();
|
||||
}
|
||||
|
||||
void MacGlobalShortcutBackend::DoUnregister() {
|
||||
@ -139,33 +135,24 @@ void MacGlobalShortcutBackend::ShowAccessibilityDialog() {
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(
|
||||
NSPreferencePanesDirectory, NSSystemDomainMask, YES);
|
||||
if ([paths count] == 1) {
|
||||
NSURL* prefpane_url = nil;
|
||||
if (Utilities::GetMacVersion() < 9) {
|
||||
prefpane_url =
|
||||
[NSURL fileURLWithPath:[[paths objectAtIndex:0]
|
||||
stringByAppendingPathComponent:
|
||||
@"UniversalAccessPref.prefPane"]];
|
||||
[[NSWorkspace sharedWorkspace] openURL:prefpane_url];
|
||||
} else {
|
||||
SBSystemPreferencesApplication* system_prefs = [SBApplication
|
||||
applicationWithBundleIdentifier:@"com.apple.systempreferences"];
|
||||
[system_prefs activate];
|
||||
SBSystemPreferencesApplication* system_prefs = [SBApplication
|
||||
applicationWithBundleIdentifier:@"com.apple.systempreferences"];
|
||||
[system_prefs activate];
|
||||
|
||||
SBElementArray* panes = [system_prefs panes];
|
||||
SBSystemPreferencesPane* security_pane = nil;
|
||||
for (SBSystemPreferencesPane* pane : panes) {
|
||||
if ([[pane id] isEqualToString:@"com.apple.preference.security"]) {
|
||||
security_pane = pane;
|
||||
break;
|
||||
}
|
||||
SBElementArray* panes = [system_prefs panes];
|
||||
SBSystemPreferencesPane* security_pane = nil;
|
||||
for (SBSystemPreferencesPane* pane : panes) {
|
||||
if ([[pane id] isEqualToString:@"com.apple.preference.security"]) {
|
||||
security_pane = pane;
|
||||
break;
|
||||
}
|
||||
[system_prefs setCurrentPane:security_pane];
|
||||
}
|
||||
[system_prefs setCurrentPane:security_pane];
|
||||
|
||||
SBElementArray* anchors = [security_pane anchors];
|
||||
for (SBSystemPreferencesAnchor* anchor : anchors) {
|
||||
if ([[anchor name] isEqualToString:@"Privacy_Accessibility"]) {
|
||||
[anchor reveal];
|
||||
}
|
||||
SBElementArray* anchors = [security_pane anchors];
|
||||
for (SBSystemPreferencesAnchor* anchor : anchors) {
|
||||
if ([[anchor name] isEqualToString:@"Privacy_Accessibility"]) {
|
||||
[anchor reveal];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,11 +33,12 @@
|
||||
#include "globalsearch/searchprovider.h"
|
||||
#include "internet/digitally/digitallyimportedclient.h"
|
||||
#include "internet/core/geolocator.h"
|
||||
#include "internet/podcasts/podcastepisode.h"
|
||||
#include "internet/podcasts/podcast.h"
|
||||
#include "internet/somafm/somafmservice.h"
|
||||
#include "library/directory.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "internet/podcasts/podcastepisode.h"
|
||||
#include "internet/podcasts/podcast.h"
|
||||
#include "songinfo/collapsibleinfopane.h"
|
||||
#include "ui/equalizer.h"
|
||||
|
||||
#ifdef HAVE_VK
|
||||
@ -54,6 +55,8 @@ class GstEnginePipeline;
|
||||
class QNetworkReply;
|
||||
|
||||
void RegisterMetaTypes() {
|
||||
qRegisterMetaType<CollapsibleInfoPane::Data>("CollapsibleInfoPane::Data");
|
||||
qRegisterMetaType<ColumnAlignmentMap>("ColumnAlignmentMap");
|
||||
qRegisterMetaType<const char*>("const char*");
|
||||
qRegisterMetaType<CoverSearchResult>("CoverSearchResult");
|
||||
qRegisterMetaType<CoverSearchResults>("CoverSearchResults");
|
||||
@ -74,16 +77,16 @@ void RegisterMetaTypes() {
|
||||
qRegisterMetaType<PlaylistItemPtr>("PlaylistItemPtr");
|
||||
qRegisterMetaType<PodcastEpisodeList>("PodcastEpisodeList");
|
||||
qRegisterMetaType<PodcastList>("PodcastList");
|
||||
qRegisterMetaType<QList<CoverSearchResult> >("QList<CoverSearchResult>");
|
||||
qRegisterMetaType<QList<PlaylistItemPtr> >("QList<PlaylistItemPtr>");
|
||||
qRegisterMetaType<QList<CoverSearchResult>>("QList<CoverSearchResult>");
|
||||
qRegisterMetaType<QList<PlaylistItemPtr>>("QList<PlaylistItemPtr>");
|
||||
qRegisterMetaType<PlaylistSequence::RepeatMode>(
|
||||
"PlaylistSequence::RepeatMode");
|
||||
qRegisterMetaType<PlaylistSequence::ShuffleMode>(
|
||||
"PlaylistSequence::ShuffleMode");
|
||||
qRegisterMetaType<QList<PodcastEpisode> >("QList<PodcastEpisode>");
|
||||
qRegisterMetaType<QList<Podcast> >("QList<Podcast>");
|
||||
qRegisterMetaType<QList<QNetworkCookie> >("QList<QNetworkCookie>");
|
||||
qRegisterMetaType<QList<Song> >("QList<Song>");
|
||||
qRegisterMetaType<QList<PodcastEpisode>>("QList<PodcastEpisode>");
|
||||
qRegisterMetaType<QList<Podcast>>("QList<Podcast>");
|
||||
qRegisterMetaType<QList<QNetworkCookie>>("QList<QNetworkCookie>");
|
||||
qRegisterMetaType<QList<Song>>("QList<Song>");
|
||||
qRegisterMetaType<QNetworkCookie>("QNetworkCookie");
|
||||
qRegisterMetaType<QNetworkReply*>("QNetworkReply*");
|
||||
qRegisterMetaType<QNetworkReply**>("QNetworkReply**");
|
||||
@ -97,12 +100,12 @@ void RegisterMetaTypes() {
|
||||
qRegisterMetaTypeStreamOperators<DigitallyImportedClient::Channel>(
|
||||
"DigitallyImportedClient::Channel");
|
||||
qRegisterMetaTypeStreamOperators<Equalizer::Params>("Equalizer::Params");
|
||||
qRegisterMetaTypeStreamOperators<QMap<int, int> >("ColumnAlignmentMap");
|
||||
qRegisterMetaTypeStreamOperators<QMap<int, int>>("ColumnAlignmentMap");
|
||||
qRegisterMetaTypeStreamOperators<SomaFMService::Stream>(
|
||||
"SomaFMService::Stream");
|
||||
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
|
||||
qRegisterMetaType<Subdirectory>("Subdirectory");
|
||||
qRegisterMetaType<QList<QUrl> >("QList<QUrl>");
|
||||
qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
|
||||
|
||||
#ifdef HAVE_VK
|
||||
qRegisterMetaType<MusicOwner>("MusicOwner");
|
||||
@ -113,7 +116,7 @@ void RegisterMetaTypes() {
|
||||
qDBusRegisterMetaType<QImage>();
|
||||
qDBusRegisterMetaType<TrackMetadata>();
|
||||
qDBusRegisterMetaType<TrackIds>();
|
||||
qDBusRegisterMetaType<QList<QByteArray> >();
|
||||
qDBusRegisterMetaType<QList<QByteArray>>();
|
||||
qDBusRegisterMetaType<MprisPlaylist>();
|
||||
qDBusRegisterMetaType<MaybePlaylist>();
|
||||
qDBusRegisterMetaType<MprisPlaylistList>();
|
||||
|
@ -331,6 +331,7 @@ QVariantMap Mpris1::GetMetadata(const Song& song) {
|
||||
AddMetadata("composer", song.composer(), &ret);
|
||||
AddMetadata("performer", song.performer(), &ret);
|
||||
AddMetadata("grouping", song.grouping(), &ret);
|
||||
AddMetadata("lyrics", song.lyrics(), &ret);
|
||||
if (song.rating() != -1.0) {
|
||||
AddMetadata("rating", song.rating() * 5, &ret);
|
||||
}
|
||||
|
@ -50,7 +50,8 @@ const QStringList OrganiseFormat::kKnownTags = QStringList() << "title"
|
||||
<< "samplerate"
|
||||
<< "extension"
|
||||
<< "performer"
|
||||
<< "grouping";
|
||||
<< "grouping"
|
||||
<< "lyrics";
|
||||
|
||||
// From http://en.wikipedia.org/wiki/8.3_filename#Directory_table
|
||||
const char OrganiseFormat::kInvalidFatCharacters[] = "\"*/\\:<>?|";
|
||||
@ -191,6 +192,8 @@ QString OrganiseFormat::TagValue(const QString& tag, const Song& song) const {
|
||||
value = song.performer();
|
||||
else if (tag == "grouping")
|
||||
value = song.grouping();
|
||||
else if (tag == "lyrics")
|
||||
value = song.lyrics();
|
||||
else if (tag == "genre")
|
||||
value = song.genre();
|
||||
else if (tag == "comment")
|
||||
|
@ -92,7 +92,7 @@ class PlayerInterface : public QObject {
|
||||
virtual void Play() = 0;
|
||||
virtual void ShowOSD() = 0;
|
||||
|
||||
signals:
|
||||
signals:
|
||||
void Playing();
|
||||
void Paused();
|
||||
void Stopped();
|
||||
|
@ -111,7 +111,8 @@ const QStringList Song::kColumns = QStringList() << "title"
|
||||
<< "effective_albumartist"
|
||||
<< "etag"
|
||||
<< "performer"
|
||||
<< "grouping";
|
||||
<< "grouping"
|
||||
<< "lyrics";
|
||||
|
||||
const QString Song::kColumnSpec = Song::kColumns.join(", ");
|
||||
const QString Song::kBindSpec =
|
||||
@ -151,6 +152,7 @@ struct Song::Private : public QSharedData {
|
||||
QString composer_;
|
||||
QString performer_;
|
||||
QString grouping_;
|
||||
QString lyrics_;
|
||||
int track_;
|
||||
int disc_;
|
||||
float bpm_;
|
||||
@ -278,6 +280,7 @@ const QString& Song::playlist_albumartist() const {
|
||||
const QString& Song::composer() const { return d->composer_; }
|
||||
const QString& Song::performer() const { return d->performer_; }
|
||||
const QString& Song::grouping() const { return d->grouping_; }
|
||||
const QString& Song::lyrics() const { return d->lyrics_; }
|
||||
int Song::track() const { return d->track_; }
|
||||
int Song::disc() const { return d->disc_; }
|
||||
float Song::bpm() const { return d->bpm_; }
|
||||
@ -334,6 +337,7 @@ void Song::set_albumartist(const QString& v) { d->albumartist_ = v; }
|
||||
void Song::set_composer(const QString& v) { d->composer_ = v; }
|
||||
void Song::set_performer(const QString& v) { d->performer_ = v; }
|
||||
void Song::set_grouping(const QString& v) { d->grouping_ = v; }
|
||||
void Song::set_lyrics(const QString& v) { d->lyrics_ = v; }
|
||||
void Song::set_track(int v) { d->track_ = v; }
|
||||
void Song::set_disc(int v) { d->disc_ = v; }
|
||||
void Song::set_bpm(float v) { d->bpm_ = v; }
|
||||
@ -490,6 +494,7 @@ void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
|
||||
d->composer_ = QStringFromStdString(pb.composer());
|
||||
d->performer_ = QStringFromStdString(pb.performer());
|
||||
d->grouping_ = QStringFromStdString(pb.grouping());
|
||||
d->lyrics_ = QStringFromStdString(pb.lyrics());
|
||||
d->track_ = pb.track();
|
||||
d->disc_ = pb.disc();
|
||||
d->bpm_ = pb.bpm();
|
||||
@ -535,6 +540,7 @@ void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
|
||||
pb->set_composer(DataCommaSizeFromQString(d->composer_));
|
||||
pb->set_performer(DataCommaSizeFromQString(d->performer_));
|
||||
pb->set_grouping(DataCommaSizeFromQString(d->grouping_));
|
||||
pb->set_lyrics(DataCommaSizeFromQString(d->lyrics_));
|
||||
pb->set_track(d->track_);
|
||||
pb->set_disc(d->disc_);
|
||||
pb->set_bpm(d->bpm_);
|
||||
@ -625,6 +631,7 @@ void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
|
||||
|
||||
d->performer_ = tostr(col + 38);
|
||||
d->grouping_ = tostr(col + 39);
|
||||
d->lyrics_ = tostr(col + 40);
|
||||
|
||||
InitArtManual();
|
||||
|
||||
@ -910,9 +917,8 @@ void Song::BindToQuery(QSqlQuery* query) const {
|
||||
|
||||
if (Application::kIsPortable &&
|
||||
Utilities::UrlOnSameDriveAsClementine(d->url_)) {
|
||||
query->bindValue(":filename",
|
||||
Utilities::
|
||||
GetRelativePathToClementineBin(d->url_).toEncoded());
|
||||
query->bindValue(":filename", Utilities::GetRelativePathToClementineBin(
|
||||
d->url_).toEncoded());
|
||||
} else {
|
||||
query->bindValue(":filename", d->url_.toEncoded());
|
||||
}
|
||||
@ -950,6 +956,7 @@ void Song::BindToQuery(QSqlQuery* query) const {
|
||||
|
||||
query->bindValue(":performer", strval(d->performer_));
|
||||
query->bindValue(":grouping", strval(d->grouping_));
|
||||
query->bindValue(":lyrics", strval(d->lyrics_));
|
||||
|
||||
#undef intval
|
||||
#undef notnullintval
|
||||
@ -1058,7 +1065,8 @@ bool Song::IsMetadataEqual(const Song& other) const {
|
||||
d->samplerate_ == other.d->samplerate_ &&
|
||||
d->art_automatic_ == other.d->art_automatic_ &&
|
||||
d->art_manual_ == other.d->art_manual_ &&
|
||||
d->cue_path_ == other.d->cue_path_;
|
||||
d->cue_path_ == other.d->cue_path_ &&
|
||||
d->lyrics_ == other.d->lyrics_;
|
||||
}
|
||||
|
||||
bool Song::IsEditable() const {
|
||||
|
@ -171,6 +171,7 @@ class Song {
|
||||
const QString& composer() const;
|
||||
const QString& performer() const;
|
||||
const QString& grouping() const;
|
||||
const QString& lyrics() const;
|
||||
int track() const;
|
||||
int disc() const;
|
||||
float bpm() const;
|
||||
@ -249,6 +250,7 @@ class Song {
|
||||
void set_composer(const QString& v);
|
||||
void set_performer(const QString& v);
|
||||
void set_grouping(const QString& v);
|
||||
void set_lyrics(const QString& v);
|
||||
void set_track(int v);
|
||||
void set_disc(int v);
|
||||
void set_bpm(float v);
|
||||
|
23
src/core/thread.cpp
Normal file
23
src/core/thread.cpp
Normal file
@ -0,0 +1,23 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "thread.h"
|
||||
|
||||
void Thread::run() {
|
||||
Utilities::SetThreadIOPriority(io_priority_);
|
||||
QThread::run();
|
||||
}
|
39
src/core/thread.h
Normal file
39
src/core/thread.h
Normal file
@ -0,0 +1,39 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef CORE_THREAD_H_
|
||||
#define CORE_THREAD_H_
|
||||
|
||||
#include <QThread>
|
||||
#include "core/utilities.h"
|
||||
|
||||
// Improve QThread by adding a SetIoPriority function
|
||||
class Thread : public QThread {
|
||||
public:
|
||||
Thread(QObject* parent = nullptr)
|
||||
: QThread(parent), io_priority_(Utilities::IOPRIO_CLASS_NONE) {}
|
||||
|
||||
void SetIoPriority(Utilities::IoPriority priority) {
|
||||
io_priority_ = priority;
|
||||
}
|
||||
virtual void run() override;
|
||||
|
||||
private:
|
||||
Utilities::IoPriority io_priority_;
|
||||
};
|
||||
|
||||
#endif // CORE_THREAD_H_
|
@ -375,12 +375,6 @@ QString GetConfigPath(ConfigPath config) {
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
qint32 GetMacVersion() {
|
||||
SInt32 minor_version;
|
||||
Gestalt(gestaltSystemVersionMinor, &minor_version);
|
||||
return minor_version;
|
||||
}
|
||||
|
||||
// Better than openUrl(dirname(path)) - also highlights file at path
|
||||
void RevealFileInFinder(QString const& path) {
|
||||
QProcess::execute("/usr/bin/open", QStringList() << "-R" << path);
|
||||
@ -565,24 +559,19 @@ bool ParseUntilElement(QXmlStreamReader* reader, const QString& name) {
|
||||
}
|
||||
|
||||
QDateTime ParseRFC822DateTime(const QString& text) {
|
||||
// This sucks but we need it because some podcasts don't quite follow the
|
||||
// spec properly - they might have 1-digit hour numbers for example.
|
||||
QDateTime ret;
|
||||
QRegExp re(
|
||||
"([a-zA-Z]{3}),? (\\d{1,2}) ([a-zA-Z]{3}) (\\d{4}) "
|
||||
"(\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
|
||||
if (re.indexIn(text) != -1) {
|
||||
ret = QDateTime(
|
||||
QDate::fromString(QString("%1 %2 %3 %4")
|
||||
.arg(re.cap(1), re.cap(3), re.cap(2), re.cap(4)),
|
||||
Qt::TextDate),
|
||||
QTime(re.cap(5).toInt(), re.cap(6).toInt(), re.cap(7).toInt()));
|
||||
QRegExp regexp("(\\d{1,2}) (\\w{3,12}) (\\d+) (\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
|
||||
if (regexp.indexIn(text) == -1) {
|
||||
return QDateTime();
|
||||
}
|
||||
if (ret.isValid()) return ret;
|
||||
// Because http://feeds.feedburner.com/reasonabledoubts/Msxh?format=xml
|
||||
QRegExp re2(
|
||||
"(\\d{1,2}) ([a-zA-Z]{3}) (\\d{4}) "
|
||||
"(\\d{1,2}):(\\d{1,2}):(\\d{1,2})");
|
||||
|
||||
enum class MatchNames {
|
||||
DAYS = 1,
|
||||
MONTHS,
|
||||
YEARS,
|
||||
HOURS,
|
||||
MINUTES,
|
||||
SECONDS
|
||||
};
|
||||
|
||||
QMap<QString, int> monthmap;
|
||||
monthmap["Jan"] = 1;
|
||||
@ -597,13 +586,28 @@ QDateTime ParseRFC822DateTime(const QString& text) {
|
||||
monthmap["Oct"] = 10;
|
||||
monthmap["Nov"] = 11;
|
||||
monthmap["Dec"] = 12;
|
||||
monthmap["January"] = 1;
|
||||
monthmap["February"] = 2;
|
||||
monthmap["March"] = 3;
|
||||
monthmap["April"] = 4;
|
||||
monthmap["May"] = 5;
|
||||
monthmap["June"] = 6;
|
||||
monthmap["July"] = 7;
|
||||
monthmap["August"] = 8;
|
||||
monthmap["September"] = 9;
|
||||
monthmap["October"] = 10;
|
||||
monthmap["November"] = 11;
|
||||
monthmap["December"] = 12;
|
||||
|
||||
if (re2.indexIn(text) != -1) {
|
||||
QDate date(re2.cap(3).toInt(), monthmap[re2.cap(2)], re2.cap(1).toInt());
|
||||
ret = QDateTime(date, QTime(re2.cap(4).toInt(), re2.cap(5).toInt(),
|
||||
re2.cap(6).toInt()));
|
||||
}
|
||||
return ret;
|
||||
const QDate date(regexp.cap(static_cast<int>(MatchNames::YEARS)).toInt(),
|
||||
monthmap[regexp.cap(static_cast<int>(MatchNames::MONTHS))],
|
||||
regexp.cap(static_cast<int>(MatchNames::DAYS)).toInt());
|
||||
|
||||
const QTime time(regexp.cap(static_cast<int>(MatchNames::HOURS)).toInt(),
|
||||
regexp.cap(static_cast<int>(MatchNames::MINUTES)).toInt(),
|
||||
regexp.cap(static_cast<int>(MatchNames::SECONDS)).toInt());
|
||||
|
||||
return QDateTime (date, time);
|
||||
}
|
||||
|
||||
const char* EnumToString(const QMetaObject& meta, const char* name, int value) {
|
||||
|
@ -62,7 +62,11 @@ bool Copy(QIODevice* source, QIODevice* destination);
|
||||
|
||||
void OpenInFileBrowser(const QList<QUrl>& filenames);
|
||||
|
||||
enum HashFunction { Md5_Algo, Sha256_Algo, Sha1_Algo, };
|
||||
enum HashFunction {
|
||||
Md5_Algo,
|
||||
Sha256_Algo,
|
||||
Sha1_Algo,
|
||||
};
|
||||
QByteArray Hmac(const QByteArray& key, const QByteArray& data,
|
||||
HashFunction algo);
|
||||
QByteArray HmacMd5(const QByteArray& key, const QByteArray& data);
|
||||
@ -130,9 +134,6 @@ enum ConfigPath {
|
||||
};
|
||||
QString GetConfigPath(ConfigPath config);
|
||||
|
||||
// Returns the minor version of OS X (ie. 6 for Snow Leopard, 7 for Lion).
|
||||
qint32 GetMacVersion();
|
||||
|
||||
// Borrowed from schedutils
|
||||
enum IoPriority {
|
||||
IOPRIO_CLASS_NONE = 0,
|
||||
@ -140,7 +141,11 @@ enum IoPriority {
|
||||
IOPRIO_CLASS_BE,
|
||||
IOPRIO_CLASS_IDLE,
|
||||
};
|
||||
enum { IOPRIO_WHO_PROCESS = 1, IOPRIO_WHO_PGRP, IOPRIO_WHO_USER, };
|
||||
enum {
|
||||
IOPRIO_WHO_PROCESS = 1,
|
||||
IOPRIO_WHO_PGRP,
|
||||
IOPRIO_WHO_USER,
|
||||
};
|
||||
static const int IOPRIO_CLASS_SHIFT = 13;
|
||||
|
||||
int SetThreadIOPriority(IoPriority priority);
|
||||
|
@ -113,9 +113,6 @@ GstEngine::~GstEngine() {
|
||||
|
||||
current_pipeline_.reset();
|
||||
|
||||
// Save configuration
|
||||
gst_deinit();
|
||||
|
||||
qDeleteAll(device_finders_);
|
||||
}
|
||||
|
||||
@ -492,6 +489,17 @@ void GstEngine::Stop(bool stop_after) {
|
||||
url_ = QUrl(); // To ensure we return Empty from state()
|
||||
beginning_nanosec_ = end_nanosec_ = 0;
|
||||
|
||||
// Check if we started a fade out. If it isn't finished yet and the user
|
||||
// pressed stop, we cancel the fader and just stop the playback.
|
||||
if (is_fading_out_to_pause_) {
|
||||
disconnect(current_pipeline_.get(), SIGNAL(FaderFinished()), 0, 0);
|
||||
is_fading_out_to_pause_ = false;
|
||||
has_faded_out_ = true;
|
||||
|
||||
fadeout_pause_pipeline_.reset();
|
||||
fadeout_pipeline_.reset();
|
||||
}
|
||||
|
||||
if (fadeout_enabled_ && current_pipeline_ && !stop_after) StartFadeout();
|
||||
|
||||
current_pipeline_.reset();
|
||||
|
@ -903,6 +903,15 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin,
|
||||
g_object_set(element, "extra-headers", headers, nullptr);
|
||||
gst_structure_free(headers);
|
||||
}
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element),
|
||||
"extra-headers") &&
|
||||
instance->url().host().contains("amazonaws.com")) {
|
||||
GstStructure* headers = gst_structure_new(
|
||||
"extra-headers", "Authorization", G_TYPE_STRING,
|
||||
instance->url().fragment().toLatin1().data(), nullptr);
|
||||
g_object_set(element, "extra-headers", headers, nullptr);
|
||||
gst_structure_free(headers);
|
||||
}
|
||||
|
||||
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "user-agent")) {
|
||||
QString user_agent =
|
||||
|
244
src/internet/amazon/amazonclouddrive.cpp
Normal file
244
src/internet/amazon/amazonclouddrive.cpp
Normal file
@ -0,0 +1,244 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "internet/amazon/amazonclouddrive.h"
|
||||
|
||||
#include <QIcon>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/player.h"
|
||||
#include "core/waitforsignal.h"
|
||||
#include "internet/core/oauthenticator.h"
|
||||
#include "internet/amazon/amazonurlhandler.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "ui/settingsdialog.h"
|
||||
|
||||
const char* AmazonCloudDrive::kServiceName = "Cloud Drive";
|
||||
const char* AmazonCloudDrive::kSettingsGroup = "AmazonCloudDrive";
|
||||
|
||||
namespace {
|
||||
static const char* kServiceId = "amazon_cloud_drive";
|
||||
static const char* kClientId =
|
||||
"amzn1.application-oa2-client.2b1157a7dadc45c3888567882b3a9f05";
|
||||
static const char* kClientSecret =
|
||||
"acfbf95340cc4c381dd43fb75b5e111882d7fd1b02a02f3013ab124baf8d1655";
|
||||
static const char* kOAuthScope = "clouddrive:read";
|
||||
static const char* kOAuthEndpoint = "https://www.amazon.com/ap/oa";
|
||||
static const char* kOAuthTokenEndpoint = "https://api.amazon.com/auth/o2/token";
|
||||
|
||||
static const char* kEndpointEndpoint =
|
||||
"https://drive.amazonaws.com/drive/v1/account/endpoint";
|
||||
static const char* kChangesEndpoint = "%1/changes";
|
||||
static const char* kDownloadEndpoint = "%1/nodes/%2/content";
|
||||
} // namespace
|
||||
|
||||
AmazonCloudDrive::AmazonCloudDrive(Application* app, InternetModel* parent)
|
||||
: CloudFileService(app, parent, kServiceName, kServiceId,
|
||||
QIcon(":/providers/amazonclouddrive.png"),
|
||||
SettingsDialog::Page_AmazonCloudDrive),
|
||||
network_(new NetworkAccessManager) {
|
||||
app->player()->RegisterUrlHandler(new AmazonUrlHandler(this, this));
|
||||
}
|
||||
|
||||
bool AmazonCloudDrive::has_credentials() const {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
return !s.value("refresh_token").toString().isEmpty();
|
||||
}
|
||||
|
||||
QUrl AmazonCloudDrive::GetStreamingUrlFromSongId(const QUrl& url) {
|
||||
EnsureConnected(); // Access token must be up to date.
|
||||
QUrl download_url(
|
||||
QString(kDownloadEndpoint).arg(content_url_).arg(url.path()));
|
||||
download_url.setFragment(QString("Bearer %1").arg(access_token_));
|
||||
return download_url;
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::Connect() {
|
||||
OAuthenticator* oauth = new OAuthenticator(
|
||||
kClientId, kClientSecret,
|
||||
// Amazon forbids arbitrary query parameters so REMOTE_WITH_STATE is
|
||||
// required.
|
||||
OAuthenticator::RedirectStyle::REMOTE_WITH_STATE, this);
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
QString refresh_token = s.value("refresh_token").toString();
|
||||
if (refresh_token.isEmpty()) {
|
||||
oauth->StartAuthorisation(kOAuthEndpoint, kOAuthTokenEndpoint, kOAuthScope);
|
||||
} else {
|
||||
oauth->RefreshAuthorisation(kOAuthTokenEndpoint, refresh_token);
|
||||
}
|
||||
|
||||
NewClosure(oauth, SIGNAL(Finished()), this,
|
||||
SLOT(ConnectFinished(OAuthenticator*)), oauth);
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::EnsureConnected() {
|
||||
if (access_token_.isEmpty() ||
|
||||
QDateTime::currentDateTime().secsTo(expiry_time_) < 60) {
|
||||
Connect();
|
||||
WaitForSignal(this, SIGNAL(Connected()));
|
||||
}
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::ForgetCredentials() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.remove("");
|
||||
access_token_ = QString();
|
||||
expiry_time_ = QDateTime();
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::ConnectFinished(OAuthenticator* oauth) {
|
||||
oauth->deleteLater();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("refresh_token", oauth->refresh_token());
|
||||
|
||||
access_token_ = oauth->access_token();
|
||||
expiry_time_ = oauth->expiry_time();
|
||||
|
||||
FetchEndpoint();
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::FetchEndpoint() {
|
||||
QUrl url(kEndpointEndpoint);
|
||||
QNetworkRequest request(url);
|
||||
AddAuthorizationHeader(&request);
|
||||
QNetworkReply* reply = network_->get(request);
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(FetchEndpointFinished(QNetworkReply*)), reply);
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::FetchEndpointFinished(QNetworkReply* reply) {
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
|
||||
content_url_ = json_response["contentUrl"].toString();
|
||||
metadata_url_ = json_response["metadataUrl"].toString();
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
QString checkpoint = s.value("checkpoint", "").toString();
|
||||
RequestChanges(checkpoint);
|
||||
|
||||
// We wait until we know the endpoint URLs before emitting Connected();
|
||||
emit Connected();
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::RequestChanges(const QString& checkpoint) {
|
||||
EnsureConnected();
|
||||
QUrl url(QString(kChangesEndpoint).arg(metadata_url_));
|
||||
|
||||
QJsonDocument data;
|
||||
QJsonObject object;
|
||||
object.insert("includePurged", QJsonValue("true"));
|
||||
if (!checkpoint.isEmpty()) {
|
||||
object.insert("checkpoint", checkpoint);
|
||||
}
|
||||
data.setObject(object);
|
||||
QByteArray json = data.toBinaryData();
|
||||
|
||||
QNetworkRequest request(url);
|
||||
AddAuthorizationHeader(&request);
|
||||
QNetworkReply* reply = network_->post(request, json);
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(RequestChangesFinished(QNetworkReply*)), reply);
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::RequestChangesFinished(QNetworkReply* reply) {
|
||||
reply->deleteLater();
|
||||
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonObject json_response = QJsonDocument::fromBinaryData(data).object();
|
||||
|
||||
QString checkpoint = json_response["checkpoint"].toString();
|
||||
QSettings settings;
|
||||
settings.beginGroup(kSettingsGroup);
|
||||
settings.setValue("checkpoint", checkpoint);
|
||||
|
||||
QJsonArray nodes = json_response["nodes"].toArray();
|
||||
for (const QJsonValue& n : nodes) {
|
||||
QJsonObject node = n.toObject();
|
||||
if (node["kind"].toString() == "FOLDER") {
|
||||
// Skip directories.
|
||||
continue;
|
||||
}
|
||||
QUrl url;
|
||||
url.setScheme("amazonclouddrive");
|
||||
url.setPath("/" + node["id"].toString());
|
||||
|
||||
QString status = node["status"].toString();
|
||||
if (status == "PURGED") {
|
||||
// Remove no longer available files.
|
||||
Song song = library_backend_->GetSongByUrl(url);
|
||||
if (song.is_valid()) {
|
||||
library_backend_->DeleteSongs(SongList() << song);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (status != "AVAILABLE") {
|
||||
// Ignore any other statuses.
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject content_properties = node["contentProperties"].toObject();
|
||||
QString mime_type = content_properties["contentType"].toString();
|
||||
|
||||
if (ShouldIndexFile(url, mime_type)) {
|
||||
QString node_id = node["id"].toString();
|
||||
QUrl content_url(
|
||||
QString(kDownloadEndpoint).arg(content_url_).arg(node_id));
|
||||
QString md5 = content_properties["md5"].toString();
|
||||
|
||||
Song song;
|
||||
song.set_url(url);
|
||||
song.set_etag(md5);
|
||||
song.set_mtime(QDateTime::fromString(node["modifiedDate"].toString()).toTime_t());
|
||||
song.set_ctime(QDateTime::fromString(node["createdDate"].toString()).toTime_t());
|
||||
song.set_title(node["name"].toString());
|
||||
song.set_filesize(content_properties["size"].toInt());
|
||||
|
||||
MaybeAddFileToDatabase(song, mime_type, content_url, QString("Bearer %1").arg(access_token_));
|
||||
}
|
||||
}
|
||||
|
||||
// The API potentially returns a second JSON dictionary appended with a
|
||||
// newline at the end of the response with {"end": true} indicating that our
|
||||
// client is up to date with the latest changes.
|
||||
const int last_newline_index = data.lastIndexOf('\n');
|
||||
QByteArray last_line = data.mid(last_newline_index);
|
||||
QJsonObject end_json = QJsonDocument::fromJson(last_line).object();
|
||||
if (end_json.contains("end") && end_json["end"].toBool()) {
|
||||
return;
|
||||
} else {
|
||||
RequestChanges(checkpoint);
|
||||
}
|
||||
}
|
||||
|
||||
void AmazonCloudDrive::AddAuthorizationHeader(QNetworkRequest* request) {
|
||||
request->setRawHeader("Authorization",
|
||||
QString("Bearer %1").arg(access_token_).toUtf8());
|
||||
}
|
71
src/internet/amazon/amazonclouddrive.h
Normal file
71
src/internet/amazon/amazonclouddrive.h
Normal file
@ -0,0 +1,71 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_AMAZON_AMAZON_CLOUD_DRIVE_H_
|
||||
#define INTERNET_AMAZON_AMAZON_CLOUD_DRIVE_H_
|
||||
|
||||
#include "internet/core/cloudfileservice.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
class NetworkAccessManager;
|
||||
class OAuthenticator;
|
||||
class QNetworkReply;
|
||||
class QNetworkRequest;
|
||||
|
||||
class AmazonCloudDrive : public CloudFileService {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AmazonCloudDrive(Application* app, InternetModel* parent);
|
||||
|
||||
static const char* kServiceName;
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
virtual bool has_credentials() const;
|
||||
|
||||
QUrl GetStreamingUrlFromSongId(const QUrl& url);
|
||||
|
||||
void ForgetCredentials();
|
||||
|
||||
signals:
|
||||
void Connected();
|
||||
|
||||
public slots:
|
||||
void Connect();
|
||||
|
||||
private:
|
||||
void FetchEndpoint();
|
||||
void RequestChanges(const QString& checkpoint);
|
||||
void AddAuthorizationHeader(QNetworkRequest* request);
|
||||
void EnsureConnected();
|
||||
|
||||
private slots:
|
||||
void ConnectFinished(OAuthenticator*);
|
||||
void FetchEndpointFinished(QNetworkReply*);
|
||||
void RequestChangesFinished(QNetworkReply*);
|
||||
|
||||
private:
|
||||
NetworkAccessManager* network_;
|
||||
QString access_token_;
|
||||
QDateTime expiry_time_;
|
||||
QString content_url_;
|
||||
QString metadata_url_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_AMAZON_AMAZON_CLOUD_DRIVE_H_
|
80
src/internet/amazon/amazonsettingspage.cpp
Normal file
80
src/internet/amazon/amazonsettingspage.cpp
Normal file
@ -0,0 +1,80 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "amazonsettingspage.h"
|
||||
#include "ui_amazonsettingspage.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "internet/amazon/amazonclouddrive.h"
|
||||
#include "internet/core/internetmodel.h"
|
||||
#include "ui/settingsdialog.h"
|
||||
|
||||
AmazonSettingsPage::AmazonSettingsPage(SettingsDialog* parent)
|
||||
: SettingsPage(parent),
|
||||
ui_(new Ui::AmazonSettingsPage),
|
||||
service_(dialog()->app()->internet_model()->Service<AmazonCloudDrive>()) {
|
||||
ui_->setupUi(this);
|
||||
ui_->login_state->AddCredentialGroup(ui_->login_container);
|
||||
|
||||
connect(ui_->login_button, SIGNAL(clicked()), SLOT(LoginClicked()));
|
||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
|
||||
connect(service_, SIGNAL(Connected()), SLOT(Connected()));
|
||||
|
||||
dialog()->installEventFilter(this);
|
||||
}
|
||||
|
||||
AmazonSettingsPage::~AmazonSettingsPage() { delete ui_; }
|
||||
|
||||
void AmazonSettingsPage::Load() {
|
||||
QSettings s;
|
||||
s.beginGroup(AmazonCloudDrive::kSettingsGroup);
|
||||
|
||||
const QString token = s.value("refresh_token").toString();
|
||||
|
||||
if (!token.isEmpty()) {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
|
||||
}
|
||||
}
|
||||
|
||||
void AmazonSettingsPage::Save() {
|
||||
QSettings s;
|
||||
s.beginGroup(AmazonCloudDrive::kSettingsGroup);
|
||||
}
|
||||
|
||||
void AmazonSettingsPage::LoginClicked() {
|
||||
service_->Connect();
|
||||
ui_->login_button->setEnabled(false);
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
|
||||
}
|
||||
|
||||
bool AmazonSettingsPage::eventFilter(QObject* object, QEvent* event) {
|
||||
if (object == dialog() && event->type() == QEvent::Enter) {
|
||||
ui_->login_button->setEnabled(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return SettingsPage::eventFilter(object, event);
|
||||
}
|
||||
|
||||
void AmazonSettingsPage::LogoutClicked() {
|
||||
service_->ForgetCredentials();
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
|
||||
}
|
||||
|
||||
void AmazonSettingsPage::Connected() {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn);
|
||||
}
|
53
src/internet/amazon/amazonsettingspage.h
Normal file
53
src/internet/amazon/amazonsettingspage.h
Normal file
@ -0,0 +1,53 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_AMAZON_AMAZONSETTINGSPAGE_H_
|
||||
#define INTERNET_AMAZON_AMAZONSETTINGSPAGE_H_
|
||||
|
||||
#include "ui/settingspage.h"
|
||||
|
||||
#include <QModelIndex>
|
||||
#include <QWidget>
|
||||
|
||||
class AmazonCloudDrive;
|
||||
class Ui_AmazonSettingsPage;
|
||||
|
||||
class AmazonSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AmazonSettingsPage(SettingsDialog* parent = nullptr);
|
||||
~AmazonSettingsPage();
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
// QObject
|
||||
bool eventFilter(QObject* object, QEvent* event);
|
||||
|
||||
private slots:
|
||||
void LoginClicked();
|
||||
void LogoutClicked();
|
||||
void Connected();
|
||||
|
||||
private:
|
||||
Ui_AmazonSettingsPage* ui_;
|
||||
|
||||
AmazonCloudDrive* service_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_AMAZON_AMAZONSETTINGSPAGE_H_
|
110
src/internet/amazon/amazonsettingspage.ui
Normal file
110
src/internet/amazon/amazonsettingspage.ui
Normal file
@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AmazonSettingsPage</class>
|
||||
<widget class="QWidget" name="AmazonSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>569</width>
|
||||
<height>491</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Amazon</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/providers/amazonclouddrive.png</normaloff>:/providers/amazonclouddrive.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Clementine can play music that you have uploaded to Amazon Cloud Drive</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="login_container" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>28</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="login_button">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Clicking the Login button will open a web browser. You should return to Clementine after you have logged in.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>357</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LoginStateWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/loginstatewidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
28
src/internet/amazon/amazonurlhandler.cpp
Normal file
28
src/internet/amazon/amazonurlhandler.cpp
Normal file
@ -0,0 +1,28 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "internet/amazon/amazonurlhandler.h"
|
||||
|
||||
#include "internet/amazon/amazonclouddrive.h"
|
||||
|
||||
AmazonUrlHandler::AmazonUrlHandler(AmazonCloudDrive* service, QObject* parent)
|
||||
: UrlHandler(parent), service_(service) {}
|
||||
|
||||
UrlHandler::LoadResult AmazonUrlHandler::StartLoading(const QUrl& url) {
|
||||
return LoadResult(url, LoadResult::TrackAvailable,
|
||||
service_->GetStreamingUrlFromSongId(url));
|
||||
}
|
39
src/internet/amazon/amazonurlhandler.h
Normal file
39
src/internet/amazon/amazonurlhandler.h
Normal file
@ -0,0 +1,39 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2015, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef INTERNET_AMAZON_AMAZONURLHANDLER_H_
|
||||
#define INTERNET_AMAZON_AMAZONURLHANDLER_H_
|
||||
|
||||
#include "core/urlhandler.h"
|
||||
|
||||
class AmazonCloudDrive;
|
||||
|
||||
class AmazonUrlHandler : public UrlHandler {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AmazonUrlHandler(
|
||||
AmazonCloudDrive* service, QObject* parent = nullptr);
|
||||
|
||||
QString scheme() const { return "amazonclouddrive"; }
|
||||
QIcon icon() const { return QIcon(":providers/amazonclouddrive.png"); }
|
||||
LoadResult StartLoading(const QUrl& url);
|
||||
|
||||
private:
|
||||
AmazonCloudDrive* service_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_AMAZON_AMAZONURLHANDLER_H_
|
@ -68,8 +68,8 @@ CloudFileService::CloudFileService(Application* app, InternetModel* parent,
|
||||
library_sort_model_->setSortLocaleAware(true);
|
||||
library_sort_model_->sort(0);
|
||||
|
||||
app->global_search()->AddProvider(new CloudFileSearchProvider(
|
||||
library_backend_, service_id, icon_, this));
|
||||
app->global_search()->AddProvider(
|
||||
new CloudFileSearchProvider(library_backend_, service_id, icon_, this));
|
||||
}
|
||||
|
||||
QStandardItem* CloudFileService::CreateRootItem() {
|
||||
@ -160,6 +160,8 @@ void CloudFileService::MaybeAddFileToDatabase(const Song& metadata,
|
||||
TagReaderClient::ReplyType* reply = app_->tag_reader_client()->ReadCloudFile(
|
||||
download_url, metadata.title(), metadata.filesize(), mime_type,
|
||||
authorisation);
|
||||
pending_tagreader_replies_.append(reply);
|
||||
|
||||
NewClosure(reply, SIGNAL(Finished(bool)), this,
|
||||
SLOT(ReadTagsFinished(TagReaderClient::ReplyType*, Song)), reply,
|
||||
metadata);
|
||||
@ -167,8 +169,17 @@ void CloudFileService::MaybeAddFileToDatabase(const Song& metadata,
|
||||
|
||||
void CloudFileService::ReadTagsFinished(TagReaderClient::ReplyType* reply,
|
||||
const Song& metadata) {
|
||||
int index_reply;
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if ((index_reply = pending_tagreader_replies_.indexOf(reply)) == -1) {
|
||||
qLog(Debug) << "Ignore the reply";
|
||||
return;
|
||||
}
|
||||
|
||||
pending_tagreader_replies_.removeAt(index_reply);
|
||||
|
||||
indexing_task_progress_++;
|
||||
if (indexing_task_progress_ == indexing_task_max_) {
|
||||
task_manager_->SetTaskFinished(indexing_task_id_);
|
||||
@ -219,3 +230,12 @@ QString CloudFileService::GuessMimeTypeForFile(const QString& filename) const {
|
||||
}
|
||||
return QString::null;
|
||||
}
|
||||
|
||||
void CloudFileService::AbortReadTagsReplies() {
|
||||
qLog(Debug) << "Aborting the read tags replies";
|
||||
pending_tagreader_replies_.clear();
|
||||
|
||||
task_manager_->SetTaskFinished(indexing_task_id_);
|
||||
indexing_task_id_ = -1;
|
||||
emit AllIndexingTasksFinished();
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class CloudFileService : public InternetService {
|
||||
virtual bool has_credentials() const = 0;
|
||||
bool is_indexing() const { return indexing_task_id_ != -1; }
|
||||
|
||||
signals:
|
||||
signals:
|
||||
void AllIndexingTasksFinished();
|
||||
|
||||
public slots:
|
||||
@ -67,6 +67,7 @@ class CloudFileService : public InternetService {
|
||||
const QString& authorisation);
|
||||
virtual bool IsSupportedMimeType(const QString& mime_type) const;
|
||||
QString GuessMimeTypeForFile(const QString& filename) const;
|
||||
void AbortReadTagsReplies();
|
||||
|
||||
protected slots:
|
||||
void ShowCoverManager();
|
||||
@ -86,6 +87,7 @@ class CloudFileService : public InternetService {
|
||||
std::unique_ptr<AlbumCoverManager> cover_manager_;
|
||||
PlaylistManager* playlist_manager_;
|
||||
TaskManager* task_manager_;
|
||||
QList<TagReaderClient::ReplyType*> pending_tagreader_replies_;
|
||||
|
||||
private:
|
||||
QIcon icon_;
|
||||
|
@ -64,6 +64,9 @@
|
||||
#ifdef HAVE_SEAFILE
|
||||
#include "internet/seafile/seafileservice.h"
|
||||
#endif
|
||||
#ifdef HAVE_AMAZON_CLOUD_DRIVE
|
||||
#include "internet/amazon/amazonclouddrive.h"
|
||||
#endif
|
||||
|
||||
using smart_playlists::Generator;
|
||||
using smart_playlists::GeneratorMimeData;
|
||||
@ -117,6 +120,9 @@ InternetModel::InternetModel(Application* app, QObject* parent)
|
||||
#ifdef HAVE_VK
|
||||
AddService(new VkService(app, this));
|
||||
#endif
|
||||
#ifdef HAVE_AMAZON_CLOUD_DRIVE
|
||||
AddService(new AmazonCloudDrive(app, this));
|
||||
#endif
|
||||
|
||||
invisibleRootItem()->sortChildren(0, Qt::AscendingOrder);
|
||||
UpdateServices();
|
||||
|
@ -299,6 +299,7 @@ void GroovesharkService::SearchSongsFinished(QNetworkReply* reply) {
|
||||
task_search_id_ = 0;
|
||||
|
||||
// Fill results list
|
||||
if (!search_) return;
|
||||
for (const Song& song : songs) {
|
||||
QStandardItem* child = CreateSongItem(song);
|
||||
search_->appendRow(child);
|
||||
|
@ -103,6 +103,7 @@ void PodcastDeleter::AutoDelete() {
|
||||
timeout_ms = QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
timeout_ms -= oldest_episode_time.toMSecsSinceEpoch();
|
||||
timeout_ms = (delete_after_secs_ * kMsecPerSec) - timeout_ms;
|
||||
qLog(Info) << "Timeout for autodelete set to:" << timeout_ms <<"ms";
|
||||
if (timeout_ms >= 0) {
|
||||
auto_delete_timer_->setInterval(timeout_ms);
|
||||
} else {
|
||||
|
@ -49,7 +49,7 @@ Task::Task(PodcastEpisode episode, QFile* file, PodcastBackend* backend)
|
||||
connect(repl.get(), SIGNAL(finished()), SLOT(finishedInternal()));
|
||||
connect(repl.get(), SIGNAL(downloadProgress(qint64, qint64)),
|
||||
SLOT(downloadProgressInternal(qint64, qint64)));
|
||||
emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
|
||||
emit ProgressChanged(episode_, PodcastDownload::Queued, 0);
|
||||
}
|
||||
|
||||
PodcastEpisode Task::episode() const { return episode_; }
|
||||
|
@ -29,6 +29,7 @@
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/player.h"
|
||||
#include "core/waitforsignal.h"
|
||||
#include "internet/seafile/seafileurlhandler.h"
|
||||
@ -54,7 +55,11 @@ static const int kMaxTries = 10;
|
||||
SeafileService::SeafileService(Application* app, InternetModel* parent)
|
||||
: CloudFileService(app, parent, kServiceName, kSettingsGroup,
|
||||
QIcon(":/providers/seafile.png"),
|
||||
SettingsDialog::Page_Seafile) {
|
||||
SettingsDialog::Page_Seafile),
|
||||
indexing_task_id_(-1),
|
||||
indexing_task_max_(0),
|
||||
indexing_task_progress_(0),
|
||||
changing_libary_(false) {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
access_token_ = s.value("access_token").toString();
|
||||
@ -179,6 +184,21 @@ void SeafileService::GetLibrariesFinished(QNetworkReply* reply) {
|
||||
}
|
||||
|
||||
void SeafileService::ChangeLibrary(const QString& new_library) {
|
||||
if (new_library == library_updated_ || changing_libary_) return;
|
||||
|
||||
if (indexing_task_id_ != -1) {
|
||||
qLog(Debug) << "Want to change the Seafile library, but Clementine waits "
|
||||
"the previous indexing...";
|
||||
changing_libary_ = true;
|
||||
NewClosure(this, SIGNAL(UpdatingLibrariesFinishedSignal()), this,
|
||||
SLOT(ChangeLibrary(QString)), new_library);
|
||||
return;
|
||||
}
|
||||
|
||||
AbortReadTagsReplies();
|
||||
|
||||
qLog(Debug) << "Change the Seafile library";
|
||||
|
||||
// Every other libraries have to be destroyed from the tree
|
||||
if (new_library != "all") {
|
||||
for (SeafileTree::TreeItem* library : tree_.libraries()) {
|
||||
@ -188,6 +208,7 @@ void SeafileService::ChangeLibrary(const QString& new_library) {
|
||||
}
|
||||
}
|
||||
|
||||
changing_libary_ = false;
|
||||
UpdateLibraries();
|
||||
}
|
||||
|
||||
@ -200,6 +221,14 @@ void SeafileService::Connect() {
|
||||
}
|
||||
|
||||
void SeafileService::UpdateLibraries() {
|
||||
// Quit if we are already updating the libraries
|
||||
if (indexing_task_id_ != -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
indexing_task_id_ =
|
||||
app_->task_manager()->StartTask(tr("Building Seafile index..."));
|
||||
|
||||
connect(this, SIGNAL(GetLibrariesFinishedSignal(QMap<QString, QString>)),
|
||||
this, SLOT(UpdateLibrariesInProgress(QMap<QString, QString>)));
|
||||
|
||||
@ -215,14 +244,20 @@ void SeafileService::UpdateLibrariesInProgress(
|
||||
s.beginGroup(kSettingsGroup);
|
||||
QString library_to_update = s.value("library").toString();
|
||||
|
||||
// If the library doesn't change, we don't need to update
|
||||
// If the library didn't change, we don't need to update
|
||||
if (!library_updated_.isNull() && library_updated_ == library_to_update) {
|
||||
app_->task_manager()->SetTaskFinished(indexing_task_id_);
|
||||
indexing_task_id_ = -1;
|
||||
UpdatingLibrariesFinishedSignal();
|
||||
return;
|
||||
}
|
||||
|
||||
library_updated_ = library_to_update;
|
||||
|
||||
if (library_to_update == "none") {
|
||||
app_->task_manager()->SetTaskFinished(indexing_task_id_);
|
||||
indexing_task_id_ = -1;
|
||||
UpdatingLibrariesFinishedSignal();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -236,7 +271,7 @@ void SeafileService::UpdateLibrariesInProgress(
|
||||
SeafileTree::Entry(library.value(), library.key(),
|
||||
SeafileTree::Entry::LIBRARY),
|
||||
"/");
|
||||
// If not, we can destroy the library from the tree
|
||||
// If not, we can destroy the library from the tree
|
||||
} else {
|
||||
// If the library was not in the tree, it's not a problem because
|
||||
// DeleteEntry won't do anything
|
||||
@ -245,6 +280,13 @@ void SeafileService::UpdateLibrariesInProgress(
|
||||
SeafileTree::Entry::LIBRARY));
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't do anything, set the task finished
|
||||
if (indexing_task_max_ == 0) {
|
||||
app_->task_manager()->SetTaskFinished(indexing_task_id_);
|
||||
indexing_task_id_ = -1;
|
||||
UpdatingLibrariesFinishedSignal();
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkReply* SeafileService::PrepareFetchFolderItems(const QString& library,
|
||||
@ -263,6 +305,8 @@ QNetworkReply* SeafileService::PrepareFetchFolderItems(const QString& library,
|
||||
|
||||
void SeafileService::FetchAndCheckFolderItems(const SeafileTree::Entry& library,
|
||||
const QString& path) {
|
||||
StartTaskInProgress();
|
||||
|
||||
QNetworkReply* reply = PrepareFetchFolderItems(library.id(), path);
|
||||
NewClosure(reply, SIGNAL(finished()), this,
|
||||
SLOT(FetchAndCheckFolderItemsFinished(
|
||||
@ -276,6 +320,7 @@ void SeafileService::FetchAndCheckFolderItemsFinished(
|
||||
if (!CheckReply(&reply)) {
|
||||
qLog(Warning)
|
||||
<< "Something wrong with the reply... (FetchFolderItemsToList)";
|
||||
FinishedTaskInProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -305,10 +350,14 @@ void SeafileService::FetchAndCheckFolderItemsFinished(
|
||||
}
|
||||
|
||||
tree_.CheckEntries(entries, library, path);
|
||||
|
||||
FinishedTaskInProgress();
|
||||
}
|
||||
|
||||
void SeafileService::AddRecursivelyFolderItems(const QString& library,
|
||||
const QString& path) {
|
||||
StartTaskInProgress();
|
||||
|
||||
QNetworkReply* reply = PrepareFetchFolderItems(library, path);
|
||||
NewClosure(
|
||||
reply, SIGNAL(finished()), this,
|
||||
@ -321,6 +370,7 @@ void SeafileService::AddRecursivelyFolderItemsFinished(QNetworkReply* reply,
|
||||
const QString& path) {
|
||||
if (!CheckReply(&reply)) {
|
||||
qLog(Warning) << "Something wrong with the reply... (FetchFolderItems)";
|
||||
FinishedTaskInProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -347,9 +397,8 @@ void SeafileService::AddRecursivelyFolderItemsFinished(QNetworkReply* reply,
|
||||
entry_type);
|
||||
|
||||
// If AddEntry was not successful we stop
|
||||
// It could happen when the user changes the library to update while an
|
||||
// update was in progress
|
||||
if (!tree_.AddEntry(library, path, entry)) {
|
||||
FinishedTaskInProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -359,6 +408,8 @@ void SeafileService::AddRecursivelyFolderItemsFinished(QNetworkReply* reply,
|
||||
MaybeAddFileEntry(entry.name(), library, path);
|
||||
}
|
||||
}
|
||||
|
||||
FinishedTaskInProgress();
|
||||
}
|
||||
|
||||
QNetworkReply* SeafileService::PrepareFetchContentForFile(
|
||||
@ -600,6 +651,24 @@ bool SeafileService::CheckReply(QNetworkReply** reply, int tries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void SeafileService::StartTaskInProgress() {
|
||||
indexing_task_max_++;
|
||||
task_manager_->SetTaskProgress(indexing_task_id_, indexing_task_progress_,
|
||||
indexing_task_max_);
|
||||
}
|
||||
|
||||
void SeafileService::FinishedTaskInProgress() {
|
||||
indexing_task_progress_++;
|
||||
if (indexing_task_progress_ == indexing_task_max_) {
|
||||
task_manager_->SetTaskFinished(indexing_task_id_);
|
||||
indexing_task_id_ = -1;
|
||||
UpdatingLibrariesFinishedSignal();
|
||||
} else {
|
||||
task_manager_->SetTaskProgress(indexing_task_id_, indexing_task_progress_,
|
||||
indexing_task_max_);
|
||||
}
|
||||
}
|
||||
|
||||
SeafileService::~SeafileService() {
|
||||
// Save the tree !
|
||||
QSettings s;
|
||||
|
@ -76,16 +76,17 @@ class SeafileService : public CloudFileService {
|
||||
const QString& server);
|
||||
// Get all the libraries available for the user. Will emit a signal
|
||||
void GetLibraries();
|
||||
void ChangeLibrary(const QString& new_library);
|
||||
|
||||
public slots:
|
||||
void Connect();
|
||||
void ForgetCredentials();
|
||||
void ChangeLibrary(const QString& new_library);
|
||||
|
||||
signals:
|
||||
signals:
|
||||
void Connected();
|
||||
// QMap, key : library's id, value : library's name
|
||||
void GetLibrariesFinishedSignal(QMap<QString, QString>);
|
||||
void UpdatingLibrariesFinishedSignal();
|
||||
|
||||
private slots:
|
||||
// Will emit the signal
|
||||
@ -143,10 +144,19 @@ class SeafileService : public CloudFileService {
|
||||
// argument
|
||||
bool CheckReply(QNetworkReply** reply, int tries = 1);
|
||||
|
||||
void StartTaskInProgress();
|
||||
void FinishedTaskInProgress();
|
||||
|
||||
SeafileTree tree_;
|
||||
QString access_token_;
|
||||
QString server_;
|
||||
QString library_updated_;
|
||||
|
||||
int indexing_task_id_;
|
||||
int indexing_task_max_;
|
||||
int indexing_task_progress_;
|
||||
|
||||
bool changing_libary_;
|
||||
};
|
||||
|
||||
#endif // INTERNET_SEAFILE_SEAFILESERVICE_H_
|
||||
|
@ -134,6 +134,8 @@ void SeafileSettingsPage::Login() {
|
||||
}
|
||||
|
||||
void SeafileSettingsPage::Logout() {
|
||||
// Forget the songs added
|
||||
service_->ChangeLibrary("none");
|
||||
service_->ForgetCredentials();
|
||||
|
||||
// We choose to keep the server
|
||||
|
@ -842,7 +842,7 @@ void SpotifyService::DropMimeData(const QMimeData* data,
|
||||
|
||||
QModelIndex playlist_root_index = index;
|
||||
QVariant q_playlist_type = playlist_root_index.data(InternetModel::Role_Type);
|
||||
if (!q_playlist_type.isValid()) {
|
||||
if (!q_playlist_type.isValid() || q_playlist_type.toInt() == InternetModel::Type_Track) {
|
||||
// In case song was dropped on a playlist item, not on the playlist
|
||||
// title/root element
|
||||
playlist_root_index = index.parent();
|
||||
|
@ -24,12 +24,11 @@
|
||||
#include "core/player.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/thread.h"
|
||||
#include "smartplaylists/generator.h"
|
||||
#include "smartplaylists/querygenerator.h"
|
||||
#include "smartplaylists/search.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
const char* Library::kSongsTable = "songs";
|
||||
const char* Library::kDirsTable = "directories";
|
||||
const char* Library::kSubdirsTable = "subdirectories";
|
||||
@ -67,17 +66,15 @@ Library::Library(Application* app, QObject* parent)
|
||||
Search::Sort_Random, SearchTerm::Field_Title, 50)))
|
||||
<< GeneratorPtr(new QueryGenerator(
|
||||
QT_TRANSLATE_NOOP("Library", "Ever played"),
|
||||
Search(Search::Type_And,
|
||||
Search::TermList()
|
||||
<< SearchTerm(SearchTerm::Field_PlayCount,
|
||||
SearchTerm::Op_GreaterThan, 0),
|
||||
Search(Search::Type_And, Search::TermList() << SearchTerm(
|
||||
SearchTerm::Field_PlayCount,
|
||||
SearchTerm::Op_GreaterThan, 0),
|
||||
Search::Sort_Random, SearchTerm::Field_Title)))
|
||||
<< GeneratorPtr(new QueryGenerator(
|
||||
QT_TRANSLATE_NOOP("Library", "Never played"),
|
||||
Search(Search::Type_And,
|
||||
Search::TermList()
|
||||
<< SearchTerm(SearchTerm::Field_PlayCount,
|
||||
SearchTerm::Op_Equals, 0),
|
||||
Search(Search::Type_And, Search::TermList() << SearchTerm(
|
||||
SearchTerm::Field_PlayCount,
|
||||
SearchTerm::Op_Equals, 0),
|
||||
Search::Sort_Random, SearchTerm::Field_Title)))
|
||||
<< GeneratorPtr(new QueryGenerator(
|
||||
QT_TRANSLATE_NOOP("Library", "Last played"),
|
||||
@ -110,12 +107,11 @@ Library::Library(Application* app, QObject* parent)
|
||||
<< SearchTerm(SearchTerm::Field_SkipCount,
|
||||
SearchTerm::Op_GreaterThan, 4),
|
||||
Search::Sort_FieldDesc, SearchTerm::Field_SkipCount))))
|
||||
<< (LibraryModel::GeneratorList()
|
||||
<< GeneratorPtr(new QueryGenerator(
|
||||
QT_TRANSLATE_NOOP("Library", "Dynamic random mix"),
|
||||
Search(Search::Type_All, Search::TermList(),
|
||||
Search::Sort_Random, SearchTerm::Field_Title),
|
||||
true))));
|
||||
<< (LibraryModel::GeneratorList() << GeneratorPtr(new QueryGenerator(
|
||||
QT_TRANSLATE_NOOP("Library", "Dynamic random mix"),
|
||||
Search(Search::Type_All, Search::TermList(), Search::Sort_Random,
|
||||
SearchTerm::Field_Title),
|
||||
true))));
|
||||
|
||||
// full rescan revisions
|
||||
full_rescan_revisions_[26] = tr("CUE sheet support");
|
||||
@ -131,7 +127,8 @@ Library::~Library() {
|
||||
|
||||
void Library::Init() {
|
||||
watcher_ = new LibraryWatcher;
|
||||
watcher_thread_ = new QThread(this);
|
||||
watcher_thread_ = new Thread(this);
|
||||
watcher_thread_->SetIoPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
watcher_->moveToThread(watcher_thread_);
|
||||
watcher_thread_->start(QThread::IdlePriority);
|
||||
@ -206,9 +203,7 @@ void Library::WriteAllSongsStatisticsToFiles() {
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
}
|
||||
|
||||
void Library::Stopped() {
|
||||
CurrentSongChanged(Song());
|
||||
}
|
||||
void Library::Stopped() { CurrentSongChanged(Song()); }
|
||||
|
||||
void Library::CurrentSongChanged(const Song& song) {
|
||||
TagReaderReply* reply = nullptr;
|
||||
@ -244,7 +239,7 @@ void Library::SongsStatisticsChanged(const SongList& songs) {
|
||||
}
|
||||
|
||||
SongList Library::FilterCurrentWMASong(SongList songs, Song* queued) {
|
||||
for (SongList::iterator it = songs.begin(); it != songs.end(); ) {
|
||||
for (SongList::iterator it = songs.begin(); it != songs.end();) {
|
||||
if (it->url() == current_wma_song_url_) {
|
||||
*queued = *it;
|
||||
it = songs.erase(it);
|
||||
|
@ -30,6 +30,7 @@ class LibraryBackend;
|
||||
class LibraryModel;
|
||||
class LibraryWatcher;
|
||||
class TaskManager;
|
||||
class Thread;
|
||||
|
||||
class Library : public QObject {
|
||||
Q_OBJECT
|
||||
@ -79,7 +80,7 @@ class Library : public QObject {
|
||||
LibraryModel* model_;
|
||||
|
||||
LibraryWatcher* watcher_;
|
||||
QThread* watcher_thread_;
|
||||
Thread* watcher_thread_;
|
||||
|
||||
bool save_statistics_in_files_;
|
||||
bool save_ratings_in_files_;
|
||||
|
@ -59,8 +59,6 @@ LibraryWatcher::LibraryWatcher(QObject* parent)
|
||||
rescan_paused_(false),
|
||||
total_watches_(0),
|
||||
cue_parser_(new CueParser(backend_, this)) {
|
||||
Utilities::SetThreadIOPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
rescan_timer_->setInterval(1000);
|
||||
rescan_timer_->setSingleShot(true);
|
||||
|
||||
@ -636,7 +634,6 @@ void LibraryWatcher::RescanPathsNow() {
|
||||
}
|
||||
|
||||
QString LibraryWatcher::PickBestImage(const QStringList& images) {
|
||||
|
||||
// This is used when there is more than one image in a directory.
|
||||
// Pick the biggest image that matches the most important filter
|
||||
|
||||
|
@ -175,6 +175,7 @@ void NetworkRemote::AcceptConnection() {
|
||||
qLog(Info) << "Got a connection from public ip"
|
||||
<< client_socket->peerAddress().toString();
|
||||
client_socket->close();
|
||||
client_socket->deleteLater();
|
||||
} else {
|
||||
CreateRemoteClient(client_socket);
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ RemoteClient::~RemoteClient() {
|
||||
client_->waitForDisconnected(2000);
|
||||
|
||||
song_sender_->deleteLater();
|
||||
client_->deleteLater();
|
||||
}
|
||||
|
||||
void RemoteClient::setDownloader(bool downloader) { downloader_ = downloader; }
|
||||
|
@ -1761,7 +1761,7 @@ void Playlist::ExpandDynamicPlaylist() {
|
||||
}
|
||||
|
||||
void Playlist::RemoveItemsNotInQueue() {
|
||||
if (queue_->is_empty()) {
|
||||
if (queue_->is_empty() && !current_item_index_.isValid()) {
|
||||
RemoveItemsWithoutUndo(0, items_.count());
|
||||
return;
|
||||
}
|
||||
@ -1771,7 +1771,7 @@ void Playlist::RemoveItemsNotInQueue() {
|
||||
// Find a place to start - first row that isn't in the queue
|
||||
forever {
|
||||
if (start >= rowCount()) return;
|
||||
if (!queue_->ContainsSourceRow(start)) break;
|
||||
if (!queue_->ContainsSourceRow(start) && current_row() != start) break;
|
||||
start++;
|
||||
}
|
||||
|
||||
@ -1780,7 +1780,9 @@ void Playlist::RemoveItemsNotInQueue() {
|
||||
int count = 1;
|
||||
forever {
|
||||
if (start + count >= rowCount()) break;
|
||||
if (queue_->ContainsSourceRow(start + count)) break;
|
||||
if (queue_->ContainsSourceRow(start + count) ||
|
||||
current_row() == start + count)
|
||||
break;
|
||||
count++;
|
||||
}
|
||||
|
||||
|
@ -141,6 +141,7 @@ void PlaylistContainer::SetManager(PlaylistManager* manager) {
|
||||
SLOT(SetViewModel(Playlist*)));
|
||||
connect(manager, SIGNAL(PlaylistAdded(int, QString, bool)),
|
||||
SLOT(PlaylistAdded(int, QString, bool)));
|
||||
connect(manager, SIGNAL(PlaylistManagerInitialized()), SLOT(Started()));
|
||||
connect(manager, SIGNAL(PlaylistClosed(int)), SLOT(PlaylistClosed(int)));
|
||||
connect(manager, SIGNAL(PlaylistRenamed(int, QString)),
|
||||
SLOT(PlaylistRenamed(int, QString)));
|
||||
@ -269,6 +270,10 @@ void PlaylistContainer::PlaylistAdded(int id, const QString& name,
|
||||
}
|
||||
}
|
||||
|
||||
void PlaylistContainer::Started() {
|
||||
starting_up_ = false;
|
||||
}
|
||||
|
||||
void PlaylistContainer::PlaylistClosed(int id) {
|
||||
ui_->tab_bar->RemoveTab(id);
|
||||
|
||||
|
@ -74,6 +74,8 @@ signals:
|
||||
void PlaylistClosed(int id);
|
||||
void PlaylistRenamed(int id, const QString& new_name);
|
||||
|
||||
void Started();
|
||||
|
||||
void ActivePlaying();
|
||||
void ActivePaused();
|
||||
void ActiveStopped();
|
||||
|
297
src/ripper/ripcddialog.cpp
Normal file
297
src/ripper/ripcddialog.cpp
Normal file
@ -0,0 +1,297 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ripper/ripcddialog.h"
|
||||
|
||||
#include <cdio/cdio.h>
|
||||
#include <QCheckBox>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
|
||||
#include "config.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/utilities.h"
|
||||
#include "ripper/ripper.h"
|
||||
#include "ui_ripcddialog.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
#include "transcoder/transcoderoptionsdialog.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
namespace {
|
||||
bool ComparePresetsByName(const TranscoderPreset& left,
|
||||
const TranscoderPreset& right) {
|
||||
return left.name_ < right.name_;
|
||||
}
|
||||
|
||||
const int kCheckboxColumn = 0;
|
||||
const int kTrackNumberColumn = 1;
|
||||
const int kTrackTitleColumn = 2;
|
||||
}
|
||||
|
||||
const char* RipCDDialog::kSettingsGroup = "Transcoder";
|
||||
const int RipCDDialog::kMaxDestinationItems = 10;
|
||||
|
||||
RipCDDialog::RipCDDialog(QWidget* parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui_RipCDDialog),
|
||||
ripper_(new Ripper(this)),
|
||||
working_(false) {
|
||||
// Init
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Set column widths in the QTableWidget.
|
||||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(
|
||||
kCheckboxColumn, QHeaderView::ResizeToContents);
|
||||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(
|
||||
kTrackNumberColumn, QHeaderView::ResizeToContents);
|
||||
ui_->tableWidget->horizontalHeader()->setSectionResizeMode(kTrackTitleColumn,
|
||||
QHeaderView::Stretch);
|
||||
|
||||
// Add a rip button
|
||||
rip_button_ = ui_->button_box->addButton(tr("Start ripping"),
|
||||
QDialogButtonBox::ActionRole);
|
||||
cancel_button_ = ui_->button_box->button(QDialogButtonBox::Cancel);
|
||||
close_button_ = ui_->button_box->button(QDialogButtonBox::Close);
|
||||
|
||||
// Hide elements
|
||||
cancel_button_->hide();
|
||||
ui_->progress_group->hide();
|
||||
|
||||
connect(ui_->select_all_button, SIGNAL(clicked()), SLOT(SelectAll()));
|
||||
connect(ui_->select_none_button, SIGNAL(clicked()), SLOT(SelectNone()));
|
||||
connect(ui_->invert_selection_button, SIGNAL(clicked()),
|
||||
SLOT(InvertSelection()));
|
||||
connect(rip_button_, SIGNAL(clicked()), SLOT(ClickedRipButton()));
|
||||
connect(cancel_button_, SIGNAL(clicked()), ripper_, SLOT(Cancel()));
|
||||
connect(close_button_, SIGNAL(clicked()), SLOT(hide()));
|
||||
|
||||
connect(ui_->options, SIGNAL(clicked()), SLOT(Options()));
|
||||
connect(ui_->select, SIGNAL(clicked()), SLOT(AddDestination()));
|
||||
|
||||
connect(ripper_, SIGNAL(Finished()), SLOT(Finished()));
|
||||
connect(ripper_, SIGNAL(Cancelled()), SLOT(Cancelled()));
|
||||
connect(ripper_, SIGNAL(ProgressInterval(int, int)),
|
||||
SLOT(SetupProgressBarLimits(int, int)));
|
||||
connect(ripper_, SIGNAL(Progress(int)), SLOT(UpdateProgressBar(int)));
|
||||
|
||||
setWindowTitle(tr("Rip CD"));
|
||||
AddDestinationDirectory(QDir::homePath());
|
||||
|
||||
// Get presets
|
||||
QList<TranscoderPreset> presets = Transcoder::GetAllPresets();
|
||||
qSort(presets.begin(), presets.end(), ComparePresetsByName);
|
||||
for (const TranscoderPreset& preset : presets) {
|
||||
ui_->format->addItem(
|
||||
QString("%1 (.%2)").arg(preset.name_).arg(preset.extension_),
|
||||
QVariant::fromValue(preset));
|
||||
}
|
||||
|
||||
// Load settings
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
last_add_dir_ = s.value("last_add_dir", QDir::homePath()).toString();
|
||||
|
||||
QString last_output_format = s.value("last_output_format", "ogg").toString();
|
||||
for (int i = 0; i < ui_->format->count(); ++i) {
|
||||
if (last_output_format ==
|
||||
ui_->format->itemData(i).value<TranscoderPreset>().extension_) {
|
||||
ui_->format->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RipCDDialog::~RipCDDialog() {}
|
||||
|
||||
bool RipCDDialog::CheckCDIOIsValid() { return ripper_->CheckCDIOIsValid(); }
|
||||
|
||||
void RipCDDialog::showEvent(QShowEvent* event) {
|
||||
BuildTrackListTable();
|
||||
if (!working_) {
|
||||
ui_->progress_group->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void RipCDDialog::ClickedRipButton() {
|
||||
if (ripper_->MediaChanged()) {
|
||||
QMessageBox cdio_fail(QMessageBox::Critical, tr("Error Ripping CD"),
|
||||
tr("Media has changed. Reloading"));
|
||||
cdio_fail.exec();
|
||||
if (CheckCDIOIsValid()) {
|
||||
BuildTrackListTable();
|
||||
} else {
|
||||
ui_->tableWidget->clearContents();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Add tracks and album information to the ripper.
|
||||
ripper_->ClearTracks();
|
||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
||||
.value<TranscoderPreset>();
|
||||
for (int i = 1; i <= ui_->tableWidget->rowCount(); ++i) {
|
||||
if (!checkboxes_.value(i - 1)->isChecked()) {
|
||||
continue;
|
||||
}
|
||||
QString transcoded_filename = GetOutputFileName(
|
||||
ParseFileFormatString(ui_->format_filename->text(), i));
|
||||
QString title = track_names_.value(i - 1)->text();
|
||||
ripper_->AddTrack(i, title, transcoded_filename, preset);
|
||||
}
|
||||
ripper_->SetAlbumInformation(
|
||||
ui_->albumLineEdit->text(), ui_->artistLineEdit->text(),
|
||||
ui_->genreLineEdit->text(), ui_->yearLineEdit->text().toInt(),
|
||||
ui_->discLineEdit->text().toInt(), preset.type_);
|
||||
|
||||
SetWorking(true);
|
||||
ripper_->Start();
|
||||
}
|
||||
|
||||
void RipCDDialog::Options() {
|
||||
TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex())
|
||||
.value<TranscoderPreset>();
|
||||
|
||||
TranscoderOptionsDialog dialog(preset.type_, this);
|
||||
if (dialog.is_valid()) {
|
||||
dialog.exec();
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a folder to the destination box.
|
||||
void RipCDDialog::AddDestination() {
|
||||
int index = ui_->destination->currentIndex();
|
||||
QString initial_dir = (!ui_->destination->itemData(index).isNull()
|
||||
? ui_->destination->itemData(index).toString()
|
||||
: QDir::homePath());
|
||||
QString dir =
|
||||
QFileDialog::getExistingDirectory(this, tr("Add folder"), initial_dir);
|
||||
|
||||
if (!dir.isEmpty()) {
|
||||
// Keep only a finite number of items in the box.
|
||||
while (ui_->destination->count() >= kMaxDestinationItems) {
|
||||
ui_->destination->removeItem(0); // The oldest item.
|
||||
}
|
||||
AddDestinationDirectory(dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a directory to the 'destination' combo box.
|
||||
void RipCDDialog::AddDestinationDirectory(QString dir) {
|
||||
QIcon icon = IconLoader::Load("folder");
|
||||
QVariant data = QVariant::fromValue(dir);
|
||||
// Do not insert duplicates.
|
||||
int duplicate_index = ui_->destination->findData(data);
|
||||
if (duplicate_index == -1) {
|
||||
ui_->destination->addItem(icon, dir, data);
|
||||
ui_->destination->setCurrentIndex(ui_->destination->count() - 1);
|
||||
} else {
|
||||
ui_->destination->setCurrentIndex(duplicate_index);
|
||||
}
|
||||
}
|
||||
|
||||
void RipCDDialog::SelectAll() {
|
||||
for (QCheckBox* checkbox : checkboxes_) {
|
||||
checkbox->setCheckState(Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
void RipCDDialog::SelectNone() {
|
||||
for (QCheckBox* checkbox : checkboxes_) {
|
||||
checkbox->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
}
|
||||
|
||||
void RipCDDialog::InvertSelection() {
|
||||
for (QCheckBox* checkbox : checkboxes_) {
|
||||
checkbox->setCheckState(checkbox->isChecked() ? Qt::Unchecked
|
||||
: Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
void RipCDDialog::Finished() { SetWorking(false); }
|
||||
|
||||
void RipCDDialog::Cancelled() {
|
||||
ui_->progress_bar->setValue(0);
|
||||
SetWorking(false);
|
||||
}
|
||||
|
||||
void RipCDDialog::SetupProgressBarLimits(int min, int max) {
|
||||
ui_->progress_bar->setRange(min, max);
|
||||
}
|
||||
|
||||
void RipCDDialog::UpdateProgressBar(int progress) {
|
||||
ui_->progress_bar->setValue(progress);
|
||||
}
|
||||
|
||||
void RipCDDialog::SetWorking(bool working) {
|
||||
working_ = working;
|
||||
rip_button_->setVisible(!working);
|
||||
cancel_button_->setVisible(working);
|
||||
close_button_->setVisible(!working);
|
||||
ui_->input_group->setEnabled(!working);
|
||||
ui_->output_group->setEnabled(!working);
|
||||
ui_->progress_group->setVisible(true);
|
||||
}
|
||||
|
||||
void RipCDDialog::BuildTrackListTable() {
|
||||
checkboxes_.clear();
|
||||
track_names_.clear();
|
||||
|
||||
int tracks = ripper_->TracksOnDisc();
|
||||
|
||||
ui_->tableWidget->setRowCount(tracks);
|
||||
for (int i = 1; i <= tracks; i++) {
|
||||
QCheckBox* checkbox_i = new QCheckBox(ui_->tableWidget);
|
||||
checkbox_i->setCheckState(Qt::Checked);
|
||||
checkboxes_.append(checkbox_i);
|
||||
ui_->tableWidget->setCellWidget(i - 1, kCheckboxColumn, checkbox_i);
|
||||
ui_->tableWidget->setCellWidget(i - 1, kTrackNumberColumn,
|
||||
new QLabel(QString::number(i)));
|
||||
QString track_title = QString("Track %1").arg(i);
|
||||
QLineEdit* line_edit_track_title_i =
|
||||
new QLineEdit(track_title, ui_->tableWidget);
|
||||
track_names_.append(line_edit_track_title_i);
|
||||
ui_->tableWidget->setCellWidget(i - 1, kTrackTitleColumn,
|
||||
line_edit_track_title_i);
|
||||
}
|
||||
}
|
||||
|
||||
QString RipCDDialog::GetOutputFileName(const QString& basename) const {
|
||||
QFileInfo path(
|
||||
ui_->destination->itemData(ui_->destination->currentIndex()).toString());
|
||||
QString extension = ui_->format->itemData(ui_->format->currentIndex())
|
||||
.value<TranscoderPreset>()
|
||||
.extension_;
|
||||
return path.filePath() + '/' + basename + '.' + extension;
|
||||
}
|
||||
|
||||
QString RipCDDialog::ParseFileFormatString(const QString& file_format,
|
||||
int track_no) const {
|
||||
QString to_return = file_format;
|
||||
to_return.replace(QString("%artist%"), ui_->artistLineEdit->text());
|
||||
to_return.replace(QString("%album%"), ui_->albumLineEdit->text());
|
||||
to_return.replace(QString("%genre%"), ui_->genreLineEdit->text());
|
||||
to_return.replace(QString("%year%"), ui_->yearLineEdit->text());
|
||||
to_return.replace(QString("%tracknum%"), QString::number(track_no));
|
||||
to_return.replace(QString("%track%"),
|
||||
track_names_.value(track_no - 1)->text());
|
||||
return to_return;
|
||||
}
|
80
src/ripper/ripcddialog.h
Normal file
80
src/ripper/ripcddialog.h
Normal file
@ -0,0 +1,80 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SRC_RIPPER_RIPCDDIALOG_H_
|
||||
#define SRC_RIPPER_RIPCDDIALOG_H_
|
||||
|
||||
#include <memory>
|
||||
#include <QDialog>
|
||||
#include <QFile>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QLineEdit;
|
||||
|
||||
class Ripper;
|
||||
class Ui_RipCDDialog;
|
||||
|
||||
class RipCDDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RipCDDialog(QWidget* parent = nullptr);
|
||||
~RipCDDialog();
|
||||
bool CheckCDIOIsValid();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event);
|
||||
|
||||
private slots:
|
||||
void ClickedRipButton();
|
||||
void Options();
|
||||
void AddDestination();
|
||||
void SelectAll();
|
||||
void SelectNone();
|
||||
void InvertSelection();
|
||||
void Finished();
|
||||
void Cancelled();
|
||||
void SetupProgressBarLimits(int min, int max);
|
||||
void UpdateProgressBar(int progress);
|
||||
|
||||
private:
|
||||
static const char* kSettingsGroup;
|
||||
static const int kMaxDestinationItems;
|
||||
|
||||
// Constructs a filename from the given base name with a path taken
|
||||
// from the ui dialog and an extension that corresponds to the audio
|
||||
// format chosen in the ui.
|
||||
void AddDestinationDirectory(QString dir);
|
||||
void BuildTrackListTable();
|
||||
QString GetOutputFileName(const QString& basename) const;
|
||||
QString ParseFileFormatString(const QString& file_format, int track_no) const;
|
||||
void SetWorking(bool working);
|
||||
|
||||
QList<QCheckBox*> checkboxes_;
|
||||
QList<QLineEdit*> track_names_;
|
||||
QString last_add_dir_;
|
||||
QPushButton* cancel_button_;
|
||||
QPushButton* close_button_;
|
||||
QPushButton* rip_button_;
|
||||
std::unique_ptr<Ui_RipCDDialog> ui_;
|
||||
Ripper* ripper_;
|
||||
bool working_;
|
||||
};
|
||||
#endif // SRC_RIPPER_RIPCDDIALOG_H_
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>RipCD</class>
|
||||
<widget class="QDialog" name="RipCD">
|
||||
<class>RipCDDialog</class>
|
||||
<widget class="QDialog" name="RipCDDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
311
src/ripper/ripper.cpp
Normal file
311
src/ripper/ripper.cpp
Normal file
@ -0,0 +1,311 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ripper.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QMutexLocker>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
// winspool.h defines this :(
|
||||
#ifdef AddJob
|
||||
#undef AddJob
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
const char kWavHeaderRiffMarker[] = "RIFF";
|
||||
const char kWavFileTypeFormatChunk[] = "WAVEfmt ";
|
||||
const char kWavDataString[] = "data";
|
||||
} // namespace
|
||||
|
||||
Ripper::Ripper(QObject* parent)
|
||||
: QObject(parent),
|
||||
transcoder_(new Transcoder(this)),
|
||||
cancel_requested_(false),
|
||||
finished_success_(0),
|
||||
finished_failed_(0),
|
||||
files_tagged_(0) {
|
||||
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
||||
|
||||
connect(this, SIGNAL(RippingComplete()), transcoder_, SLOT(Start()));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)),
|
||||
SLOT(TranscodingJobComplete(QString, QString, bool)));
|
||||
connect(transcoder_, SIGNAL(AllJobsComplete()),
|
||||
SLOT(AllTranscodingJobsComplete()));
|
||||
connect(transcoder_, SIGNAL(LogLine(QString)), SLOT(LogLine(QString)));
|
||||
}
|
||||
|
||||
Ripper::~Ripper() { cdio_destroy(cdio_); }
|
||||
|
||||
void Ripper::AddTrack(int track_number, const QString& title,
|
||||
const QString& transcoded_filename,
|
||||
const TranscoderPreset& preset) {
|
||||
if (track_number < 1 || track_number > TracksOnDisc()) {
|
||||
qLog(Warning) << "Invalid track number:" << track_number << "Ignoring";
|
||||
return;
|
||||
}
|
||||
TrackInformation track(track_number, title, transcoded_filename, preset);
|
||||
tracks_.append(track);
|
||||
}
|
||||
|
||||
void Ripper::SetAlbumInformation(const QString& album, const QString& artist,
|
||||
const QString& genre, int year, int disc,
|
||||
Song::FileType type) {
|
||||
album_.album = album;
|
||||
album_.artist = artist;
|
||||
album_.genre = genre;
|
||||
album_.year = year;
|
||||
album_.disc = disc;
|
||||
album_.type = type;
|
||||
}
|
||||
|
||||
int Ripper::TracksOnDisc() const {
|
||||
int number_of_tracks = cdio_get_num_tracks(cdio_);
|
||||
// Return zero tracks if there is an error, e.g. no medium found.
|
||||
if (number_of_tracks == CDIO_INVALID_TRACK) number_of_tracks = 0;
|
||||
return number_of_tracks;
|
||||
}
|
||||
|
||||
int Ripper::AddedTracks() const { return tracks_.length(); }
|
||||
|
||||
void Ripper::ClearTracks() { tracks_.clear(); }
|
||||
|
||||
bool Ripper::CheckCDIOIsValid() {
|
||||
if (cdio_) {
|
||||
cdio_destroy(cdio_);
|
||||
}
|
||||
cdio_ = cdio_open(NULL, DRIVER_UNKNOWN);
|
||||
// Refresh the status of the cd media. This will prevent unnecessary
|
||||
// rebuilds of the track list table.
|
||||
if (cdio_) {
|
||||
cdio_get_media_changed(cdio_);
|
||||
}
|
||||
return cdio_;
|
||||
}
|
||||
|
||||
bool Ripper::MediaChanged() const {
|
||||
if (cdio_ && cdio_get_media_changed(cdio_))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
void Ripper::Start() {
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
cancel_requested_ = false;
|
||||
}
|
||||
SetupProgressInterval();
|
||||
|
||||
qLog(Debug) << "Ripping" << AddedTracks() << "tracks.";
|
||||
QtConcurrent::run(this, &Ripper::Rip);
|
||||
}
|
||||
|
||||
void Ripper::Cancel() {
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
cancel_requested_ = true;
|
||||
}
|
||||
transcoder_->Cancel();
|
||||
RemoveTemporaryDirectory();
|
||||
emit(Cancelled());
|
||||
}
|
||||
|
||||
void Ripper::TranscodingJobComplete(const QString& input, const QString& output,
|
||||
bool success) {
|
||||
if (success)
|
||||
finished_success_++;
|
||||
else
|
||||
finished_failed_++;
|
||||
UpdateProgress();
|
||||
|
||||
// The the transcoder does not overwrite files. Instead, it changes
|
||||
// the name of the output file. We need to update the transcoded
|
||||
// filename for the corresponding track so that we tag the correct
|
||||
// file later on.
|
||||
for (QList<TrackInformation>::iterator it = tracks_.begin();
|
||||
it != tracks_.end(); ++it) {
|
||||
if (it->temporary_filename == input) {
|
||||
it->transcoded_filename = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Ripper::AllTranscodingJobsComplete() {
|
||||
RemoveTemporaryDirectory();
|
||||
TagFiles();
|
||||
}
|
||||
|
||||
void Ripper::LogLine(const QString& message) { qLog(Debug) << message; }
|
||||
|
||||
/*
|
||||
* WAV Header documentation
|
||||
* as taken from:
|
||||
* http://www.topherlee.com/software/pcm-tut-wavformat.html
|
||||
* Pos Value Description
|
||||
* 0-3 | "RIFF" | Marks the file as a riff file.
|
||||
* | Characters are each 1 byte long.
|
||||
* 4-7 | File size (integer) | Size of the overall file - 8 bytes,
|
||||
* | in bytes (32-bit integer).
|
||||
* 8-11 | "WAVE" | File Type Header. For our purposes,
|
||||
* | it always equals "WAVE".
|
||||
* 13-16 | "fmt " | Format chunk marker. Includes trailing null.
|
||||
* 17-20 | 16 | Length of format data as listed above
|
||||
* 21-22 | 1 | Type of format (1 is PCM) - 2 byte integer
|
||||
* 23-24 | 2 | Number of Channels - 2 byte integer
|
||||
* 25-28 | 44100 | Sample Rate - 32 byte integer. Common values
|
||||
* | are 44100 (CD), 48000 (DAT).
|
||||
* | Sample Rate = Number of Samples per second, or Hertz.
|
||||
* 29-32 | 176400 | (Sample Rate * BitsPerSample * Channels) / 8.
|
||||
* 33-34 | 4 | (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16
|
||||
* bit mono4 - 16 bit stereo
|
||||
* 35-36 | 16 | Bits per sample
|
||||
* 37-40 | "data" | "data" chunk header.
|
||||
* | Marks the beginning of the data section.
|
||||
* 41-44 | File size (data) | Size of the data section.
|
||||
*/
|
||||
void Ripper::WriteWAVHeader(QFile* stream, int32_t i_bytecount) {
|
||||
QDataStream data_stream(stream);
|
||||
data_stream.setByteOrder(QDataStream::LittleEndian);
|
||||
// sizeof() - 1 to avoid including "\0" in the file too
|
||||
data_stream.writeRawData(kWavHeaderRiffMarker,
|
||||
sizeof(kWavHeaderRiffMarker) - 1); /* 0-3 */
|
||||
data_stream << qint32(i_bytecount + 44 - 8); /* 4-7 */
|
||||
data_stream.writeRawData(kWavFileTypeFormatChunk,
|
||||
sizeof(kWavFileTypeFormatChunk) - 1); /* 8-15 */
|
||||
data_stream << (qint32)16; /* 16-19 */
|
||||
data_stream << (qint16)1; /* 20-21 */
|
||||
data_stream << (qint16)2; /* 22-23 */
|
||||
data_stream << (qint32)44100; /* 24-27 */
|
||||
data_stream << (qint32)(44100 * 2 * 2); /* 28-31 */
|
||||
data_stream << (qint16)4; /* 32-33 */
|
||||
data_stream << (qint16)16; /* 34-35 */
|
||||
data_stream.writeRawData(kWavDataString,
|
||||
sizeof(kWavDataString) - 1); /* 36-39 */
|
||||
data_stream << (qint32)i_bytecount; /* 40-43 */
|
||||
}
|
||||
|
||||
void Ripper::Rip() {
|
||||
temporary_directory_ = Utilities::MakeTempDir() + "/";
|
||||
finished_success_ = 0;
|
||||
finished_failed_ = 0;
|
||||
|
||||
// Set up progress bar
|
||||
UpdateProgress();
|
||||
|
||||
for (QList<TrackInformation>::iterator it = tracks_.begin();
|
||||
it != tracks_.end(); ++it) {
|
||||
QString filename =
|
||||
QString("%1%2.wav").arg(temporary_directory_).arg(it->track_number);
|
||||
QFile destination_file(filename);
|
||||
destination_file.open(QIODevice::WriteOnly);
|
||||
|
||||
lsn_t i_first_lsn = cdio_get_track_lsn(cdio_, it->track_number);
|
||||
lsn_t i_last_lsn = cdio_get_track_last_lsn(cdio_, it->track_number);
|
||||
WriteWAVHeader(&destination_file,
|
||||
(i_last_lsn - i_first_lsn + 1) * CDIO_CD_FRAMESIZE_RAW);
|
||||
|
||||
QByteArray buffered_input_bytes(CDIO_CD_FRAMESIZE_RAW, '\0');
|
||||
for (lsn_t i_cursor = i_first_lsn; i_cursor <= i_last_lsn; i_cursor++) {
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
if (cancel_requested_) {
|
||||
qLog(Debug) << "CD ripping canceled.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cdio_read_audio_sector(cdio_, buffered_input_bytes.data(),
|
||||
i_cursor) == DRIVER_OP_SUCCESS) {
|
||||
destination_file.write(buffered_input_bytes.data(),
|
||||
buffered_input_bytes.size());
|
||||
} else {
|
||||
qLog(Error) << "CD read error";
|
||||
break;
|
||||
}
|
||||
}
|
||||
finished_success_++;
|
||||
UpdateProgress();
|
||||
|
||||
it->temporary_filename = filename;
|
||||
transcoder_->AddJob(it->temporary_filename, it->preset,
|
||||
it->transcoded_filename);
|
||||
}
|
||||
emit(RippingComplete());
|
||||
}
|
||||
|
||||
// The progress interval is [0, 200*AddedTracks()], where the first
|
||||
// half corresponds to the CD ripping and the second half corresponds
|
||||
// to the transcoding.
|
||||
void Ripper::SetupProgressInterval() {
|
||||
int max = AddedTracks() * 2 * 100;
|
||||
emit ProgressInterval(0, max);
|
||||
}
|
||||
|
||||
void Ripper::UpdateProgress() {
|
||||
int progress = (finished_success_ + finished_failed_) * 100;
|
||||
QMap<QString, float> current_jobs = transcoder_->GetProgress();
|
||||
for (float value : current_jobs.values()) {
|
||||
progress += qBound(0, static_cast<int>(value * 100), 99);
|
||||
}
|
||||
emit Progress(progress);
|
||||
qLog(Debug) << "Progress:" << progress;
|
||||
}
|
||||
|
||||
void Ripper::RemoveTemporaryDirectory() {
|
||||
if (!temporary_directory_.isEmpty())
|
||||
Utilities::RemoveRecursive(temporary_directory_);
|
||||
temporary_directory_.clear();
|
||||
}
|
||||
|
||||
void Ripper::TagFiles() {
|
||||
files_tagged_ = 0;
|
||||
for (const TrackInformation& track : tracks_) {
|
||||
Song song;
|
||||
song.InitFromFilePartial(track.transcoded_filename);
|
||||
song.set_track(track.track_number);
|
||||
song.set_title(track.title);
|
||||
song.set_album(album_.album);
|
||||
song.set_artist(album_.artist);
|
||||
song.set_genre(album_.genre);
|
||||
song.set_year(album_.year);
|
||||
song.set_disc(album_.disc);
|
||||
song.set_filetype(album_.type);
|
||||
|
||||
TagReaderReply* reply =
|
||||
TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
|
||||
NewClosure(reply, SIGNAL(Finished(bool)), this,
|
||||
SLOT(FileTagged(TagReaderReply*)), reply);
|
||||
}
|
||||
}
|
||||
|
||||
void Ripper::FileTagged(TagReaderReply* reply) {
|
||||
files_tagged_++;
|
||||
qLog(Debug) << "Tagged" << files_tagged_ << "of" << tracks_.length()
|
||||
<< "files";
|
||||
if (files_tagged_ == tracks_.length()) {
|
||||
qLog(Debug) << "CD ripper finished.";
|
||||
emit(Finished());
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
132
src/ripper/ripper.h
Normal file
132
src/ripper/ripper.h
Normal file
@ -0,0 +1,132 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2014, Andre Siviero <altsiviero@gmail.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SRC_RIPPER_RIPPER_H_
|
||||
#define SRC_RIPPER_RIPPER_H_
|
||||
|
||||
#include <cdio/cdio.h>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
|
||||
class QFile;
|
||||
|
||||
// Rips selected tracks from an audio CD, transcodes them to a chosen
|
||||
// format, and finally tags the files with the supplied metadata.
|
||||
//
|
||||
// Usage: Add tracks with AddTrack() and album metadata with
|
||||
// SetAlbumInformation(). Then start the ripper with Start(). The ripper
|
||||
// emits the Finished() signal when it's done or the Cancelled()
|
||||
// signal if the ripping has been cancelled.
|
||||
class Ripper : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Ripper(QObject* parent = nullptr);
|
||||
~Ripper();
|
||||
|
||||
// Adds a track to the rip list if the track number corresponds to a
|
||||
// track on the audio cd. The track will transcoded according to the
|
||||
// chosen TranscoderPreset.
|
||||
void AddTrack(int track_number, const QString& title,
|
||||
const QString& transcoded_filename,
|
||||
const TranscoderPreset& preset);
|
||||
// Sets album metadata. This information is used when tagging the
|
||||
// final files.
|
||||
void SetAlbumInformation(const QString& album, const QString& artist,
|
||||
const QString& genre, int year, int disc,
|
||||
Song::FileType type);
|
||||
// Returns the number of audio tracks on the disc.
|
||||
int TracksOnDisc() const;
|
||||
// Returns the number of tracks added to the rip list.
|
||||
int AddedTracks() const;
|
||||
// Clears the rip list.
|
||||
void ClearTracks();
|
||||
// Returns true if a cd device was successfully opened.
|
||||
bool CheckCDIOIsValid();
|
||||
// Returns true if the cd media has changed.
|
||||
bool MediaChanged() const;
|
||||
|
||||
signals:
|
||||
void Finished();
|
||||
void Cancelled();
|
||||
void ProgressInterval(int min, int max);
|
||||
void Progress(int progress);
|
||||
void RippingComplete();
|
||||
|
||||
public slots:
|
||||
void Start();
|
||||
void Cancel();
|
||||
|
||||
private slots:
|
||||
void TranscodingJobComplete(const QString& input, const QString& output,
|
||||
bool success);
|
||||
void AllTranscodingJobsComplete();
|
||||
void LogLine(const QString& message);
|
||||
void FileTagged(TagReaderReply* reply);
|
||||
|
||||
private:
|
||||
struct TrackInformation {
|
||||
TrackInformation(int track_number, const QString& title,
|
||||
const QString& transcoded_filename,
|
||||
const TranscoderPreset& preset)
|
||||
: track_number(track_number),
|
||||
title(title),
|
||||
transcoded_filename(transcoded_filename),
|
||||
preset(preset) {}
|
||||
|
||||
int track_number;
|
||||
QString title;
|
||||
QString transcoded_filename;
|
||||
TranscoderPreset preset;
|
||||
QString temporary_filename;
|
||||
};
|
||||
|
||||
struct AlbumInformation {
|
||||
AlbumInformation() : year(0), disc(0), type(Song::Type_Unknown) {}
|
||||
|
||||
QString album;
|
||||
QString artist;
|
||||
QString genre;
|
||||
int year;
|
||||
int disc;
|
||||
Song::FileType type;
|
||||
};
|
||||
|
||||
void WriteWAVHeader(QFile* stream, int32_t i_bytecount);
|
||||
void Rip();
|
||||
void SetupProgressInterval();
|
||||
void UpdateProgress();
|
||||
void RemoveTemporaryDirectory();
|
||||
void TagFiles();
|
||||
|
||||
CdIo_t* cdio_;
|
||||
Transcoder* transcoder_;
|
||||
QString temporary_directory_;
|
||||
bool cancel_requested_;
|
||||
QMutex mutex_;
|
||||
int finished_success_;
|
||||
int finished_failed_;
|
||||
int files_tagged_;
|
||||
QList<TrackInformation> tracks_;
|
||||
AlbumInformation album_;
|
||||
};
|
||||
|
||||
#endif // SRC_RIPPER_RIPPER_H_
|
@ -32,10 +32,13 @@ SongInfoFetcher::SongInfoFetcher(QObject* parent)
|
||||
|
||||
void SongInfoFetcher::AddProvider(SongInfoProvider* provider) {
|
||||
providers_ << provider;
|
||||
connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)));
|
||||
connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)),
|
||||
Qt::QueuedConnection);
|
||||
connect(provider, SIGNAL(InfoReady(int, CollapsibleInfoPane::Data)),
|
||||
SLOT(InfoReady(int, CollapsibleInfoPane::Data)));
|
||||
connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)));
|
||||
SLOT(InfoReady(int, CollapsibleInfoPane::Data)),
|
||||
Qt::QueuedConnection);
|
||||
connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)),
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
int SongInfoFetcher::FetchInfo(const Song& metadata) {
|
||||
|
@ -18,6 +18,7 @@
|
||||
#include "config.h"
|
||||
#include "songinfoprovider.h"
|
||||
#include "songinfoview.h"
|
||||
#include "taglyricsinfoprovider.h"
|
||||
#include "ultimatelyricsprovider.h"
|
||||
#include "ultimatelyricsreader.h"
|
||||
|
||||
@ -48,6 +49,7 @@ SongInfoView::SongInfoView(QWidget* parent)
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
fetcher_->AddProvider(new LastfmTrackInfoProvider);
|
||||
#endif
|
||||
fetcher_->AddProvider(new TagLyricsInfoProvider);
|
||||
}
|
||||
|
||||
SongInfoView::~SongInfoView() {}
|
||||
|
39
src/songinfo/taglyricsinfoprovider.cpp
Normal file
39
src/songinfo/taglyricsinfoprovider.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "songinfotextview.h"
|
||||
#include "taglyricsinfoprovider.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
void TagLyricsInfoProvider::FetchInfo(int id, const Song& metadata) {
|
||||
QString lyrics;
|
||||
lyrics = metadata.lyrics();
|
||||
|
||||
if (!lyrics.isEmpty()) {
|
||||
CollapsibleInfoPane::Data data;
|
||||
data.id_ = "tag/lyrics";
|
||||
data.title_ = tr("Lyrics from the ID3v2 tag");
|
||||
data.type_ = CollapsibleInfoPane::Data::Type_Lyrics;
|
||||
|
||||
SongInfoTextView* editor = new SongInfoTextView;
|
||||
editor->setPlainText(lyrics);
|
||||
data.contents_ = editor;
|
||||
|
||||
emit InfoReady(id, data);
|
||||
}
|
||||
emit Finished(id);
|
||||
}
|
30
src/songinfo/taglyricsinfoprovider.h
Normal file
30
src/songinfo/taglyricsinfoprovider.h
Normal file
@ -0,0 +1,30 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGLYRICSINFOPROVIDER_H
|
||||
#define TAGLYRICSINFOPROVIDER_H
|
||||
|
||||
#include "songinfoprovider.h"
|
||||
|
||||
class TagLyricsInfoProvider : public SongInfoProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
void FetchInfo(int id, const Song& metadata);
|
||||
};
|
||||
|
||||
#endif // TAGLYRICSINFOPROVIDER_H
|
@ -76,7 +76,8 @@ TranscodeDialog::TranscodeDialog(QWidget* parent)
|
||||
last_add_dir_ = s.value("last_add_dir", QDir::homePath()).toString();
|
||||
last_import_dir_ = s.value("last_import_dir", QDir::homePath()).toString();
|
||||
|
||||
QString last_output_format = s.value("last_output_format", "audio/x-vorbis").toString();
|
||||
QString last_output_format =
|
||||
s.value("last_output_format", "audio/x-vorbis").toString();
|
||||
for (int i = 0; i < ui_->format->count(); ++i) {
|
||||
if (last_output_format ==
|
||||
ui_->format->itemData(i).value<TranscoderPreset>().codec_mimetype_) {
|
||||
@ -142,9 +143,10 @@ void TranscodeDialog::Start() {
|
||||
|
||||
// Add jobs to the transcoder
|
||||
for (int i = 0; i < file_model->rowCount(); ++i) {
|
||||
QString filename = file_model->index(i, 0).data(Qt::UserRole).toString();
|
||||
QString outfilename = GetOutputFileName(filename, preset);
|
||||
transcoder_->AddJob(filename, preset, outfilename);
|
||||
QFileInfo input_fileinfo(
|
||||
file_model->index(i, 0).data(Qt::UserRole).toString());
|
||||
QString output_filename = GetOutputFileName(input_fileinfo, preset);
|
||||
transcoder_->AddJob(input_fileinfo.filePath(), preset, output_filename);
|
||||
}
|
||||
|
||||
// Set up the progressbar
|
||||
@ -171,8 +173,12 @@ void TranscodeDialog::Cancel() {
|
||||
SetWorking(false);
|
||||
}
|
||||
|
||||
void TranscodeDialog::JobComplete(const QString& input, const QString& output, bool success) {
|
||||
(*(success ? &finished_success_ : &finished_failed_))++;
|
||||
void TranscodeDialog::JobComplete(const QString& input, const QString& output,
|
||||
bool success) {
|
||||
if (success)
|
||||
finished_success_++;
|
||||
else
|
||||
finished_failed_++;
|
||||
queued_--;
|
||||
|
||||
UpdateStatusText();
|
||||
@ -302,7 +308,7 @@ void TranscodeDialog::AddDestination() {
|
||||
if (!dir.isEmpty()) {
|
||||
// Keep only a finite number of items in the box.
|
||||
while (ui_->destination->count() >= kMaxDestinationItems) {
|
||||
ui_->destination->removeItem(1); // The oldest folder item.
|
||||
ui_->destination->removeItem(1); // Remove the oldest folder item.
|
||||
}
|
||||
|
||||
QIcon icon = IconLoader::Load("folder");
|
||||
@ -318,21 +324,16 @@ void TranscodeDialog::AddDestination() {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the rightmost non-empty part of 'path'.
|
||||
QString TranscodeDialog::TrimPath(const QString& path) const {
|
||||
return path.section('/', -1, -1, QString::SectionSkipEmpty);
|
||||
}
|
||||
|
||||
QString TranscodeDialog::GetOutputFileName(
|
||||
const QString& input, const TranscoderPreset& preset) const {
|
||||
QString path =
|
||||
ui_->destination->itemData(ui_->destination->currentIndex()).toString();
|
||||
if (path.isEmpty()) {
|
||||
// Keep the original path.
|
||||
return input.section('.', 0, -2) + '.' + preset.extension_;
|
||||
const QFileInfo& input, const TranscoderPreset& preset) const {
|
||||
QFileInfo path(
|
||||
ui_->destination->itemData(ui_->destination->currentIndex()).toString());
|
||||
QString output_path;
|
||||
if (path.isDir()) {
|
||||
output_path = path.filePath();
|
||||
} else {
|
||||
QString file_name = TrimPath(input);
|
||||
file_name = file_name.section('.', 0, -2);
|
||||
return path + '/' + file_name + '.' + preset.extension_;
|
||||
// Keep the original path.
|
||||
output_path = input.path();
|
||||
}
|
||||
return output_path + '/' + input.completeBaseName() + '.' + preset.extension_;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
#include <QBasicTimer>
|
||||
#include <QDialog>
|
||||
#include <QFileInfo>
|
||||
|
||||
class Transcoder;
|
||||
class Ui_TranscodeDialog;
|
||||
@ -59,8 +60,7 @@ class TranscodeDialog : public QDialog {
|
||||
void SetWorking(bool working);
|
||||
void UpdateStatusText();
|
||||
void UpdateProgress();
|
||||
QString TrimPath(const QString& path) const;
|
||||
QString GetOutputFileName(const QString& input,
|
||||
QString GetOutputFileName(const QFileInfo& input,
|
||||
const TranscoderPreset& preset) const;
|
||||
|
||||
private:
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user