|
|
|
/***************************************************************************
|
|
|
|
* copyright : (C) 2005 Seb Ruiz <me@sebruiz.net> *
|
|
|
|
* copyright : (C) 2006 Gábor Lehel <illissius@gmail.com> *
|
|
|
|
* copyright : (C) 2006 Bonne Eggleston <b.eggleston@gmail.com *
|
|
|
|
**************************************************************************/
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
|
|
* *
|
|
|
|
* This program 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 2 of the License, or *
|
|
|
|
* (at your option) any later version. *
|
|
|
|
* *
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
#define DEBUG_PREFIX "DynamicMode"
|
|
|
|
|
|
|
|
#include "collectiondb.h" // querybuilder, similar artists
|
|
|
|
#include "debug.h"
|
|
|
|
#include "enginecontroller.h" // HACK to get current artist for suggestion retrieval
|
|
|
|
#include "mountpointmanager.h" // device ids
|
|
|
|
#include "playlist.h"
|
|
|
|
#include "playlistbrowser.h"
|
|
|
|
#include "playlistbrowseritem.h"
|
|
|
|
#include "playlistselection.h"
|
|
|
|
#include "playlistwindow.h"
|
|
|
|
#include "statusbar.h"
|
|
|
|
|
|
|
|
#include "dynamicmode.h"
|
|
|
|
|
|
|
|
#include <tdeapplication.h> // random func
|
|
|
|
|
|
|
|
#include <tqregexp.h>
|
|
|
|
#include <tqvaluevector.h>
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS DynamicMode
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
DynamicMode::DynamicMode( const TQString &name )
|
|
|
|
: m_title( name )
|
|
|
|
, m_cycle( true )
|
|
|
|
, m_upcoming( 20 )
|
|
|
|
, m_previous( 5 )
|
|
|
|
, m_appendType( RANDOM )
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
DynamicMode::~DynamicMode()
|
|
|
|
{}
|
|
|
|
|
|
|
|
void
|
|
|
|
DynamicMode::deleting()
|
|
|
|
{
|
|
|
|
if( this == Playlist::instance()->dynamicMode() )
|
|
|
|
Playlist::instance()->disableDynamicMode();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
DynamicMode::edit()
|
|
|
|
{
|
|
|
|
if( this == Playlist::instance()->dynamicMode() )
|
|
|
|
Playlist::instance()->editActiveDynamicMode(); //so the changes get noticed
|
|
|
|
else
|
|
|
|
ConfigDynamic::editDynamicPlaylist( PlaylistWindow::self(), this );
|
|
|
|
}
|
|
|
|
|
|
|
|
TQStringList DynamicMode::items() const { return m_items; }
|
|
|
|
|
|
|
|
TQString DynamicMode::title() const { return m_title; }
|
|
|
|
bool DynamicMode::cycleTracks() const { return m_cycle; }
|
|
|
|
int DynamicMode::upcomingCount() const { return m_upcoming; }
|
|
|
|
int DynamicMode::previousCount() const { return m_previous; }
|
|
|
|
int DynamicMode::appendType() const { return m_appendType; }
|
|
|
|
|
|
|
|
void DynamicMode::setItems( const TQStringList &list ) { m_items = list; }
|
|
|
|
void DynamicMode::setCycleTracks( bool e ) { m_cycle = e; }
|
|
|
|
void DynamicMode::setUpcomingCount( int c ) { m_upcoming = c; }
|
|
|
|
void DynamicMode::setPreviousCount( int c ) { m_previous = c; }
|
|
|
|
void DynamicMode::setAppendType( int type ) { m_appendType = type; }
|
|
|
|
void DynamicMode::setTitle( const TQString& title ) { m_title = title; }
|
|
|
|
|
|
|
|
void DynamicMode::setDynamicItems( TQPtrList<PlaylistBrowserEntry>& newList )
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
|
|
|
|
TQStringList strListEntries;
|
|
|
|
PlaylistBrowserEntry* entry;
|
|
|
|
TQPtrListIterator<PlaylistBrowserEntry> it( newList );
|
|
|
|
|
|
|
|
while( (entry = it.current()) != 0 )
|
|
|
|
{
|
|
|
|
++it;
|
|
|
|
strListEntries << entry->text(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
setItems( strListEntries );
|
|
|
|
PlaylistBrowser::instance()->saveDynamics();
|
|
|
|
|
|
|
|
rebuildCachedItemSet();
|
|
|
|
}
|
|
|
|
|
|
|
|
void DynamicMode::rebuildCachedItemSet()
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
|
|
|
|
m_cachedItemSet.clear();
|
|
|
|
|
|
|
|
if( appendType() == RANDOM || appendType() == SUGGESTION )
|
|
|
|
{
|
|
|
|
QueryBuilder qb;
|
|
|
|
qb.setOptions( QueryBuilder::optRandomize | QueryBuilder::optRemoveDuplicates );
|
|
|
|
qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL );
|
|
|
|
|
|
|
|
if( appendType() == SUGGESTION )
|
|
|
|
{
|
|
|
|
// TODO some clever stuff here for spanning across artists
|
|
|
|
TQString artist = EngineController::instance()->bundle().artist();
|
|
|
|
|
|
|
|
if( artist.isEmpty() )
|
|
|
|
{
|
|
|
|
PlaylistItem *currentItem = Playlist::instance()->currentItem();
|
|
|
|
if( currentItem != 0 )
|
|
|
|
artist = currentItem->artist();
|
|
|
|
}
|
|
|
|
|
|
|
|
debug() << "seeding from: " << artist << endl;
|
|
|
|
|
|
|
|
TQStringList suggestions = CollectionDB::instance()->similarArtists( artist, 16 );
|
|
|
|
// for this artist, choose 4 suggested artists at random, to get further suggestions from
|
|
|
|
TQStringList newChosen;
|
|
|
|
for( uint suggestCount = 0; suggestCount < 4; ++suggestCount )
|
|
|
|
{
|
|
|
|
if( suggestions.isEmpty() )
|
|
|
|
break;
|
|
|
|
|
|
|
|
TQString chosen = suggestions[ TDEApplication::random() % suggestions.count() ];
|
|
|
|
|
|
|
|
debug() << "found similar artist: " << chosen << endl;
|
|
|
|
|
|
|
|
TQStringList newSuggestions = CollectionDB::instance()->similarArtists( chosen, 10 );
|
|
|
|
for( uint c = 0; c < 4; ++c ) // choose another 4 artists
|
|
|
|
{
|
|
|
|
if( newSuggestions.isEmpty() )
|
|
|
|
break;
|
|
|
|
|
|
|
|
TQString s = newSuggestions[ TDEApplication::random() % newSuggestions.count() ];
|
|
|
|
|
|
|
|
debug() << "found extended similar artist: " << s << endl;
|
|
|
|
newChosen += s;
|
|
|
|
newSuggestions.remove( s );
|
|
|
|
}
|
|
|
|
suggestions.remove( chosen );
|
|
|
|
}
|
|
|
|
if ( !newChosen.isEmpty() )
|
|
|
|
suggestions += newChosen;
|
|
|
|
qb.addMatches( QueryBuilder::tabArtist, suggestions );
|
|
|
|
}
|
|
|
|
|
|
|
|
qb.setLimit( 0, CACHE_SIZE );
|
|
|
|
debug() << "Using SQL: " << qb.query() << endl;
|
|
|
|
|
|
|
|
TQStringList urls = qb.run();
|
|
|
|
|
|
|
|
foreach( urls ) //we have to run setPath on all raw paths
|
|
|
|
{
|
|
|
|
KURL current;
|
|
|
|
current.setPath( *it );
|
|
|
|
m_cachedItemSet += current;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
{
|
|
|
|
PlaylistBrowser *pb = PlaylistBrowser::instance();
|
|
|
|
TQPtrList<PlaylistBrowserEntry> dynamicEntries = pb->dynamicEntries();
|
|
|
|
if( !dynamicEntries.count() )
|
|
|
|
{
|
|
|
|
Amarok::StatusBar::instance()->longMessage( i18n( "This dynamic playlist has no sources set." ),
|
|
|
|
KDE::StatusBar::Sorry );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Create an array of the sizes of each of the playlists
|
|
|
|
TQValueVector<int> trackCount(dynamicEntries.count()) ;
|
|
|
|
int trackCountTotal = 0;
|
|
|
|
|
|
|
|
for( uint i=0; i < dynamicEntries.count(); i++ ){
|
|
|
|
trackCount[i] = 0;
|
|
|
|
|
|
|
|
if ( TQListViewItem *item = dynamicEntries.at( i ) ){
|
|
|
|
if( item->rtti() == PlaylistEntry::RTTI )
|
|
|
|
trackCount[i] = static_cast<PlaylistEntry *>(item)->tracksURL().count();
|
|
|
|
else if( item->rtti() == SmartPlaylist::RTTI )
|
|
|
|
trackCount[i] = static_cast<SmartPlaylist *>(item)->length();
|
|
|
|
|
|
|
|
trackCountTotal += trackCount[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlaylistBrowserEntry* entry;
|
|
|
|
TQPtrListIterator<PlaylistBrowserEntry> it( dynamicEntries );
|
|
|
|
|
|
|
|
//const int itemsPerSource = CACHE_SIZE / dynamicEntries.count() != 0 ? CACHE_SIZE / dynamicEntries.count() : 1;
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
while( (entry = it.current()) != 0 )
|
|
|
|
{
|
|
|
|
++it;
|
|
|
|
//trackCountTotal might be 0
|
|
|
|
int itemsForThisSource = trackCountTotal ? CACHE_SIZE * trackCount[i] / trackCountTotal : 1;
|
|
|
|
if (itemsForThisSource == 0)
|
|
|
|
itemsForThisSource = 1;
|
|
|
|
debug() << "this source will return " << itemsForThisSource << " entries" << endl;
|
|
|
|
|
|
|
|
if( entry->rtti() == PlaylistEntry::RTTI )
|
|
|
|
{
|
|
|
|
KURL::List t = tracksFromStaticPlaylist( static_cast<PlaylistEntry*>(entry), itemsForThisSource);
|
|
|
|
m_cachedItemSet += t;
|
|
|
|
}
|
|
|
|
|
|
|
|
else if( entry->rtti() == SmartPlaylist::RTTI )
|
|
|
|
{
|
|
|
|
KURL::List t = tracksFromSmartPlaylist( static_cast<SmartPlaylist*>(entry), itemsForThisSource);
|
|
|
|
m_cachedItemSet += t;
|
|
|
|
}
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
KURL::List DynamicMode::tracksFromStaticPlaylist( PlaylistEntry *item, uint songCount )
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
|
|
|
|
KURL::List trackList = item->tracksURL();
|
|
|
|
KURL::List returnList;
|
|
|
|
|
|
|
|
for( uint i=0; i < songCount; )
|
|
|
|
{
|
|
|
|
if( trackList.isEmpty() )
|
|
|
|
break;
|
|
|
|
|
|
|
|
KURL::List::Iterator urlIt = trackList.at( TDEApplication::random() % trackList.count() );
|
|
|
|
if( (*urlIt).isValid() )
|
|
|
|
{
|
|
|
|
returnList << (*urlIt).path();
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
trackList.remove( urlIt );
|
|
|
|
}
|
|
|
|
|
|
|
|
debug() << "Returning " << returnList.count() << " tracks from " << item->text(0) << endl;
|
|
|
|
|
|
|
|
return returnList;
|
|
|
|
}
|
|
|
|
|
|
|
|
KURL::List DynamicMode::tracksFromSmartPlaylist( SmartPlaylist *item, uint songCount )
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
if( !item || !songCount )
|
|
|
|
return KURL::List();
|
|
|
|
|
|
|
|
bool useDirect = true;
|
|
|
|
const bool hasTimeOrder = item->isTimeOrdered();
|
|
|
|
debug() << "The smart playlist: " << item->text(0) << ", time order? " << hasTimeOrder << endl;
|
|
|
|
|
|
|
|
TQString sql = item->query();
|
|
|
|
|
|
|
|
// FIXME: All this SQL magic out of collectiondb is not a good thing
|
|
|
|
|
|
|
|
// if there is no ordering, add random ordering
|
|
|
|
if ( sql.find( TQString("ORDER BY"), false ) == -1 )
|
|
|
|
{
|
|
|
|
TQRegExp limit( "(LIMIT.*)?;$" );
|
|
|
|
sql.replace( limit, TQString(" ORDER BY %1 LIMIT %2 OFFSET 0;")
|
|
|
|
.arg( CollectionDB::instance()->randomFunc() )
|
|
|
|
.arg( songCount ) );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
uint limit=0, offset=0;
|
|
|
|
|
|
|
|
TQRegExp limitSearch( "LIMIT.*(\\d+).*OFFSET.*(\\d+)" );
|
|
|
|
int findLocation = limitSearch.search( sql, 0 );
|
|
|
|
if( findLocation == -1 ) //not found, let's find out the higher limit the hard way
|
|
|
|
{
|
|
|
|
TQString counting( sql );
|
|
|
|
counting.replace( TQRegExp( "SELECT.*FROM" ), "SELECT COUNT(*) FROM" );
|
|
|
|
// Postgres' grouping rule doesn't like the following clause
|
|
|
|
counting.replace( TQRegExp( "ORDER BY.*" ), "" );
|
|
|
|
TQStringList countingResult = CollectionDB::instance()->query( counting );
|
|
|
|
limit = countingResult[0].toInt();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{ // There's a Limit, we have to respect it.
|
|
|
|
// capturedTexts() gives us the strings that were matched by each subexpression
|
|
|
|
offset = limitSearch.capturedTexts()[2].toInt();
|
|
|
|
limit = limitSearch.capturedTexts()[1].toInt();
|
|
|
|
}
|
|
|
|
|
|
|
|
// we must be ordering by some other arbitrary query.
|
|
|
|
// we can scrap it, since it won't affect our result
|
|
|
|
if( !hasTimeOrder )
|
|
|
|
{
|
|
|
|
// We can mess with the limits if the smart playlist is not orderd by a time criteria
|
|
|
|
// Why? We can have a smart playlist which is ordered by name or by some other quality which
|
|
|
|
// is meaningless in dynamic mode
|
|
|
|
TQRegExp orderLimit( "(ORDER BY.*)?;$" );
|
|
|
|
|
|
|
|
sql.replace( orderLimit, TQString(" ORDER BY %1 LIMIT %2 OFFSET 0;")
|
|
|
|
.arg( CollectionDB::instance()->randomFunc() )
|
|
|
|
.arg( songCount ) );
|
|
|
|
}
|
|
|
|
else // time ordered criteria, only mess with the limits
|
|
|
|
{
|
|
|
|
debug() << "time based criteria used!" << endl;
|
|
|
|
if ( limit <= songCount ) // The list is even smaller than the number of songs we want :-(
|
|
|
|
songCount = limit;
|
|
|
|
else
|
|
|
|
// Let's get a random limit, repecting the original one.
|
|
|
|
offset += TDEApplication::random() % (limit - songCount);
|
|
|
|
|
|
|
|
if( findLocation == -1 ) // there is no limit
|
|
|
|
{
|
|
|
|
TQRegExp queryEnd( ";$" ); // find the end of the query an add a limit
|
|
|
|
sql.replace( queryEnd, TQString(" LIMIT %1 OFFSET %2;" ).arg( songCount*5 ).arg( offset ) );
|
|
|
|
useDirect = false;
|
|
|
|
}
|
|
|
|
else // there is a limit, so find it and replace it
|
|
|
|
sql.replace( limitSearch, TQString(" LIMIT %1 OFFSET %2;" ).arg( songCount ).arg( offset ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// only return the fields that we need
|
|
|
|
sql.replace( TQRegExp( "SELECT.*FROM" ), "SELECT tags.url, tags.deviceid FROM" );
|
|
|
|
TQStringList queryResult = CollectionDB::instance()->query( sql );
|
|
|
|
TQStringList items;
|
|
|
|
|
|
|
|
debug() << "Smart Playlist: adding urls from query: " << sql << endl;
|
|
|
|
if ( !item->query().isEmpty() )
|
|
|
|
//We have to filter all the un-needed results from query( sql )
|
|
|
|
for( uint x=0; x < queryResult.count() ; x += 2 )
|
|
|
|
items << MountPointManager::instance()->getAbsolutePath( queryResult[x+1].toInt(), queryResult[x] );
|
|
|
|
else
|
|
|
|
items = queryResult;
|
|
|
|
|
|
|
|
|
|
|
|
KURL::List urls;
|
|
|
|
foreach( items ) //we have to run setPath on all raw paths
|
|
|
|
{
|
|
|
|
KURL tmp;
|
|
|
|
tmp.setPath( *it );
|
|
|
|
urls << tmp;
|
|
|
|
}
|
|
|
|
KURL::List addMe;
|
|
|
|
|
|
|
|
// we have to randomly select tracks from the returned query since we can't have
|
|
|
|
// ORDER BY RAND() for some statements
|
|
|
|
if( !useDirect )
|
|
|
|
{
|
|
|
|
for( uint i=0; i < songCount && urls.count(); i++ )
|
|
|
|
{
|
|
|
|
KURL::List::iterator newItem = urls.at( TDEApplication::random() % urls.count() );
|
|
|
|
addMe << (*newItem);
|
|
|
|
urls.remove( newItem );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
useDirect ?
|
|
|
|
debug() << "Returning " << urls.count() << " tracks from " << item->text(0) << endl:
|
|
|
|
debug() << "Returning " << addMe.count() << " tracks from " << item->text(0) << endl;
|
|
|
|
|
|
|
|
return useDirect ? urls : addMe;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
KURL::List DynamicMode::retrieveTracks( const uint trackCount )
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
KURL::List retrieval;
|
|
|
|
|
|
|
|
// always rebuild with suggested mode since the artists will be changing
|
|
|
|
if( m_cachedItemSet.count() <= trackCount || appendType() == SUGGESTION )
|
|
|
|
rebuildCachedItemSet();
|
|
|
|
|
|
|
|
for( uint i=0; i < trackCount; i++ )
|
|
|
|
{
|
|
|
|
if( m_cachedItemSet.isEmpty() )
|
|
|
|
break;
|
|
|
|
const int pos = TDEApplication::random() % m_cachedItemSet.count();
|
|
|
|
KURL::List::iterator newItem = m_cachedItemSet.at( pos );
|
|
|
|
if( TQFile::exists( (*newItem).path() ) )
|
|
|
|
retrieval << (*newItem);
|
|
|
|
m_cachedItemSet.remove( newItem );
|
|
|
|
}
|
|
|
|
|
|
|
|
return retrieval;
|
|
|
|
}
|