Merge master branch and adapt it to qt5

This commit is contained in:
Chocobozzz 2015-04-16 17:16:34 +02:00
commit e986ab5a4b
153 changed files with 19399 additions and 16145 deletions

View File

@ -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;
}
}
}

View File

@ -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
)

View File

@ -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

View File

@ -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
---------------------

View File

@ -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>

View 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=" ._@,;&amp;\/'&quot;-" with=""/>
<urlFormat replace=" ._@,;&amp;\/()'&quot;-" with=""/>
<extract>
<item begin="&lt;!-- END OF RINGTONE 1 --&gt;" end="&lt;!-- RINGTONE 2 --&gt;"/>
<item begin="&lt;!-- start of lyrics --&gt;" end="&lt;!-- end of lyrics --&gt;"/>
</extract>
<exclude>
<item tag="&lt;B&gt;"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

50
data/schema/schema-48.sql Normal file
View 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;

View File

@ -0,0 +1,3 @@
ALTER TABLE %allsongstables ADD COLUMN lyrics TEXT;
UPDATE schema_version SET version=49;

2
debian/copyright vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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);

View File

@ -51,6 +51,7 @@ message SongMetadata {
optional string etag = 30;
optional string performer = 31;
optional string grouping = 32;
optional string lyrics = 33;
}
message ReadFileRequest {

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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_;

View File

@ -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

View File

@ -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];
}
}
}

View File

@ -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>();

View File

@ -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);
}

View File

@ -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")

View File

@ -92,7 +92,7 @@ class PlayerInterface : public QObject {
virtual void Play() = 0;
virtual void ShowOSD() = 0;
signals:
signals:
void Playing();
void Paused();
void Stopped();

View File

@ -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 {

View File

@ -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
View 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
View 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_

View File

@ -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) {

View File

@ -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);

View File

@ -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();

View File

@ -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 =

View 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());
}

View 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_

View 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);
}

View 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_

View 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>

View 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));
}

View 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_

View File

@ -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();
}

View File

@ -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_;

View File

@ -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();

View File

@ -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);

View File

@ -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 {

View File

@ -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_; }

View File

@ -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;

View File

@ -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_

View File

@ -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

View File

@ -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();

View File

@ -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);

View File

@ -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_;

View File

@ -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

View File

@ -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);
}

View File

@ -56,6 +56,7 @@ RemoteClient::~RemoteClient() {
client_->waitForDisconnected(2000);
song_sender_->deleteLater();
client_->deleteLater();
}
void RemoteClient::setDownloader(bool downloader) { downloader_ = downloader; }

View File

@ -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++;
}

View File

@ -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);

View File

@ -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
View 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
View 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_

View File

@ -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
View 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
View 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_

View File

@ -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) {

View File

@ -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() {}

View 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);
}

View 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

View File

@ -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_;
}

View File

@ -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