diff --git a/ChangeLog b/ChangeLog index 068255cfc8..f8c3de2ed6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,2724 +1,2726 @@ Amarok ChangeLog ================ (C) 2002-2007 the Amarok authors. VERSION 1.4.6 CHANGES: * First rating star now lets you toggle between no rating, half a star, and one full star. BUGFIXES: * Fix detection of vfat devices on FreeBSD. (BR 141614) * Right-click on volume slider would change the volume. (BR 141672) + * Fix Quadratic loading in Playlists (BR 142255) Patch by Ovy + VERSION 1.4.5 FEATURES: * Added support for custom song labels. Labels can be managed through the GUI or using new DCOP functions. (BR 89314) * New DCOP functions to make it easier for scripts to use Amarok's Dynamic Collection feature. * Download songs from Shared Music (DAAP) directly into the collection. * Fadeout for Helix engine when pressing Stop. * Guided editing of the collection/playlist/devices filters. Patch by Giovanni Venturi . (BR 139292) * Added GUI options for fadeout and fadeout on exit. Both are now enabled by default. * Support for Speex (.spx), WavPack (.wv) and TrueAudio (.tta) files in the collection thanks to taglib plugins by Lukáš Lalinský . * Search inside of lyrics, by using "/" on Context Browser. Patch by Carles Pina i Estany . (BR 139210) * "Automatically show context browser" feature makes a return, as per popular request. It is however disabled by default. * Improved keyboard navigation: Space key is now a shortcut for Play/Pause, and cursor left/right seeks forward/backward. * Cover images are shown in collection browser. Patch by Trever Fischer . (BR 91044) * Send cover art to MTP media devices if they support it. * Elapsed time can be shown in OSD. Patch by Christian Engels . (BR 120051) * New redownload manager for the Magnatune.com store. Allows re-download of any previous purchase free of charge (in any format). * New items in the playlist are colorized, as a visual cue. * Show rating as stars in flat collection view. Patch by Daniel Faust . (BR 133797) * Synchronize play count, last played time and date of modification to iPods. Patch by Michael . (BR 136759) * Propose list of composers in collection when editing the composer tag from the playlist. (BR 137775) * Greatly improved sound quality for the xine equalizer. Patch by Tobias Knieper . (BR 127307) * Fancy graphical volume slider for the OSD. Patch by Alexander Bechikov . * Shoutcast stream directory. Contributed by Adam Pigg . * Support for %composer and %genre when guessing tags from filenames. * Cached lyrics are now AFT-enabled, and will follow your files around as you move and rename them. CHANGES: * Added configure option to build without DAAP support. * Album covers are now downloaded and added to album directory when purchasing from the Magnatune.com store. (BR 136680) * Update context browser when a change in the collection has been detected. (BR 140588) * Ignore leading 'The ' when sorting playlist by artist. (BR 139829) * Smart Playlists now have 'does not start with' and 'does not end with' options, as well as a dropdown for mount points. (BR 139552) * Support for cue files not matching audio files' name. Patch by Dawid Wróbel . (BR 128046) * Script Manager now remembers if categories were open or closed. * Restart collection scanner as long as not more than 5 % of the files make it crash. (BR 106474) * Ensure the first selected item in the Collection Browser stays visible when the search field is cleared using the clear button. * Duplicate filenames are now allowed on MTP media devices if the files are in different folders. * Save media device transfer queue when adding items or after transfers. (BR 138885) * Upgraded internal SQLite to 3.3.12. * MTP media devices are not automatically connected on start-up. This should solve slow loading times for those with large collections on an MTP media device. Contributed by Mikko Seppälä. (BR 138409) * Internationalize unknown artist/album/genre strings. Contributed by Mikko Seppälä. (BR 138409) * Don't assume that a device returning 0 tracks is invalid. It could just have no tracks on. Contributed by Mikko Seppälä. (BR 138409) * Magnatune store look now matches rest of Amarok much better. * Album art is displayed on the Magnatune purchase dialog. * Generic media device now has an option to force VFAT-safe filenames even on non-VFAT filesystems. * Double-clicked items in sidebar and urls passed on the command line are treated equally: append them to playlist if not yet there and start playing the first if nothing is playing. * "Scan Changes" button was replaced with "Update Collection" menu entry. * Consistent double-click behavior in sidebar. (BR 138125) * Propose name of currently loaded playlist when saving current one. * Remove support for older libmtp versions. We now require 0.0.15 or newer. * Deleting a playlist item on an MTP media device now results in it being removed from the playlist. * Magnatune store is lazy loaded to improve startup times. * Dynamic mode logic has been rethought to provide a faster and better user experience. * When checking for duplicate files on a Rio Karma media device, use track number in addition to artist, album & title. (BR 137152) * The XMMS visualization interface has been removed. LibVisual supersedes this feature. * It is now possible to select the time unit for length-based smart playlists. (BR 136841) * Show shadowed cover images in the system tray tooltip. (BR 136589) * Amarok won't crossfade if it was paused, and user started another track. Patch by Tuomas Nurmi . (BR 136428) * Amarok now saves playlists with relative paths by default. BUGFIXES: * Disable seeking in streams. (BR 140364) * With the default theme, the playlist browser info pane would not show the horizontal scrollbar if necessary. (BR 134221) * Some .rm files would make Amarok crash. (BR 137695) * Remember 'User Cover Art for Folder Icons' when organizing files. (BR 138562) * "Listening since..." has been changed to the more clear "First Played..." Patch by Andrew Ash . (BR 131727) * Fixed regression: the DEL key no longer worked in the playlist after opening the File Browser context menu. (BR 140197) * Smart playlists now work correctly with "is not" filters containing numbers. Patch by Felix Rotthowe . * Context browser would not display updated covers correctly. (BR 130518) * The select custom cover dialog no longer starts in the wrong directory for compilations. (BR 131776) * Amarok's xine engine would cut off approximately the last second of an audio file. (BR 135190) * Cue sheet would remain enabled when switching to a stream. Patch by Ted Percival . (BR 127683) * Length of tracks wouldn't be shown correctly for some cue files. Patch by Dawid Wróbel (BR 139707) * Assume that all dots but the last in script executable files belong to the script name. (BR 139460) * Don't crash when quitting while initially loading the playlist. (BR 136353) * The same track could be queued multiple times for transferring to a media device. (BR 129136) * Migrate statistics for files moved from outside to the collection. (BR 127776) * Select All/Copy action would not copy from context browser. (BR 138635) * Xine-engine: When a track was fading out (after pressing Stop), and you started another track, Amarok could become unresponsive. * Improved seeking with xine-engine. No longer jumps to 0 when you seek too quickly. Patch by Alexander Bechikov . (BR 99808) * Improved cover images handling for Various Artists. Patch by Tobias Knieper . (BR 136833) * Don't enable a mount point for devices that can't support them (mtp, njb). * With SQLite, the search in the collection browser was case-sensitive with UTF-8. Patch by Stanislav Nikolov . (BR 138482) * (Don't) Show Under Various Artists would not work when multiple albums are selected. Patch by Tobias Knieper . (BR 112422) * Changed temp download location for Magnatune purchases. (BR 137912) * Fixed potential double payment issues in the Magnatune store. * Only synchronize already set values to media devices. (BR 138150) * Correctly update total playlist play time when removing last.fm streams. Patch by Modestas Vainius . (BR 134333) * File organization jobs could not be canceled. Patch by Wenli Liu . (BR 136527) * Sending filenames to MTP media devices as UTF-8 caused problems, use Latin-1 instead. * It's now possible to delete a file from an MTP media device and re-upload it without having to reconnect the device. * Wikipedia links to edit sections are no longer shown. * Metadata is read from Rio Karma media devices as UTF-8. * Last.fm streams could be paused with DCOP or global shortcuts. (BR 133013) * Dynamic mode can still be used after a collection rescan. (BR 133269) * Dynamic mode will repopulate from all available sources. (BR 137212) * Dynamic mode no longer repeats songs often. (BR 107693) * When transferring files to a Generic media device, having certain characters (such as '#') in a tag field could cause a directory based on that field to not be created. * Editing lyrics from within the context browser no longer removes all linebreaks. * Read metadata from MTP media devices as UTF-8. * Some shoutcast streams would show an empty title. (BR 127741) * Pause would act as Play/Pause. (BR 116101) * The same track would sometimes be shown twice in suggested songs. (BR 129395) * Detect VFAT partitioned devices on FreeBSD. Patch by Daniel O'Connor . * Favorite Tracks wouldn't be shown on Context Browser, and Statistics Panel would be empty for SQLite users. (BR 136791) * Volume slider in the player window would not react correctly to the mouse wheel. (BR 136714) * When using a proxy set by script, context browser wouldn't work properly, and the application would crash when closing. (BR 112437) * Proxy settings wouldn't be respected when downloading podcast episodes. (BR 134028) * Xine engine could hang when skipping through tracks quickly with crossfade on. * Fix crash when an MTP media device returned a playlist with an invalid track ID. (BR 136552) * The Install MP3 support script would be run regardless of what the user answered to the shown dialog. (BR 136294) * OSD wouldn't always show up-to-date ratings. Patch by Tuomas Nurmi . (BR 125612) VERSION 1.4.4 FEATURES: * Transfer .wav-files to iPods. (BR 131130) * Xine and Helix engines now support three different crossfading modes: always, on manual track changes only, or on automatic track changes only. * Manually specify local file for podcast episodes via right-click menu. * Action menu entry for adding podcasts to Amarok. Based on .desktop files by Harald Sitter and Fabio Bacigalupo . * Open podcast items with external application from right-click menu. * Synchronize listened flag for podcast between Amarok and iPods. * Added integrated Magnatune.com music store. Includes artist and album info and full previews of all tracks. * Fade-out for xine-engine when pressing Stop. Patch by Tuomas Nurmi . (BR 127316) * Support downloading of files from an MTP device. * Purged podcast episodes can be readded by increasing the purge number. * Added rudimentary support for the Rio Karma. (BR 132713) * Support creation and editing of playlists on MTP media devices. * Undo/Redo functionality is now available over sessions. (BR 131072) * Allow the creation of empty playlists in the playlist browser. Available either from the Add button in the toolbar or the context menu of a playlist folder. (BR 133543) CHANGES: * Ignore leading "The " when sorting artists on media devices. (BR 136233) * Improved handling of VFAT/ASCII files and paths when organizing the collection and using the Generic media device. * Enable playing audio CDs on CD insert. Patch by Will Stephenson . (BR 136106) * Bring Amarok main window to front when starting amarok again without arguments. Patch by Lubos Lunak . (BR 135396) * Don't switch to playlist browser after saving a playlist from files tab. (BR 130189) * Add .ape and .mpc to possible file types supported by a generic media device. (BR 133491) * Move button for saving current playlist from playlist browser toolbar to playlist toolbar. (BR 129300) * Run 'kdeeject -q devicenode' when no post-disconnect command has been configured for media devices. * Reduced memory usage for MTP media devices. (BR 134663) * Faster searching on playlist and startup, due to some optimizing in string usage. Patch by Ovidiu Gheorghioiu . * Correctly translate media:, home:, ... style urls on KDE 3.5 and newer. * When tracks are added to the collection and Playlist entries already exist (as determined by the file tracking code), the corresponding Playlist entries are updated to the new location and enabled if they were previously disabled. * When file tracking is updating Playlist entries, multiple entries of the same song will now all be updated, instead of just one. * When tracks are removed from the collection (deleted on disk or moved outside of a collection folder) any corresponding entry in the Playlist will be disabled. * Dragging podcasts to to playlist will insert them in a chronological order, so you can listen to the oldest first automatically! * Improve application startup times dramaticaly by lazy loading podcast episodes. * Transferring tracks to an MTP device now shows a progress bar and doesn't hang the rest of the UI. (only available for libmtp >= 0.0.15) * Show a proper tag dialog when viewing information for DAAP music shares. BUGFIXES: * Ipod Mode on Collection Browser would have duplicated headers. * Multiple problems related to Amarok using wrong playlists on Dynamic Mode fixed. * Deleting files from generic media devices would not update the progress bar, resulting in the progress staying at 0%. (BR 130009) * If nothing at all existed on a generic device, the first item transferred would incorrectly show that an error had occured during transfer. (BR 133528) * Synchronising a smart playlist to a device when it didn't exist before would crash Amarok. (BR 135956) * Proxies would not take into account certain settings in KDE's Proxy control center modules for PAC files and more. (BR 123021) * Generic media devices would not accept files with an extension that only differs in case from a supported extension. (BR 135261) * Xine-engine: Pausing during crossfade would not work properly. Patches by Markus Kaufhold . (BR 122514 & 135285) * Stop a running cross-fading operation before starting another one. Patch by Markus Kaufhold . (BR 128629) * Queuing again would dequeue. (BR 121206) * In some cases, the Removal and Enqueue buttons in the queue manager would have no icons. (BR 115895) * Don't change length of position slider when navigating within a track. (BR 122569) * Direct copying of non-local items would result in wrong properties on iPods. (BR 135681) * Honor setting to show Amarok's menu in main toolbar. * "Burn this album" would burn all albums of the same name. (BR 121963) * Ignore double-clicks on tree item openers. (BR 125121) * Visibility of sidebar tabs would depend on the current locale. (BR 135316) * Ctrl-C for copying urls from the tag editor would not work when selected with the mouse. (BR 123327) * Check for some integral data types for improved DAAP portability. (BR 132939) * Take disc number into account when checking if a song is already on an iPod. (BR 135643) * Editing metadata in the playlist itself now matches possible alternatives case-insensitively. (BR 135683) * Fix loading directory in external browser in the tag editor when the path contains parentheses. (BR 132961) * Stop scripts using a proxy when it's disabled in KDE. Patch by Felix Geyer . * While playing Last.fm Streams, sometimes metadata wouldn't be updated on track changes. Patch by Tom Kaitchuck . * Speed patch to load playlist columns from statistic tables on population of the playlist, makes adding to the playlist and starting up faster. Thanks Ovy ! (BR 135324) * Save MTP playlists when they are renamed so we don't lose changes. * Prevent new podcastepisodes from showing up in the playlistbrowser twice by opening it's parent before adding. (BR 134108) * New iPods would not get initialized. * Files that were detected as being added back to the collection would not always be re-enabled in the Playlist. (BR 130359) * Fix some spelling and layout issues. Part of a patch by Malcolm Parsons . * Correctly handle horizontal wheel events in position slider. (BR 119254) * Don't rescan collection while transcoding. (BR 133423) * Don't try to copy to collection from urls without kio slaves. * Don't quit immediately if amarokrc was removed. (BR 134439) * The DAAP client would crash Amarok under certain conditions when kdelibs was compiled with asserts on. (BR 132851) * Configuring the toolbar would disable the stop button. Patch by Markus Kaufhold . (BR 132477) * Changed tags of songs on iPods would not propagate to its database. (BR 133842) * Fixed playlist encoding problems. (BR 133613) * Cover images for compilation albums can now be displayed full size in the context browser. * Dragging compilation albums from the collection browser or the playlist would show multiple cover images in the tooltip. (BR 133916) * Don't crash when calling repopulate dynamic mode from dcop. (BR 133716) * Last.fm streams work with proxies. (BR 131137) * Don't try to read m4a tags from apparently invalid files. (BR 133288) * Some podcasts would insert line breaks in author/title information and cause graphical errors. (BR 133591) * File tracking could fail on files that were copies of each other but with different ID3v1 or APE tags. VERSION 1.4.3: FEATURES: * New DCOP: player trackCurrentTimeMs, returns the current track position in milliseconds. * Amarok File Tracking (formerly ATF) goes public! See http://amarok.kde.org/wiki/Amarok_File_Tracking for more information. * DAAP client now supports Zeroconf. With mDNSResponder properly setup Amarok automatically shows local DAAP servers. * DAAP client saves manually added computers between sessions. CHANGES: * Performance with big playlists has been improved by a magnitude. This also makes application shutdown faster. * Remove the option to enable/disable history in dynamic mode. (BR 133076) * Reduce the minimum available tracks to show to 0. (BR 131223) * Change in file tracking behavior: IDs are no longer embedded into tags but are calculated from a portion of the file data instead, letting users with read-only music stores take advantage of it. * Don't report "/dev/hd" style devices as new media devices. (BR 127831) * Smart Playlists only load media from currently mounted devices. BUGFIXES: * Dequeuing tracks whilst in dynamic mode might not work. (BR 133449) * When marking podcast episodes as listened, update the channel icon if necessary. (BR 133497) * Don't always mark podcast channel icon as "listened" on rescan if. (BR 133495) * User added streams were not editable once saved. (BR 133483) * Cover images were not displayed in some cases. (BR 133174) * Fixed bug which prevented Amarok from creating the collection database in rare circumstances using SQLite. (BR 133072) * Collection scanner would only restart a maximum of 2 times instead of 20. (fixed in SVN revision 578922) * MTP media device support would not compile against libmtp versions >= 0.0.12. (fixed in SVN revision 576121) * AudioCD playback would stutter and sometimes freeze Amarok. (BR 133015) * Dynamic Collection broke flat collection view when the Filename column was added (BR 132874) * DAAP client shows connection errors to the user and no longer says "Loading" perpetually. After a failed connection, the user can now try again. * Don't empty media device transfer queue when canceling a transfer. * Ctrl-C for copying urls from the tag editor would not work. (BR 123327) * Delete covers from the filesystem when requested. * Show context menu on right-click in empty area of media device browser. (BR 127154) * Sort numeric columns in flat collection view numerically. (BR 130667) VERSION 1.4.2: FEATURES: * Handle itpc:// and pcast:// url protocols for adding podcast feeds. (BR 128918) * New DCOP call "collection: totalComposers" returns the number of different composers in your collection. * Synchronize playlists to media devices. * Support for MTP/PlaysForSure media devices. (BR 128532) * iPod plugin usable with iTunes phones. (BR 131487) * Browse collection by composer. (BR 122452) * New DCOP call "playlist: filenames" returns the filenames of the songs currently in the playlist. Patch by Arash Abedinzadeh * Lyrics can be edited directly on Context Browser's Lyrics tab. * Collection browse mode similar to that used by some portable players. Patch by Joe Rabinoff . (BR 130586) * BPM field. Patch by Alf B Lervåg and Aaron VonderHaar . (BR 123142) * Improved crossfading for xine-engine: Honours the 'Crossfade Length' setting precisely, and uses a better mixing style profile. Patch by Enrico Ros . * Media and collection browser tabs now support dropping. * Allow for deleting all the tracks on a playlist from iPods. (BR 127855) * Ability to create custom last.fm station from the GUI. * Ability to mark podcasts as listened. * Show error messages when connecting to last.fm streams fails. * A new media device implements a DAAP client. So Amarok can connect to iTunes, Firefly Media Server etc. (BR 100513) * Dynamic Collection: improved support for songs on removable external harddisks, SMB and NFS shares CHANGES: * Skip tracks that failed to transfer to media devices instead of stopping transfer process. (BR 130008) * libtunepimp 0.5.0 actually compiles successfully now. * Lift size limit on pathnames and comments in collection databases not managed by MySQL. (BR 130585) * Generic media device plugin is improved. Users can configure supported filetypes and get more control over the location of songs and podcasts on disk (Patch by eute). * Move composer tag to its own database table. * Re-enable adding videos to iPods with recent libgpod-cvs. (BR 130117) * Include Skip, Love and Ban in playlist right-click menu for last.fm streams. * Advanced Tag Features (ATF) deferred to 1.4.3: Public release delayed pending some bug fixes in both Amarok and a dependency. It will be automatically disabled the first time you run 1.4.2 if you had it enabled from 1.4.2-beta1. (It will still be available in subversion snapshots.) * Optionally finish transferring all queued tracks to media device after pressing disconnect button. (BR 129716) * It's now possible to edit scores and ratings for multiple tracks in TagDialog. * TagDialog won't make Amarok unresponsive while committing tags changes to files anymore. * Exact playtime as tooltip in statusbar. Patch by Markus Kaufhold . (BR 130463) * Suspend collection rescanning while organizing files. (BR 129885) * Always use metadata from original file for transcoded files transfered to media devices. (BR 131171) * Enhancements to ATF/statistics to allow for better tracking of stats as files are moved. * Tag Editing Dialog is now ATF-enabled. * In-line tag editing is now ATF-enabled. * Previously, using ATF with MP3 files would wipe out existing UFID frames from other applications. Now Amarok plays nicely and only touches its own UFID frame. * ATF no longer requires a restart to enable or disable it. * ATF read-only functions are always enabled if a UID is found in the file. Option in the configuration dialog now only controls whether new UIDs are written to new files. * ATF will now automatically run the rescan and clear the Playlist only on the first time it is enabled. After that it will simply display an info reminding users that they may need a rescan if their library has changed since the last time it was enabled. BUGFIXES: * DCOP calls to add and remove ATF tags are no longer allowed to run while the collection is being scanned. * Last.fm streams no longer freeze Amarok's GUI with xine-engine. * Sometimes metadata wasn't updated with Last.fm streams. * Update context browser on score and rating changes. (BR 132496) * Double colons in the collection filter would lead to invalid queries. (BR 132551) * Handle changed semantics of MySQL 5.0.23+ (BR 132114) * Do not try to detach() KURLs, as this would not work for non-ascii urls. (BR 132355) * Adding songs while at end of playlist could crash in dynamic mode. Patch by Joe Rabinoff . (BR 128340) * Don't update accessdate when setting songs rating or score. (BR 132274) * Increasing or decreasing volume while muted would not correctly unmute. (BR 132228) * Better resize behavior in iPod collection view mode. Patch by Joe Rabinoff (BR 132016) * Make sure a track's compilation status is returned properly when running with Postgresql. * Check directory structure on iPods of unknown type in order to detect iTunes phones. (BR 131910) * Make 'Clear' individually translatable for playlists. (BR 131521) * Retain column visibility for flat collection view. (BR 126685) * Honour proxy exceptions for MusicBrainz lookups. Patch by N. Cat . (BR 131377) * Correctly pass links containing parentheses to external browsers. Patch by Thomas Lindroth . (BR 131307) * iPods would not show podcast descriptions. (BR 129824) * Carry over rounding increments to next larger unit for fuzzy time display. (BR 131383) * If disabled, don't show splash screen - even on Kubuntu. (BR 125210) * Correctly request last.fm similar artist information for artists containing non-ASCII characters. Patch by Thomas Lindroth . (BR 131254) * Support non-chronologically ordered podcast feeds. (BR 119911) * Support for libvisual 0.4.0 was fixed. Patch by Dennis Smit. * Adding songs already on a media device to playlists would not work. * Fix adding smart playlists to media devices. (BR 130540) * Reverse check for mount point and device node when connecting to iPods for better handling of device nodes pointed to by symlinks. (BR 129965) * Make handling of filenames on iPods case-insensitive and thus fix fix problems with too many orphaned and stale items. (BR 126431) * Correct action of queueing current item in dynamic mode. (BR 130313) * Double clicking in the filebrowser will append to playlist. (BR 117465) * Fixed problems with last.fm streams containing spaces, e.g. "Hip Hop". * When generic media devices were specified manually, transferred files would not always get converted to VFAT-friendly names if they were on a VFAT filesystem. * When using ATF, tags in MP3 files would be written as ID3v2 only and existing ID3v1 tags would be stripped, which could lead to media devices and tagging libraries that were not ID3v2.4-aware to report that no tag existed. Now both tags are written with identical data. * Correct handling of filenames with special characters. (BR 132243) VERSION 1.4.1: FEATURES: * Support for last.fm streams. (BR 111983) * New playlist toolbar menu entry for adding streams to the playlist. (BR 129349) CHANGES: * Upgraded internal SQLite to 3.3.6. * Inotify support disabled for now, due to stability issues. * Tag editor is no longer modal. * Provide warning dialog when deleting items from the playlistbrowser. (BR 129313) * GUI layout reverted to the classic Amarok layout. * The Extended Info panel in the playlistbrowser is now resizeable. BUGFIXES: * Pressing return in the search bar of the Collection Browser immediately after typing a query no longer appends the wrong items to the playlist. * Fix crash when pressing Back or Forward buttons multiple times quickly in Artist tab. Patch by Thomas Lindroth . * Fix problems where blanks would be added to data if SQLite was busy. Patch by Thomas Lindroth . (BR 127608) * Automatically refresh stream lyrics on new metadata. * Set half star ratings on multiple selected tracks when clicking on an item. (BR 129449) * Only enable Show Extended Info in the Playlist Browser when information is available. (BR 126590) * Disable global shortcut for ratings when ratings are disabled. (BR 129414) * Autodetect button in Media Devices configuration dialog would not properly signal changes, so that new devices were not always saved. VERSION 1.4.1-beta1: FEATURES: * Much improved and completed custom icon theme by Vadim Petrunin . * LibVisual 0.4 supported and required. * Support for custom scoring algorithms, via scripts. * Creative Nomad Jukebox support (untested!). Submitted by Andres Oton . (BR 103185) * Inotify support. On kernels 2.6.13 and above with Inotify support compiled in, the collection will automatically be rescanned and updated as soon as a watched folder has changed. CHANGES: * First-run wizard can no longer be restarted from the application menu. However, it can still be invoked with "amarok --wizard". * Astraweb lyrics script was removed for being crappy and unmaintained. If you want to maintain it, grab it from SVN and release on kde-apps.org. * "Append Count" option of dynamic playlists has been removed. It is now always one. (BR 120044) * Context browser can now play/queue specific discs of an album or compilation. * Automatically imported playlists go into a separate category. * Block quitting amaroK until all on-going media device operations have finished with a consistent state. * Interface choice in wizard removed. * MoodBar has been removed. The maintainer has not been updating it, and it was causing crashes for many people. * Usability improvements for the Script Manager, including a tree view. * Use KMimeType for resolving file type for metadata acquisition before falling back to extension based guessing. * Removed the "detailed mode" in the playlist-browser. * Also copy non-local URLs to collection when dropped onto collection browser. * Speed up connecting media devices with a lot of tracks to be submitted to last.fm. * For media without metadata, try to read metadata after transfer to the iPod (e.g. when copying an audio CD via KIOslaves). * Hint at starting a transcode script for transcoding while transferring to media devices. (BR 127155) * If a disc number is present, append it to the album's name when organizing files. (BR 126867) * Configure, which of fresh podcasts, newest & favorite albums are shown in context browser home view. Patch by Patrick Muench . (BR 127043) * Dynamic mode no longer skips to the next song if you press play (via dcop, for instance) while already playing a track. Instead it restarts the current one. * The Actions menu has been renamed the Engage menu. It's way cooler, right? I mean, Star Trek is really cool, right? * Multiple podcasts can be configured at once by selecting multiple channels or by configuring the children of a folder. BUGFIXES: * Allow dropping of tracks after non-existant items in the playlist. * Make changes to the default dynamic playlists persistent. * Send UTF-8 encoded requests to Wikipedia. Thanks to Thomas Lindroth for the patch. (BR 127654) * Correctly restore podcast channel title when fetching fails. * Show error message when xine mp3 decoder isn't installed, don't just play next track. * Properly render and optimise playlist loading icons. * Properly import and export XSPF playlist formats. * Optimise addition of playlists to the playlistbrowser. * In context browser, show localized date for podcasts. (BR 127853) * Regression in dynamic mode caused it to skip the first track in the playlist whenever it was started. (BR 127451) * Stop Playing after Track: remember current track (BR 127312) * Radio streams were broken for protocols other than HTTP. (BR 127848) * Collection Browser would not set/unset/burn albums with ', The' in their name. * Prevent breakage when xine couldn't initialize the audio device. Patch from Ilya Konstantinov . (BR 115960) * Allow for recognition of the webdav protocol. Patch by Ilya Konstantinov . (BR 126847) * Setting a rating on an unplayed track would affect score generated. Patch by Patrick Muench . (BR 127475) * Stop tags with different capitalisation being treated as the same when building the collection. * Make database connections actually get closed when no longer used. (BR 123113) * xine engine would truncate the last seconds of a track, if no other track followed in the playlist. * Fixed AudioCD playback with xine-engine. Patch by Markus Kaufhold . (BR 127388) * If dynamic mode was turned on and then off, the previous random and repeat modes would be forgotten. (BR 123743) * Removing the current track through DCOP while editing a field of the track in the playlist would cause a crash. (BR 119152) * Make characters encoded with % (such as a forward slash, %2f) display correctly. (BR 105266) VERSION 1.4.0: FEATURES: * New DCOP call "player: version()". Returns the amaroK version. * iFP has persistent settings when transferring tracks to the device. * GStreamer-0.10 engine now supports Audio CDs. * Context menus for entries in the statistics tool. (BR 124945) CHANGES: * Composer, Disc Number and File Size columns in flat collection view. * 'k' or 'm' suffixes for matching filesize in kibi or respectively mebi bytes. * Groupings when transferring files to media devices are now persistent. (BR 127158) * Transfer contents of smart playlists to media device without adding them to a playlist. (BR 126997) * Set %albumartist to Various Artists, but keep %artist as the track's artist when organizing compilations. (BR 126936) * Discard empty tokens surrounded by {} in custom organize file format. (BR 124337) * GStreamer-0.10 engine was disabled for this release (not yet stable). * Only pick genres for Smart playlists that exist in your collection. * VFAT plugin completely rewritten since 1.4beta3. Name is now changed to "Generic Audio Player" to make it less needlessly technical. * Don't limit the number of episodes shown with a new podcast, since the user can limit the number shown afterwards by configuring the channel. * Automatically populate the playlist with items if it is empty when a dynamic playlist is loaded. (BR 126594) * Unplayed/unrated tracks are no longer shown in the statistics dialog. * Removed the option "Import Playlists". It's now always enabled. * Show total track time in context browser (BR 126548) * Derive filename for downloaded podcast episodes from their url in the rss feed. (BR 125966) * Only show albums/artists/genres with more than 3 tracks when listing favourite albums/artists/genres. (BR 126435) * libtunepimp 0.5 compiles successfully. * Podcasts are automatically configured to be checked for updates. * Show only 2 decimal places for scores in the statistics module. * Replace 'Move to Collection' in file browser context menu by 'Organize Files' for collection directories. (BR 125702) * Removed the option "Show Status Bar". It's now always enabled. * Tracks from a media device scan be submitted to last.fm immediately, without waiting for tracks to be played in amaroK. Patch by Iain Benson . (BR 125690) * Any failed attempts to submit to last.fm are now automatically retried in the background, without waiting for new tracks to be played. * Smart playlists can be constructed using mixed ALL and ANY matches (BR 124483) * Configure media devices in global settings, disable media browser when no media device is configured. * Dynamic Playlist bar made more conspicuous. * The Konqueror setting to show a 'delete' entry in the menu is now respected, if the setting exists and KDE is version 3.4 or higher. * Cover art from m4a files. Updated m4a taglib patch by Jochen Issing and patch by Shane King . (BR 125414) BUGFIXES: * The playlist would incorrectly sort after using the queue manager in dynamic mode. * Sort disc numbers numerically (BR 127114) * Smart Playlists using 'last played time' now filter correctly. (BR 127145) * If "Transcode Whenever Possible" was selected for transferring to media devices, if the file was in the device's preferred format, transcoding would not take place. Thanks to Ants Aasma for the patch. (BR 127109) * Fix possible loss of database after changing settings. (BR 126880) * Only include audio files when expanding directories. (BR 126765) * Correctly handle 'Cancel' in confirmation dialog for deleting items from media devices. (BR 126989) * Smart-Playlist random mode was not 'sticking'. (BR 126877) * Statusbar log files would only ever write to the first log after all four logs had been filled. * iFP: Don't pretend to add newly transferred files to wrong folders. * Set a podcast as listened only when it really has been listened to. * All tracks from a cuesheet will now submit correctly to last.fm. (BR 114969) * xine-engine will now correctly detect a change when only one of the artist or album metadata changes. Patch by Kim Rasmussen . (BR 126648) * Less than and between criteria in a smart playlist for playcount, rating or score of 0 now work. (BR 97046) * Empty genres are no longer displayed in the collection browser. (BR 126495) * Fix regression causing drag and drop of playlist track items in the playlistbrowser to be functionless. (BR 126387) * Fix regression causing podcast purge property to be ignored. (BR 126194) * Automatically convert MySql/PostgreSql passwords from 1.3 to 1.4 state. * Popup Messages would flicker when being shown. * Some 1.3 podcasts wouldn't get transferred to 1.4 settings. * New podcasts didn't get a default save location. (BR 126196) * Fixed encoding problems with lyrics scripts. * Mark/unmark as compilation is now stored in the file tag so it is remembered when the colection is rescanned. (BR 120428) * Submissions from media devices are timestamped so as to be less likely to conflict with submissions from another last.fm client. (BR 125367) * The MySQL connection will no longer time out when idle. (BR 120198) * Load manually configured media devices even after failed DCOP queries. Patch by Iain Benson . (BR 125692) * Copy/move to collection recurses into directories. (BR 125334) * Amazon no longer tries to refetch invalid entries. (BR 125168) * Skip hidden directories while scanning the collection. (BR 115478) * Instead of cancelling collection organiziation operations when starting new one append to running one. * Correctly show & in playlist 'Burn' right-click submenu. Patch by Laszlo Pandy . (BR 125117) * Disable option to delete remote items in playlist right-click menu. (BR 124745) * Reload playlist browser podcasts when switching database engines. * Podcast tables recreated on startup if they don't exist. VERSION 1.4-beta3: FEATURES: * amaroK now supports multiple media devices of varying types (currently iPods, UMS/VFAT, and iFP devices). * Autodetection of iPods and UMS/VFAT devices (if KDE has HAL/DBUS support compiled in). * New DCOP call "devices: showDeviceList()" to show the Device Manager's current device knowledge. * amaroK now has a custom icon theme, and an option to switch back to the system icons, if preferred (in the General settings section). * Collection browser view is separated alphabetically. Patch by Christian Hoenig . * Ease navigation with track slider below playlist window by showing mood. (BR 121715) * Show context information for podcasts. * Filebrowser: toolbar button to change to the directory of the currently playing song. (BR 115479) * Added "Play Audio CD" entry to the amaroK menu. (BR 103409) * GStreamer-0.10 engine now supports visualizations. * xine-engine: Show metadata for ogg vorbis streams. (BR 122505) * Drag and drop podcast urls directly onto podcast folders for addition. * Add media directly into directories for iRiver ifp devices. * Button to directly edit lyrics from the context browser. (BR 123515) * Support for SMIL playlists. (BR 121983) * Support for WAX playlists. (BR 120980) * Handle the Year tag when playing AudioCDs. Patch by Markus Kaufhold . (BR 123428) * Ignore 'The ' in artist names when sorting in the cover manager, as per the collection browser. (BR 122858) * Add autocompletion to the composer field in the tag dialog. (BR 123026) CHANGES: * In context browser, show information about recently updated podcasts, recently added and favourite albums when nothing is playing. * Ratings can now have half stars: click again on the last star in the rating to toggle it between a half and a full star. * Improved handling of embedded cover art, utilizing the database. Patch by Shane King . (BR 124563) * Statistics tool has had numerous improvements. * Optimise: Only rerender the CollectionBrowser when relevant. * Disable detection of iPod model and thus solve g_object_get related problems. (BR 121990) * Don't block GUI when trying to transfer large numbers of items already on media device. (BR 123570) * Update playlist items when their location is changed during organizing files. (BR 123752) * Recursively add tracks when directories are dropped to the media browser and the collection browser. (BR 123982) * Visualizations now receive stereo data from amaroK. (BR 118765) * Upgraded internal SQLite library to version 3.3.4. * Podcast information is stored in the database. * Improved password handling in the PostgreSQL config dialog. Patch by Peter C. Ndikuwera . (BR 118304) BUGFIXES: * Expand-By smart playlists were returning the wrong number of values. * Fix display of media device transfer queues larger than 4 GB. (BR 125247) * Fix duplicate detection when transferring to media device for tracks having empty album tags. (BR 125203) * Fix spuriously garbled collection scans. Patch by Shane King . (BR 125114) * Fix error with 'Back' link when browsing related artists. (BR 123227) * Files with names containing '#' or '?' from smart playlists would not get transferred to media device. (BR 122488) * Stop Playing After Track option wouldn't be shown for the right tracks, when there were queued tracks. Patch by Marcelo Penna Guerra . (BR 124297) * Don't submit podcast episodes to last.fm. (BR 118987) * Accept system:/media/ urls into the playlist. (BR 120249) * Fix leak of file descriptors with embedded cover art. Patch by Shane King . (BR 123472) * Stop collection folders being automatically removed. Instead, allow user to remove non-existent folders by deselecting parent. (BR 123745) * Stop delete key in playlist deleting last deselected item. (BR 123265) * xine-engine: Show bitrate and samplerate for CD-Audio and WAV. Patch by Markus Kaufhold . (BR 123625) * Some podcasts would cause amaroK to hang. * Check if directories still exist when showing Collection directories. (BR 123834) * Playlist popup menu had a visual glitch with Lipstik and (probably) earlier versions of Plastik. * Fixed a huge memory leak when using xine-engine with crossfading. (BR 119230) * Sometimes iRiver devices would crash upon disconnecting. (BR 123416) * Adjust the Astraweb lyrics script for a layout change on the site. Patch by Andrew Turner . (BR 123636) * Directory selection would incorrectly highlight a directory in a corner case. (BR 123635) * Don't pretend to be able to uninstall default ContextBrowser themes. (BR 123585) * Fix preamp and frequency band scaling in the xine equalizer. Patch by Tobias Knieper . (BR 116633) * OSD text would not be stripped of empty lines. * Playlist couldn't be shuffled if queued items existed. (BR 120221) * Fixed renaming of Smart Playlists. (BR 122509) * Fixed some bugs with PostgreSQL and Smart Playlists. Patch by Peter C. Ndikuwera . (BR 123317) * Escape invalid characters when transferring files to IFP devices. (BR 123199) * Escape newline characters when showing detailed information for podcast items in the playlistbrowser. (BR 123109) VERSION 1.4-beta2: FEATURES: * Equalizer for the GStreamer-0.10 engine. * Crossfade in the helix engine! * The build date is shown in the "About amaroK" dialog. * Show album covers when dragging playlist items. Patch from Jonas Hurrelmann . CHANGES: * Summarize transfer failures to media devices instead of a message for each. (BR 122491) * Don't list the entry in the engine selection widget, when it's not the active engine. Makes no sense to select this dummy engine. * The aRts and GStreamer-0.8 engines have been removed for being obsolete. * Automatically skip to the next track in the playlist when a track is unplayable. (BR 116555) * Don't check for collection changes on startup if Watch Folders is disabled. (BR 116173) BUGFIXES: * Handle .m4a files as audio when transferring to iPod video. (BR 122492) * Smart playlists would not transfer to media devices. (BR 122838) * Assume that .mp4 files are audio only when transferring to iPod. (BR 122591) * Dereference symbolic links when transferring to iPod. (BR 123206) * Correct domain for japanese wikipedia locale. (BR 122319) * When deleting a downloaded podcast, the icon wouldn't be updated. (BR 122440) * Manage Files would create duplicates on collection. (BR 122519) * On Statistics Dialog, Compilations would be shown with a random artist, and dragging to playlist would add only the tracks by that artist. (BR 122363) * When editing current dynamic playlist, the adjusting of upcoming tracks could be faulty. (BR 122401) * Changing database on First-Run Wizard wouldn't work. * When loading M3U playlists containing "." or "..", amaroK failed to detect that the files are in the collection. Patch by Ted Percival . (BR 121046) * Konqueror sidebar would show garbage for people not using UTF-8 locales. (BR 122395) * "Open in External Browser" in the lyrics tab works now. * Lyrc lyrics script handles tick characters correctly. * Crash on startup when upgrading from 1.3, using MySQL. (BR 122042) * No more crash on exit or deleting podcast. * Handle metadata for .aac files as mpeg instead of mp4. (BR 121852) VERSION 1.4-beta1: FEATURES: * AudioCD (CDDA) support for xine-engine, including CDDB lookup. Patch by Alberto Griggio . (BR 121647) * The Helix engine now supports direct alsa playback using Realplayer 10. * New DCOP call "player: setVolumeRelative(int ticks)". * Options for Random Mode to favor tracks with a higher rating, score, or ones less recently played. * Support for playing entire albums. This works just like normal, except when choosing the next track, it'll go to the next track from the album it finds in the playlist, or the first track of another album otherwise. * Support for plain VFAT devices in the Media Device browser. * You can now mousewheel over a track's queue label to change its position in the queue. * Added a time-filter to the CollectionBrowser. Now you can make it show only those tracks, which have been added to your collection within the last day, week, month or year. * Fit to Width for the playlist columns is now optional (accessible in the context menu for the column headers). * On-the-fly transcoding when transferring to media devices, provided that an appropriate transcoding script is running. * Handle compilations as such on iPods. * New DCOP calls "mediabrowser: ..." for interfacing with media devices. * Multiple simultaneously connected media devices. * Lyrics support is now scriptable. This allows to add support for any lyrics site, and makes it possible to provide upgrades. (BR 94437) * New DCOP call "contextbrowser: showLyrics(string)". * New 'File Size' column in the playlist. * Amarok now supports ASX playlist files. (BR 114051) * New DCOP call "collection: isDirInCollection(const QString& path )". * New DCOP call "playlist: removeByIndex(int)". (BR 119143) * For mp3, aac/mp4, and ogg vorbis, it's possible to use Disc Number and Composer tags. (BR 110675) (BR 90503) * For xine-lib 1.1.1 and greater, xine engine has gapless playback. amaroK is now "The Wall" compatible. (BR 77766) * Option for selecting external web browser in amaroK. No longer requires KDE-Base. (BR 106015) * Press Enter in the Collection Browser filter to send all the visible tracks to the playlist. * Hold Ctrl while pressing Enter in the playlist's filter to apply to all visible items instead of just the first, and Shift to only queue and not play them. * Tags can be edited inline in the playlist by clicking on a single selected item. * Switchable Wikipedia locale. (BR 104383) * Initial port of GStreamer engine to GStreamer 0.10. * Drag albums and compilations from context browser to media device and playlist browser. * Browse your collection and other related artists with context browser. * Copy artwork to iPods capable of displaying it. * Show extended podcast info on iPod. * Optionally update playcount for items played on iPod and submit them to last.fm and synchronize ratings between amaroK and iPod. * Tracks can now be rated from 1-5 stars manually, in addition to the score which amaroK calculates automatically based on your listening habits. You can use the 'Rating' column and Win+1..5 to change the rating. * Ability to copy items from iPod and from filebrowser to collection. * New 'Last Played' column in the playlist, showing when the track was last played. (Like in the Context Browser.) * Browsers can be now accessed with keyboard shortcuts, Ctrl+1..5. Also Ctrl+0 to close the current one, and Ctrl+Tab to switch the focus between the playlist and the active browser. * Downloaded podcast episodes can be deleted from the context menu. * New DCOP call "player: osdEnabled". * Add contents of smart amaroK playlists as playlist to media device. * Mediabrowser support for the iRiver iFP series! * New dcop call playlistbrowser loadPlaylist. (BR 110082) * New Edit Track Information dialog. Lyrics can be edited there, comments can have more than one line, some statistics and tag guessing from filename. (BR 93982) * Show/hide browsers via context menu. (BR 110823) * Display disk space on media device. * Copy standard and amaroK playlists to media device. * Create playlist from items transferred to iPod. * Edit dumb iPod playlists with media browser. * Ability to read audible.com .aa file metadata and to transfer audiobooks to iPod via file browser. * Optionally add new podcasts to media device transfer queue on download and remove podcasts already listened to on media device connect. * Add podcast shows to the Podcast folder on iPods. * Persistent media device transfer queue. * Incremental update of media device view. * Automatic scanning for stale and orphaned iPod items. * Moodbar! * configure: report not included extra features (BR 115057) * Ability to uninstall context-browser themes. (BR 111449) * More columns available in the Flat View of the Collection Browser. * New Collection Scanner, running in an external process. No longer can amaroK crash while scanning the Collection :) * Statistics tool! * Dragging external playlists into the playlist browser will add them. * NMM engine now has a configure dialog. * Collection scanner now supports WMA, MP4/AAC, and RealMedia (RA,RV,RM). * You can now Organize Music from the Collection Browser, to move and rename files to a logical place in your collection folders based on their tags. * Option to crossfade only on manual track changes. Useful for listening to consecutive tracks on a single album. CHANGES: * Dynamic Mode is now stateless, meaning there's no Dynamic Mode any more, only loading and unloading of Dynamic Playlists. There's also now a nice info bar above the playlist when a Dynamic Playlist is loaded. * The major huge context menu used for hiding/showing columns in the playlist has been replaced with a shorter one and a nice dialog. * Elapsed time / length in the systray tooltip now updates in real time as the song progresses. * Tooltips in the playlist for truncated text are now shown directly above the text, giving the effect of it being expanded to its full length. * The option for restarting scripts automatically at startup is removed, as it is now the default behaviour. * Reduced memory usage for large playlists to under 30% of pre-1.4 versions. (Measured as the difference in memory usage between an empty playlist and loading the 'All Collection' smart playlist.) * Import iTunes album art from directories. * Media Devices (Apple iPod, iRiver iFP, ...) are now handled with plugins. * New default image for albums with no cover art. * When tabbing between cells while editing tags in the playlist, autosave the contents of the previous tag you edited, so you don't have to constantly go in and out of editing mode to edit lots of tags. * When saving playlists, if there's already one with the same name, instead of complaining about it, smartly append (2), (3), etc. to the end. * 'Stop Playing After Track' now has a shortcut (Ctrl+Alt+V), and a global shortcut for the currently playing track (Ctrl+Win+V). * Various keyboard usability and focus tweaks so using amaroK with the keyboard is nicer. * Upgraded internal SQLite database library to version 3.2.7. * Recoding mp3 tags has been removed due to many unjustified complications. * Viewing track information of remote media will show the url. * "Update"-button is now hidden in the collection browser if "Watch folders for changes" is enabled in the options. * Playlist Browser now remembers which entries were open across startups. * The tooltip and the menu from the queue icon in the statusbar now shows the total length of the queued tracks. * The Home tab has been merged into the Current tab, now called Music. * New look for the current track marker in the playlist. Pimp my roK! * When turning either random or dynamic mode on, turn the other off, instead of completely disabling random mode when dynamic is on. * libgpod from gtkpod replaces kio based iPod support for improved compatibility with various iPod models. * Podcast settings are hierarchical now, meaning you can set settings for the category's, newly added podcasts take the settings from there parent category. BUGFIXES: * Dragging text to a filter line edit would still show the "Filter Here..." text in the background. (BR 108876) * Don't show an empty playlist length holder in the statusbar. * Allow for % and _ in tags, and filter them correctly. * Do not copy files of types an iPod is not capable of playing to the iPod. (BR 117486) * Also take track number into account when comparing tags for checking if a track is already present on iPod. (BR 117380) * iPod nanos would not switch off during playing songs added with amaroK because of their file size not being set. * "Show Fullsize" now works for ID3 embedded cover images. (BR 114517) * Fix possible bug when saving unencoded podcasts to strange file systems. * OSD Preview did not update colours when toggling 'Use custom colours' option. (BR 115965) * Cached lyrics are not erased when rescanning. (BR 110489) * No more "can't create amazon table" warnings. (BR 113930) * Creating a new playlist via drag-and-drop no longer shows duplicates of each song until amaroK is restarted. VERSION 1.3.9: FEATURES: * Support for libtunepimp 0.4. (BR 94988) BUGFIXES: * Fix leak of file descriptors with embedded cover art. Patch by Shane King . (BR 123472) * Playlist popup menu had a visual glitch with Lipstik and (probably) earlier versions of Plastik. * Fix preamp and frequency band scaling in the xine equalizer. Patch by Tobias Knieper . (BR 116633) * Fixed a huge memory leak when using xine-engine with crossfading. (BR 119230) * Fix memory leak in the helix engine when the player and playlist are not visible. * Stream with URLs containing "&" wouldn't be correctly saved. (BR 121846) * Playlist Browser would save invalid PLS Playlists. (BR 122875) * Refresh All Podcasts wouldn't consider subfolders. (BR 122783) * When using a folder as playlist, deleting the playlist would delete the folder and all files inside it. (BR 122480) * OSD was showing "No track playing" for tracks without metadata. * Smart Playlists with playcount or score related conditions wouldn't match all songs properly. (BR 97046) * With enormous queues, stop menu would take a lot of time to show up. (BR 120677) VERSION 1.3.8: BUGFIXES: * NMM engine would crash when seeking after the playlist finished, state Empty wasn't emitted. * Fixed URL of the Nectarine radio stream. * Fix crash after changing the alsa device in the helix configuration dialog. * When amaroK exits, send SIGTERM to running scripts. (BR 119159) * Old error messages could be shown instead of current track lyrics. * The equalizer in the helix engine now works properly at low sample frequencies. * Fixed some threading issues in loading XML playlists. * Lyrics that are available on lyrc would be shown as "not found". * The helix engine now includes protection so that misbehaving streams do not cause the visualizations to leak memory. VERSION 1.3.7: CHANGES: * In the tree view, sort tracks alphabetically first, unless one of the categories is by album, then sort by track number first. (BR 112830) * No longer delete Amazon covers every 90 days, instead relying on RefreshImages to re-download covers every 80 days to comply with the TOS of the Amazon web service. BUGFIXES: * Fix weirdness when overwriting a playlist by dragging a file to the browser. * When using Year - Album on Collection Browser, if two albums had the same year, the order would be pseudo-random. Patch by Xepo . (BR 115584) * Fix build issue on PCLinuxOS with "cpu_set undeclared". * Fix crash in helix engine caused by improper reference counting of the audiostreamresponse object. * Helix engine no longer declares it is "empty" on a track change (caused problems with context browser). * Tag dialog doesn't delete year tags any more when editing multiple tracks. * amaroK would crash or hang when fetching similar artists information from last.fm (BR 116399) * Fix memory leak in the helix engine. (BR 116223) * When changing the database type, the apply button wouldn't be enabled, and it would be necessary to restart amaroK for it to work properly. * Fix for regression in Qt 3.3.5, causing amaroK to crash when clearing the playlist. (BR 116004) * Zombie directories are removed automatically from the collection scanner. (BR 115779) * Dates wouldn't be properly loaded when editing Smart Playlists. * Number of songs to add when using dynamic mode wouldn't be respected, if the smartplaylist didn't have a ORDER BY statement. (BR 115860) * Fix visibility related build problem on some distros. VERSION 1.3.6: BUGFIXES: * Fix autoscan with PostgreSQL. (BR 111209) * Fix problem with sequences in PostgreSQL support. (BR 115075) * Fix potential crash at startup while accessing amazon.com. (BR 115838) * Potential crash when loading media from the Collection. (BR 115234) * Podcast apply to all button was faulty. * last.fm queue wouldn't be saved to disk. Patch by John Patterson . (BR 115212) * Podcast download directory would only be effective next time the application started. * Don't crash when attempting to save an empty playlist from the Playlist menu. * Loading dynamic playlists with sources did not work properly. * Fix build issue on some Linux kernel 2.4 distros. (BR 115068) VERSION 1.3.5: BUGFIXES: * Fixed a build issue. * Fixed potential crash at startup. (BR 114983) VERSION 1.3.4: FEATURES: * Helix-engine supports ALSA (using RealPlayer 11). (BR 113909) * Atom feed compatibility for podcasts. * Statusbar messages are logged to a file, statusbar.log. (BR 99899) * Podcast configuration now provides the ability to set the values for all podcasts. (BR 114371) * Downloading multiple podcasts will throw them into a queue, and each will be downloaded sequentially. (BR 114370) * Playlistbrowser items can be dragged into folders. CHANGES: * Categories in the playlist browser are now always in the order of: Playlists, Smart Playlists, Dynamic Playlists, Radio Streams, then Podcasts, regardless of sorting options. (Items in the categories are still sorted normally.) * Reworked systray icon handling -- mostly under the hood, but it'll now update properly - eg. when you change the cover. (BR 111014) * Tooltip for the queue icon in the statusbar will now show the album cover of the upcoming track. * Totals in the collection browser will now reflect the visible items if you set a filter. * Podcast settings "download on request" and "stream on request" have been merged. * About button in script manager now uses a KAboutDialog and supports rich text format in the README file. (BR 110961) * After filtering the collection browser, if only a single item is left visible, it will automatically be expanded. * Added items for the Equalizer, Visualizations, and Queue Manager to the context menus of the volume slider, analyzer, and statusbar queue icon, respectively. BUGFIXES: * If you queue an album from the context browser and then undo, the queue icon in the statusbar is now updated properly (and hence doesn't crash if you click on it). * helix-engine no longer emits new metaData if only the bitrate of a stream changes. (BR 114348) * Fix amaroK attempting to destroy your computer, reach through the monitor and violently strangle you if you attempt to exit while the collection is being scanned. (BR 114597) (BR 114859) * Postgresql code cleanup and fixed regression for manual collection scanning. Autoscan still does not work. (BR 111209) * File browser now sets to home if it was on a remote directory to prevent annoying error messages. (BR 114498) * Podcast settings would not add a trailing slash to podcast save locations. (BR 114712) * Workaround for stability issues with HyperThreading on Linux. Added a configure check to deal with buggy GLIBC's. (BR 99199) * xine-engine: Equalizer became inactive on trackchange when crossfading was enabled. (BR 114492) * Pausing a track would abort lyrics and wiki fetch jobs. (BR 114576) * Dynamic mode did not respect repeat track mode. (BR 114585) * The Script Manager no longer captures the script's stdout. * Enqueuing files with amarok -e would not work for relative paths if the working directories of the new and the running instance of amarok differ. * Visualizations would only work when amarok was run as amarokapp. (BR 99627) * The number of podcasts items would be limited even when the user didn't set it. (BR 114353) * Switching system language wouldn't affect the root folder names on Playlist Browser. * On Context Browser, when showing a cached lyric, "add", "search", and "open in external browser" buttons wouldn't work. "Open in External Browser" is now disabled for cached lyrics. (BR 110812) * Refreshing all podcasts when folder existed caused a crash. * Multiple job statusbar widget was broken. (BR 114278) * HTML in tags was getting interpreted in the context browser. * Changing the podcast purge count could sometimes cause amaroK to hang. * NMM-engine: Fixed crash after playing a song to the end, the trackEnd signal was not emitted from the GUI thread. * With Random Mode enabled and Repeat Playlist disabled, when it got to the last track, it would play it a second time and then keep on playing other tracks, instead of just stopping. * Smart-Playlists were broken with PostgreSQL. Patch by Michael Landin Hostbaek . (BR 114269) * Collection scanner ignored files with non-ascii characters. (BR 114195) * Don't show "Change Collection Setup"-box for non-local files. * Fixed issue with loading playlists containing remote URL's. * Dynamic mode history tracks would be forgotten if there was no current track on startup. (BR 110160) * Fixed problems with "Retrieve Similar Artists" feature in combination with SQLite, which could lead to 100% CPU usage. (BR 104447) * Tabbing between items and cells in the playlist while editing them now works much nicer (goes in order and doesn't tab to invisible columns), and you can also now use Alt+Up, Down, Left, Right to navigate between cells as well. * Podcast settings failed to remember the save location. (BR 114128) * Tray icon would stop filling up and showing play/pause icon if show player window was toggled. (BR 93711) * If player window is toggled during playback, playlist window's caption now correctly shows the current track's name. * Crossfade length would be enabled in Playback options when "No crossfading" was selected. * If an engine does not support crossfading, "No crossfading" is now selected in Playback options. VERSION 1.3.3: FEATURES: * New DCOP call "contextbrowser: showHome". * New DCOP call "contextbrowser: showCurrentTrack". * New DCOP call "contextbrowser: showLyrics". * New DCOP call "contextbrowser: showWiki". * Saving a playlist will cleverly pick a default name if possible. * Dragging an album cover into the playlist from the context browser will append the album. * Middle mouse button on the current track will toggle play/pause. * Ctrl-Right click on a selection of tracks will queue all of them, not just the track below the cursor. (BR 112841) * CoverManager allows for downloads from Amazon Canada. (BR 113238) * New DCOP call "playlistbrowser: addPlaylist". * New DCOP call "playlistbrowser: scanPodcasts". Will check all podcasts for new episodes. * New DCOP call "playlistbrowser: addPodcast". * New DCOP call "player: type". Returns the current track's file type. * New DCOP call "collection: migrateFile". Updates the collection db for changes made to filenames, keeping stats intact. * Smartplaylist has Length property. (BR 113039) * Added a mouse-over effect for the volume slider. CHANGES: * Adding a playlistbrowser folder will automatically focus the lineedit for renaming the item. * Removing podcasts will delete all downloaded media. * Playlists in the playlistbrowser can no longer be removed, only deleted. * Removing tracks when in dynamic mode will only replace up to the minimum upcoming tracks requirement. * Playlist columns are automatically resized when adding or removing columns. * Added a warning dialog when HyperThreading is enabled. (BR 99199) * Blacklisted GStreamer's autoaudiosink, which is really a crapsink. * Added a context menu to the volume slider. * When viewing covers in fullsize, the window has a maximum size, and scrollbars are shown if necessary. The user can also scroll the cover by dragging it. Patch by Eyal Lotem . (BR 103990) BUGFIXES: * Patch fixing an almost-infinite directory-scanning problem while building the Collection. Patch by Dirk Mueller . * Cover Manager: Album view setting became out of sync. Patch by Michael Pujos . (BR 113370) * Starting the first track in the playlist when in dynamic mode would skip it. (BR 110160) * Position slider in player-window disappeared after 2 hours. (BR 97128) * PlaylistBrowser duplicated items when overwriting playlists. (BR 108693) * Podcast settings would forget about the purge items checkbox. * The Stop button in the toolbar was always enabled at startup. * GStreamer-Engine: Could not seek to position 00:00:00. (BR 106483) * Don't crossfade the last track in the playlist. (BR 96478) * If files were in the transfer queue before connecting the iPod they would be uploaded without checking if they already exist on the device. * Using dynamic mode's playlist shuffle would result in repeated tracks tracks during a populate operation. * Fixed Xine config options were disappearing on ESC key. (BR 113225) * Fixed problems with visibility enabled compilers. Patch by Unai Garro . (BR 113056) * Fix regression causing dynamic mode playlist shuffle to break for smart playlists which relied on ordering and limits. (BR 113121) * Automatic podcast downloads did not do anything. (BR 113129) * Playlist browser items were not properly saved on quit (with Qt 3.3.5). Patch by Matthieu Bedouet . (BR 113020) * amaroK could crash on startup, if on last exit sorting was enabled in the playlist. (BR 113042) * Adding entries to a playlist and saving it could duplicate some tracks, if the playlist hadn't been expanded before. (BR 111579) VERSION 1.3.2: FEATURES: * Tabs will open automatically when dragging files between tabs. Patch by Christian Baumgart . * Two new dcop calls which allow scripts to read many of amaroK's configuration options. script readConfig(key) for strings, integers and bools. script readListConfig(key) for lists. Note that these functions aren't guaranteed to always return the latest settings (though many do). * Added a right click menu for blank areas of the playlist, with options to save, clear or shuffle the playlist and to "enable the dynamic mode & repopulate". * Playcount is shown in the tag dialog. * New volume slider, both better looking and better working than the old one. * Podcasts can be saved to any location. (BR 111059) * Added "Save as Playlist" option to the collection and file browser context menus as well. * Allow removing of items in the Media Device browser transfer queue. CHANGES: * Scroll wheel to switch tabs in context browser. * Repopulate button is enabled or disabled together with dynamic mode. * No warning dialog when starting if the directory File Browser is on doesn't exist anymore. It just reverts to home. (BR 99208) * Sorting on Collection Browser now shows "Unknown" items first, and "Various Artists" last. Years are sorted descending now. * When selecting 'Play' from the context menu on multiple items, it'll now play the first and queue the rest. BUGFIXES: * The Equalizer and QueueManager widgets were broken on window managers other than KWin. * "Year - Album" category in the Collection Browser didn't allow for dragging tracks or fetching cover images. * Xine engine no longer adds images to the playlist. * The delete key for removing playlist items works even if the file browser is open. (BR 100145) * Filenames with XML entity codes were not playable in dynamic mode and caused it to stop. (BR 108783) * If the album or artist contained "&", cover fetching wouldn't work properly. * When restarting, Playlist Browser items used for playlist shuffle wouldn't be properly marked, though they would be taken into account. * Don't crash after changing Podcast options, or after manually deleting its first item. * When renaming a playlist, the "." would be removed from the filename. Paych by Elliot Pahl . (BR 112204) * When using next and previous on Tagdialog, after passing by a stream, the fields would be always disabled. (BR 112060) * Restarting track when in dynamic mode didn't work. * Fix issues with the GStreamer engine and alsasink, and reenable it. Patch by Vincent Tondellier . (BR 112103) * Dynamic playlist shuffle had some incorrect smart playlist handling. * Robustified the code for handling the '# of tracks in the playlist' part of the statusbar, it should not ever get out of sync with reality now. Nice side effect is you can see the track count increase while a playlist is loading. * "Last played - not in the last" smart playlists would only work for sqlite. (BR 112248) * Podcast and Dynamic subfolders are correctly restored on application start. (BR 112162) * Dropping tracks onto playlist browser folders will work correctly. * Invalid podcasts are no longer discarded on quit. (BR 112116) * Fixed playing of files that have special characters like '#' in helix engine. * Fixed issue where selecting multiple items after filtering the playlist would cause all the other items 'between' them (but invisible due to the filter) to also get selected. VERSION 1.3.1: FEATURES: * Added 'Set as Playlist (Crop)' and 'Save as Playlist' options in the playlist context menu. (BR 99932) * Support for iPod shuffle devices. Patch by Guenter Schwann . * Media Device browser now has a connect button for connecting your iPod after amaroK has already been started. Also includes configurable mounting/unmounting options. * Holding down the stop button (as opposed to just clicking it) pops up a menu letting you stop either now, after the current track, or after the end of the queue. * Collection browser filter now fully supports the same Google-esque syntax as the playlist filter, plus one extra: lyrics:"stuff to search for" to search in cached lyrics. * Pressing Shift+Enter after filtering the playlist will now queue the first track. (BR 111054) * Display short statistics in the collection browser depending on the categorisation method. * New DCOP call "collection: totalTracks". Returns the total number of tracks in the collection. * New DCOP call "collection: totalGenres". Returns the total number of genres in the collection. * New DCOP call "collection: totalCompilations". Returns the total number of compilations in the collection. * New DCOP call "collection: totalArtists". Returns the total number of artists in the collection. * New DCOP call "collection: totalAlbums". Returns the total number of tracks in the collection. * New DCOP call "collection: similarArtists(int artists)". Returns the similar artists of the current track, results are limited by 'artists'. * New DCOP call "playlist: repopulate". Repopulates the playlist with tracks from dynamic mode. * New DCOP call "player: showBrowser". Allows for showing of playlist window browser, see the handbook for useage. * New DCOP call "player: setLyricsByPath". Allows adding custom lyrics for tracks. * Add an icon in the statusbar displaying the number of queued tracks; click on it to pop up a menu letting you jump to their locations in the playlist. CHANGES: * New "Blue Danna" splash screen. Created by Nenad Grujicic, modified by Nathan Adolph. * 'Stop after track' is now saved (and so remembered across amaroK restarts). * Ported playlist + filter-lineedit behaviour to collection browser as well: you can move between the view and the filter with the up/down buttons, and just typing into the view will set the filter. (BR 108656) * Wiki Tab links use the color set for links, instead of "Selected Background". Style Authors can use "AMAROK_LINKCOLOR" if they want that color. (BR 111228) * The Equalizer widget has been pimped. * Pressing 'up' in the playlist filter will now take you to the end of the playlist, in addition to down going to the beginning, as before. * When jumping to the current track, it now gets centered instead of only barely showing. * GStreamer-engine was rewritten. The crossfading feature was removed for now (it didn't work right with recent GStreamer versions). Improvements: 1) Reduced CPU usage 2) Reduced latency 3) Increased stability * No need to restart amaroK to use your iPod! * Improved Konqueror Sidebar. * The bundled "Shouter" AmarokScript (for radio stream serving) has been updated and improved. BUGFIXES: * amaroK wouldn't remember current track when restarting. (BR 110282) * Some memory leaks found and fixed. * Fix buzz and subsequent clicking when equalizer enabled in Helix and GStreamer engines compiled with GCC 4.0.1. * Burn option wouldn't show up for "Year - Album" items on Collection Browser. * Tray's tooltip would show things like 69:40 of 1:12:01. * Wiki Tab wouldn't work for names that contained "/". (BR 111634) * With KDE 3.4, the proper context menu wouldn't be shown for File Browser. Patch by Christian Baumgart . (BR 103305) * Playcounter and Access Date wouldn't be updated properly for PostgreSQL. Patch by Tonton . (BR 111519) * Clicking twice on the uninstall button for the same script, would make amaroK crash. * Fixed an obscure crash when you emptied the playlist, had the focus on it, and pressed up. * No longer show dynamic info popup on application startup. Patch by Christian Baumgart . * Sometimes the system tray tooltip did not update on song change. * Polishing for the collection browser and expanded item states. Patch by Christian Baumgart . * With xine-engine amaroK always treated remote media like radio streams. * Selecting Classical equalizer preset prompted for name. * Fixed konqueror sidebar compilation with kde <= 3.3 and gcc patched for visibility. * Konqueror sidebar can switch again between tabs. * Fixed playing of oggs in helix engine. * Fixed crash in helix engine when switching engines if helix/realplayer not installed. * Undo/Redo for the playlist was broken in some cases. * On Collection Browser, when grouping by Genre/Artist/Year-Album it wouldn't show the tracks. (BR 110890) * SmartPlaylist Editor would reset "Match Any" to "Match All" when editing. Patch by Kevin Henderson (BR 110918). * Podcasts and playlist tracks would be sorted lexicographically (BR 97297). * Saved dynamic playlists were not removable. * xine-engine: amaroK would get stuck on exit if the Equalizer was enabled and the engine playing. (BR 110791) * Dequeued items sometimes weren't being repainted properly. VERSION 1.3: FEATURES: * The tyranny of deleting covers every 90 days is over. Instead, amaroK now automatically downloads the covers every 80 days to comply with Amazon.com requirements. CHANGES: * Removed 'Apply' button from dynamic config, all config options are now hot! (Automatically applied on alteration) * Minimum score changed from 1 to 0. (BR 107944) * Playlist item lengths now shown with hours when necessary. BUGFIXES: * M3U playlists would be broken after editing. (BR 109774) * When there's no artist tag, don't show tons of unrelated songs and albums in Context Browser. (BR 110319) * Advertisements were showing up in Lyrics Tab for some songs. * When editing tags in Playlist Window, only try to write the new tag if it's different from the old one. (BR 110299) * Changes to the score in the Edit Track Information dialog should only be applied after clicking on the "Save and Close" button. * When only the score is changed, amaroK shouldn't complain if the file is read-only. (BR 109054) * Mark/Unmark as compilation wouldn't work with SQLite. (BR 109275) * Album Covers whose name or artist contained "'" wouldn't show up when fetched from Amazon. (BR 109700) * Edit Track Information dialog wouldn't update collection database if filename contained non latin1 characters. Patch by Andrey Yasniy (BR 110030) * SmartPlaylist category created in the PlaylistBrowser once the collection has been built for the first time. * Refresh the context browser as appropriate when editing tags. (BR 108884) * Cover image shown if track has no title. * Statusbar cancel button will terminate a podcast download. * Don't show multiple popup messages when retrieving podcast information. * Don't crash when adding podcasts. (BR 109982) * Tracks with urls containg apostrophes would not cache lyrics. * PostgreSQL compile problem (BR 110033) VERSION 1.3-beta3: FEATURES: * New "not in the last" option for the date fields in Smart Playlists. (BR 107725) * New OSD tokens: %directory and %type (shows whether it's a stream, or otherwise the extension). * New DCOP call "player: lyrics" (BR 100306) and Lyrics Caching. (BR 97961) * New DCOP call "player: transferDeviceFiles". Transfers queued files to the Media Device. * New DCOP call "player: queueForTransfer". Queues files for transfer to the Media Device. * Download your favourite podcasts and let amaroK manage them for you! * 17 Equalizer presets. (BR 96302) * xine-engine supports crossfading. Note: Your audio device must support mixing. SBLive, dmix or ALSA 1.0.9 will do the trick. * Shuffle the queue list in the queue manager. (BR 108861) * The audio plugin (autodetect, ALSA, esd etc.) for xine-engine is now configurable. * Playlist-Browser now remembers the state and layout of its tree view. * Show a stop icon next to the track to stop playing after. * Miniature player window for the minimalists out there! (BR 85876) * "Stop Playing After Track" now also works for queued tracks. * "Open in External Browser" button for Lyrics Tab, patch from Nick Tryon (Dhraakellian). * Funky shadow effect for the album cover @ Context-Browser and OSD. (BR 108334) * Create playlists by dragging tracks onto the Playlist Category in the PlaylistBrowser. (BR 75029) * Show OSD when pausing and unpausing. (BR 104508) * Make 'The' prefix of artists be transparent in the collection browser and sort accordingly. (BR 85959) CHANGES: * TagLib version 1.4 is required. * Renamed "Track Name" column to "Filename", "Extension" to "Type". * "Use hardware volume mixer" option has been removed. * "Play AudioCD" gets disabled for engines that don't support KIO. * The OSD (by default) and systray tooltip now show the same infos in the same order as the columns in the playlist. * xine-engine's configuration dialog has been reworked and simplified. * xine-engine has been given the highest engine plugin rank. * Systray tooltip now shows "elapsed time / total time" for the length. BUGFIXES: * When playing, the text in the current track's columns wouldn't get ellipsii added if the column was too short. * Dragging 'All Collection' smart playlist made amaroK hang. * Compilations reported incorrect number of tracks in the Context Browser. (BR 109651) * Track play icon remains even when stopped playing. (BR 107284) * Sometimes valid tracks were not submitted to AudioScrobbler. (BR 100278) * Current playlist is now being remembered when amaroK crashes. (BR 98689) * Playlist-Browser saves its state after each change, so that no data is lost when amaroK crashes. (BR 108814) * Crash when trying to save Smart Playlists after creating a Collection for the first time. * Context menu of compilations was empty in context browser. * Don't append albums and compilations when clicking on text in the context browser. (BR 98797) * xine-engine: pre-amp for the equalizer works now. (BR 104882) * Crash when changing the number of minimum upcoming tracks right after starting amaroK. (BR 108251) VERSION 1.3-beta2: FEATURES: * New DCOP call "collection: scanCollectionChanges" Scans for changes made to the collection. * Support for "media:" URLs. Patch by Sergio Cambra (BR 102668) * Support for visualizations in the Helix engine. * Queue manager to help organise your queued tracks. (BR 90594) * Ability to create Smart Playlists based on file path. (BR 92467) * Per track scripting via custom playlist context menu items. * Added advanced, Google-esque syntax to the playlist filter. Lets you do things like artist:sirenia, "pink floyd", artist:"pink floyd", or even score:>50. When just typing words, it works as before. (BR 99312) CHANGES: * Upgraded included SQLite library to version 3.2.2. * Bumped GStreamer and GStreamer-plugins dependency to version 0.8.6. * aKode-engine has been disabled (too buggy/incomplete). * Repopulate upcoming tracks on demand when using dynamic mode. * Remodel the playlist browser to incorporate dynamic mode more fully. BUGFIXES: * Don't show textual URLs in Wikipedia Tab. (BR 108031) * Don't refresh the collection view on update scans, if nothing changed. * xine-engine: Don't pop up hundreds of error messages when something goes wrong. Patch from John Lash (BR 101646) * Automatic theme download with KNewStuff works now. (BR 107313) * Clicking on "Lookup track at musicbrainz" use %2520 for spaces in URL. (BR 107946) * Crash when loading dynamic playlists without a collection. * Crash when saving smart playlist without a collection. * Do not call TagLib::MPEG::File for non-mpeg files - some FLAC files would cause the CPU to start running in circles. (BR 107029) * Many Helix engine improvements. * Crash when dragging playlist items into Playlist Browser. (BR 107709) * Improved context display when playing radio streams with xine-engine. * Number of album tracks was incorrect when showing statistics by album. (BR 107762) * Massive performance speedup for the default analyzer (BlockAnalyzer). * Dynamic mode will grab tracks from closed playlists. * Covermanager tooltips were persistent even when window closed. Tooltips have now been replaced with statusbar text. (BR 106976) * Turning off dynamic mode when items were filtered only 're-enabled' the visible items. * Disable random mode on startup if dynamic mode is on. (BR 107311) * The user is warned if saving tags failed. (BR 91568) * Sub-Folders in Playlist Browser are correctly saved and restored. * Crash after clicking on remove playlists in dynamic mode. * Crash on Context Menu in dynamic mode. VERSION 1.3-beta1: FEATURES: * Add Media dialog allows for multiple file selection. (BR 105903) * The browser-sidebar has been redesigned for improved usability. * Cue file sheet support. Patch from Martin Ehmke . (BR 92271). * New OSD text token, %playcount, will write the playcount. * SmartPlaylists are editable. (BR 91036) * PlaylistBrowser gets a makeover! * New playlist column "Playcount" for track play counts. * New playlist column "Extension" allows easy sorting of playlist for compatible file types for portable media players. * Ability to save streams to the PlaylistBrowser (BR 91075, BR 104139) * New DCOP call "playlist: popupMessage" Displays a popup message box in the playlist window.. * New "year - album" - group by mode for collection browser. (BR 94845) * New DCOP call "player: setScoreByPath(url, int)". Sets score of a track specified by it's path. * New DCOP call "player: setScore(int)". Sets score of the current track. * New DCOP call "player: path()". Returns the path of the current track. * New DCOP call "playlist: saveM3u(path, relativePaths)". * New ScriptManager notification: "volumeChange: int". * Tooltips for album covers in the CoverManager. (BR 103996) * Automatic download of themes and scripts via KNewStuff. * Different analyzers available for the playlist window. * New DCOP call "player: enableRepeatTrack" sets repeat track on or off. * HelixPlayer-engine. * 'Load' and 'Append' entries for smart playlist context menus. (BR 99213) * Support for reading embedded images from ID3 tags. (BR 88492) * Wikipedia tab in ContextBrowser allows for artist biography retrieval and more, supporting 9 different languages! (BR 98050) (BR 104383) * Show "title by artist" on playlists titlebar and taskbar. (BR 97670) * Option to show stats in the Home tab by album. Patch from Cédric Brégardis . * New DCOP call "script: listRunningScripts()". Returns a list of all currently running scripts. (BR 102649) * New DCOP call "script: stopScript(name)". Stops a script. (BR 102649) * New DCOP call "script: runScript(name)". Runs a script. (BR 102649) * New form of playlist manipulation - Dynamic Mode. * New DCOP call "player: enableRepeatPlaylist" sets repeat playlist on or off. (BR 102754) * Add Score widget into the tag editor. (BR 100084) * Support for PostgreSQL as database backend. (BR 99863) CHANGES: * "amarokscript" filename extension is now mandatory for script packages. * Append Suggestions has been superceded by Dynamic Mode. * Add a label (with shortcut) to the Playlist filter. BUGFIXES: * Message box when saving of playlist failed (BR 105520) * Avoid weird results when fetching lyrics with slow connections. (BR 103561) (BR 101327) * Compensate for reversed slider widget in reverse layout locales, such as Hebrew and Arabic. Patch from Assaf Gillat . (BR 102978) * Playlist playMedia now works with streams. * Context Browser is updated when current track's tags are changed. (BR 102839) * Clearing the playlist while playing a track does not lead to a confusing interface anymore. (BR 103510) ==BEGIN KDE 3.3 DEPENDENCY== VERSION 1.2.4: FEATURES: * Queue selected tracks shortcut, Ctrl+D. (BR 83675) BUGFIXES: * The first engine entry in the config dialog was always blank. * If you filtered by more than one word in Collection Browser, adding expandable items (eg: artists or albums) wouldn't work. (BR 100150) * Updating the collection without any changes being made to it kept the Update button disabled forever. * Application freezes when switching shoutcast streams. (BR 103890) * MusicBrainz lookup was not escaping quote characters. (BR 103740) * Fixed crash when clicking the "clear" button in CoverManager's filter widget. * Update lyrics page on new radio stream metadata. (BR 99725) * xine-engine was reporting bogus tracklengths for ogg vorbis. (BR 102547) VERSION 1.2.3: FEATURES: * Graphequalizer script can now enable and disable the equalizer. * New DCOP call "player: equalizerEnabled" returns whether or not the equalizer is enabled. * OSD notification for mute. * Mute global shortcut, Win+M. * Add %comment token for comment display in OSD. (BR 100944) * View/Edit track entry into context menus of ContextBrowser and CollectionBrowser. * You can mark/unmark albums as compilations via CollectionBrowser's right-click contextmenu. * New DCOP call "collection: query(const QString& sql)". Allows to make arbitrary queries on the Collection database. * New DCOP call "playlist: removeCurrentTrack()". (BR 92973) CHANGES: * Show "Artist - Title" for compilation discs in CollectionBrowser and ContextBrowser. * Upgraded internal SQLite database to 3.2.0. * DCOP call saveCurrentPlaylist() now returns the path to current.xml. BUGFIXES: * Appropriate context menu entry for changing queue status for multiple playlist items. * Fix regression preventing dequeuing multiple selected tracks. * 'Show Toolbar' remembers its settings between sessions. (BR 98662) * When doing Musicbrainz lookup from the Context browser, search for the real track, not the whole album. * Memleak when a radio stream stalled. (BR 102047) * The Collection Scan finally checks for the right file modification time. * Adding a compilation disc from ContextBrowser was broken. * GStreamer-engine: Reduced the gap when switching to next track without crossfading. * GStreamer-engine: amaroK was swallowing the beginning of a track when Fade-in was set to zero. (BR 94472) * Use a better highlight color in the "Configure Collection" dialog. (BR 102059) * "Remove Duplicates / Missing" fixed. Removes dead entries correctly. * Fix units for samplerate. (BR 101528) * amaroK using 100% CPU on some systems. (BR 101524) (a KHTML bug which got exposed by code in amaroK 1.2.2) VERSION 1.2.2: FEATURES: * Context Browser CSS styles can now be installed and selected from the appearance settings. * Append Suggestions now has an icon in the statusbar. * When selecting multiple files, the "View/Edit Meta Information" dialog will show the tags that are common to all of them. (BR 100423) * A line graph equalizer added as a script "graphequalizer." CHANGES: * Add 25-track and 50-track smart-playlists. * Update current-track icons to include greater padding. * The contextbrowser now uses data:-URLs instead of temp image files, so they cannot be left on disk when amaroK terminates unexpectedly, and the Konqueror/Universal sidebar can show them when amaroK is not running. BUGFIXES: * escape '&' char in contextmenu entry (BR 101276) * Track is set as a number in the database, so shouldn't be added rounded by quotes. (BR 101208) * Rewrote the broken .pls playlist parser. * Handle delay gap between songs properly with aRts engine. (BR 90404) * Switched order of "Make playlist" and "Queue after current track" menus to avoid playlist destruction. (BR 96164 part 1) * Visualizations with LibVisual didn't work in some cases. (BR 99627) * amaroK could fail to build if the whole kdeextragear-1 module was compiled, due to conflicts with K3B on the MusicBrainz check. (BR 100906) * Images shown on OSD where incorrect for action notifications. * The handbook translations were not built when amaroK was installed from the tarball. I've written a new release script in Ruby, which can handle the new structure of kde-i18n. (BR 100498) * GStreamer-engine can now play vorbis radio streams properly, with full metadata support. (BR 89821) * GStreamer-engine now uses the "decodebin" autoplugger, which fixes the lag issues that some users had during crossfading. (BR 99570) VERSION 1.2.1: FIX: Made the Tag-Editor only operate on visible items. (BR 100268) ADD: Database settings added to the first-run wizard. FIX: playlist2html generates UTF-8 output now. (BR 100140) FIX: Bitrate/length showed random values for untagged mp3 files. (BR 100200) FIX: Crash when recoding stream MetaData without CODEC selected. (BR 100077) CHG: Show an additional "Compilations with Artist" box in ContextBrowser. ADD: Remember collapse-state of boxes in ContextBrowser. (BR 98664) ADD: Display an error when unable to connect to MySQL. ADD: Konqueror Sidebar now has full drag and drop support. CHG: Replaced "Blue Wolf" icon with Nenad Grujicic's amaroK 1.1 icon, due to legal issues. ADD: Parameter "%score" shows the current song's score in OSD. CHG: When you delete a song within amaroK, it gets removed from the Collection automatically. FIX: Directory column in the playlist was eating the first letter. ADD: New DCOP call "playlist: setStopAfterCurrent(bool)". (BR 99944) FIX: Coverfetcher: Do not crash when no cover was found. (BR 99942) ADD: Support for amazon.co.jp cover fetching CHG: Toolbar items reordered for optimal usability, as suggested by Aaron "Tom Green" Seigo. FIX: Show covers for albums containing chars '#' or '?'. (BR 96971 99780) ADD: Help file for the playlist2html script. ADD: New DCOP call "playlist: int getActiveIndex()". ADD: New DCOP call "playlist: playByIndex(int)". CHG: Upgraded internal SQLite database to 3.1.3. FIX: Update the database after editing tags in playlist. (BR 99593) ADD: New DCOP function "player: trackPlayCounter". (BR 99575) ADD: .ram playlist support with code from Kaffeine. (BR 96101) FIX: amaroK can now determine the correct track-length even for formats unknown to TagLib. Makes it possible to seek e.g. in m4a tracks. ADD: Can now pick from multiple Musicbrainz results. Patch from Jonathan Halcrow . (BR 89701) ADD: May now set a custom cover on multiple albums in the Cover-Manager. ADD: Support relative path of tracks in writing playlists. (BR 91053) FIX: Don't inline-edit tags for the whole playlist's selection. FIX: Fix "Recode Tags" crash issues. (BR 95041) ADD: "Set Custom Cover" can fetch remote images. (BR 90499) VERSION 1.2: ADD: "Repeat Track" status is reflected by an icon in the playlist. ADD: New icons from tightcode for statusbar and repeatTrack. ADD: New Smart-Playlist "Ever Played". CHG: Bumped GStreamer version requirement to 0.8.4. CHG: Made it possible to use artsdsink with GStreamer again. CHG: Don't read m3u files recursively when dropping a folder on the playlist. No more doubled entries. FIX: Shoutcast radio with GStreamer is improved, no more dropouts when starting a stream. ADD: The "Similar Artists" feature (using Audioscrobbler) can now be switched off. (BR 95280) FIX: Error in Shoutcast http-request, which made it impossible to play many radio streams with GStreamer and aRts. (BR 97211, 98569) CHG: Better default directory for selecting a custom cover. FIX: ContextBrowser reloads after setting a custom cover. (BR 96548) FIX: Cover-Manager's full-screen view works with Bughira (brushed metal). ADD: Script-Manager can auto-run scripts on application startup. ADD: aKode engine, depends on KDE 3.4. No configure check yet. FIX: Don't add non-audio files to the Collection. CHG: We now use the SqlLoader, which greatly improves the performance of adding stuff to the playlist from SmartPlaylists and the Collection. VERSION 1.2-beta4: ADD: It is now possible to select the right image if there are multiple results from Amazon. Patch from Gregory Isabelli . (BR 93287) CHG: Reorganized the DCOP interface. We used to have all DCOP functions in the "player" group. Now it's splitted up into several categories. Attention script writers: Adjust your DCOP calls! FIX: The loader is now more robust and should always find amarokapp. CHG: The search-browser has been integrated into the file-browser. CHG: OSD can have fake transparency and new fancy shadow. ADD: DCOP function "shortStatusMessage", shows a temporary message on the application's statusbar. FIX: Frequent crashes when writing tags. (BR 95344) FIX: CoverManager updates its status display correctly. FIX: "isPlaying" DCOP function now works correctly. (BR 90894) ADD: Automatic crash report generator, sends backtraces to amaroK HQ. ADD: DCOP function "saveCurrentPlaylist". Writes the playlist to current.xml, for scripts that need to access the playlist contents. ADD: Playlist2html, a script for playlist exporting. (BR 96199) ADD: Improved statusbar, with animated error notification widget. ADD: New progress display system, can show multiple expandable progress widgets in the statusbar. ADD: Alarm script, starts playing music at specified alarm time. ADD: Script-Manager for DCOP script extensions is now functional. Refer to the amaroK Wiki for information on script writing. ADD: Collection-Browser shows a help message in flat-mode when filter is empty. (BR 97000) CHG: It is possible to select the Database Engine (SQLite, MySQL) runtime, without amaroK restart. New Database Engines can be added, they need to inherit DbConnection and implement its' virtual methods (see SqliteConnection and MySqlConnection). CHG: New amaroK icon "Blue Wolf", made by Da-Flow. FIX: Possible crash when enabling Player-Window. (BR 94668) VERSION 1.2-beta3: ADD: Smart Playlists can have a random order or a score weighted random order (BR 90861) ADD: Show total length of selected songs in statusbar. (BR 90284) ADD: Context-Browser now caches the tab widgets. Patch from Matias Costa . (BR 95999) FIX: RAND and REP buttons were always enabled at startup. (BR 95861) ADD: Implemented "Append Suggestions" functionality. It means that when enabled, amaroK will append a couple of suggested songs to playlist when you play a track. This produces a continuous playlist, something similar to listening to radio. ADD: Implemented "Play Media..." functionality. FIX: Playlist-Browser was appending to playlist when clicking "Load". Now it replaces the current playlist again, as intended. ADD: Profile for KDELIRC (Remote Controls). Patch by Dirk Ziegelmeier . ADD: Remove Duplicates now also removes dead entries from playlist. FIX: Accept album-dragging from the ContextBrowser. (BR 86020) FIX: Configure check was missing for the Konqueror Sidebar (depends on KDE-Base). FIX: Browser splitter was drawn incorrectly with some styles. (BR 95333) ADD: DCOP call for relative seek. Patch by Andreas Pfaller. (BR 84989) CHG: Bumped TagLib dependency to 1.3.1. (1.3 is too damn buggy) FIX: CTRL-M can show the menubar again after hiding. (BR 94139) ADD: Support for last.fm streams. FIX: amaroK icon shows correctly in window decoration under GNOME. ADD: Support for ID3v2 cover images. (Thanks to M. Thiesen!) (BR 88492) ADD: DCOP calls for the status of Random Mode, Repeat Playlist and Repeat Track. ADD: DCOP call to return the sample rate. ADD: DCOP call to return the track number. (BR 94825) FIX: GStreamer-engine provides better scope synchronisation. ADD: Save current track position and play queue on exit. (BR 90379) FIX: Fix Directory column on playlist, show absolute directory path instead of empty string. (BR 90361) ADD: DCOP call to scan your collection. (BR 84621) FIX: When an engine fails to load, respect the rank while choosing the next engine. VERSION 1.2-beta2: FIX: Classic amaroK theme looks better. ADD: Context Browser has CSS styling. FIX: Cover fetching improvements/fixes. ADD: Last played: yesterday, etc. in ContextBrowser. FIX: Big speedup for PlaylistLoader, when adding many items. ADD: Show songs you once played, but didn't play for the longest time on ContextBrowser's Home-page. (least played) (BR 89479) FIX: Don't crash on song switch, when there's only one visible playlist item and repeat-list is activated. (BR 94030) CHG: Add and queue tracks after the current track. (BR 94121) ADD: DCOP call to raise the equalizer configuration dialog. ADD: Konqueror sidebar to view playing info and control amaroK. ADD: DCOP call to clear the playlist. (BR 90149) ADD: DCOP call to enable/disable the equalizer. ADD: DCOP call to return the score of the currently playing track. ADD: Audioscrobbler submit queue stored on disk. Tracks that are listened when offline will be available for submitting later. CHG: "Start Scan" button was renamed to "Update". Now it starts an incremental scan instead of a full rescan. FIX: Lyrics parsing failed for certain songs. (BR 94269) ADD: xine-engine saves config, and implements crossfade, bug fixed too. ADD: Player-Window can also show the BlockAnalyzer. CHG: Run incremental scanning once a minute instead of every 30 seconds. FIX: When collection scanning was interrupted with Cancel, incremental scanning didn't work any longer. CHG: Handle incremental file scanning in a thread. Now the GUI doesn't get blocked every 30 seconds, anymore. (BR 93564) ADD: CollectionBrowser now offers two operation modes: The classical TreeView and a new FlatView (like the WinAmp Library). FIX: Caching of local cover images was broken for non-unique filenames. (BR 94068) FIX: "Visualizations" menu entry was always disabled. FIX: Play button was sometimes stuck in disabled state. FIX: OSD was showing "%artist - %track" instead of "%artist - %title". FIX: Forward command line option --engine to amarokapp. FIX: CoverFetcher was always looking for "album - album". VERSION 1.2-beta1: ADD: Full support for Audioscrobbler, including submission of tracks. FIX: Arts engine resumes from position when session is restored. ADD: Vorbis stream metadata support (GStreamer-engine). (BR 82378) ADD: Cover image and lyric fetchers include filters for common extensions, such as (Disc 1). (BR 90630) ADD: Ability to choose from four different Amazon locales. (BR 90664) ADD: OSD now draws gradient instead of solid colour. ADD: 'Stop after current song' functionality. (BR 88652) FIX: Queue function from context/collection browsers actually properly queues tracks. (BR 90319) ADD: MySQL database support. Patch by Andreas Mair . Please refer to mailing list for detailed instructions. ADD: Metadata history for streams in Context-Browser. (BR 89839) ADD: Command line option --engine. ADD: OSD text is now configurable, and it displays the album cover. FIX: Remote folders are read recursively when dropped on the playlist. FIX: Audiocd protocol in filebrowser had empty folders. ADD: Cache system for current-track animation in playlist. Reduces CPU load when the playlist is visible. ADD: 10-band IIR equalizer for GStreamer and xine engines. FIX: The background gradient effect in Context-Browser is now much faster. The gradient also looks nicer. (BR 91276) FIX: Password-protected streams did not work correctly. (BR 91184). Patch by . ADD: NMM-engine was rewritten and updated for the latest NMM release. Supports audio and video playback. ADD: Cover-Manager supports drag-and-drop. ADD: Tags are now read from the Collection database if they are already stored. This speeds up adding items to the playlist. (BR 90137) ADD: Context-browser shows "Suggested Tracks", utilizing audioscrobbler. FIX: Configure does no longer print "Good - Configure has finished" when a dependency is missing. ADD: Intelligent automatic resize for playlist columns ADD: Shaded current-track marker in playlist. ADD: Automatic song lyrics display. CHG: Internal SQLite upgraded to 3.0.8. VERSION 1.1.1: FIX: Crash when using GStreamer-engine on 64bit. (BR 90869) CHG: New splash screen by Nenad Grujicic . FIX: Crash when fetching 1 missing cover using the fetch button. (BR 90673) REM: Unsupported option "Show Metadata in Playlist". ADD: Menubar (optional). FIX: GStreamer-engine now resumes playback at correct position. ADD: iCandy for Context-Browser: Background gradient and toolbar. CHG: Collection-Browser now has a toolbar instead of menubar. FIX: With "Title Streaming" disabled GStreamer could not play streams. FIX: Osssink is now the default sink for GStreamer. If sink initialization fails, a dialog will ask to select another sink. FIX: Pausing failed on some systems with GStreamer-engine. (BR 90417) FIX: Never scan the same directory twice. FIX: Disable CD-burning menu for streams. (BR 90336) ADD: Open Cover-Manager from Context-Browser popup-menu and main menu. FIX: Made amaroK build with --disable-amazon flag. FIX: Docs translations were not installed correctly. (BR 90307) FIX: GStreamer-engine refused to play some mp3 files. (BR 90317) VERSION 1.1: FIX: Huge speedup for Context-Browser, makes changing tracks faster. ADD: Progress display for Cover-Manager. CHG: Systray animation is now optional. CHG: Updated included sqlite to 3.0.7 (stable). ADD: Tag editor can operate on multiple files (mass tagging). FIX: Collection encoding broken for non-latin1 characters. (BR 89747) ADD: Popup-menu for cover images in Context-Browser. FIX: The first track to play is now random for random-mode. (BR 77055) FIX: Show systray on startup. (BR 89661) FIX: Let xine recognise tracks that have non lower-case extensions. VERSION 1.1-beta2: ADD: K3B integration for burning CDs. (BR 88052) ADD: Third category for Collection-Browser. (BR 83609) ADD: Playlist search now supports categories. (BR 86296) ADD: Support for MAS (Media Application Server). MAS-engine is in experimental state. ADD: Context-Browser shows information about radio streams. ADD: Custom Smart Playlists with built-in editor. ADD: Systray icon shows track progress and play status. CHG: Imported SQLite3 and ported CollectionDB. ADD: "Cool-Streams", a list of amaroK Squad recommended streams for playlist-browser. ADD: Detecting Sampler/VA discs in CollectionBrowser (shown as "Various Artists"). (BR 81683) ADD: Configuration GUI for xine-engine. ADD: Next and previous track buttons for Tag-Editor. ADD: Player-window adapts to current color scheme. ADD: Crossfading and fade-in/out function for GStreamer-Engine. ADD: Genre and Favorite Tracks by Artist smart playlist in the Playlist-Browser. ADD: IMMS-like rating system for songs. FIX: aRts-engine has been ported to the new engine interface and is available again (but not recommended). FIX: Try to autodetect Sampler-Discs and show them properly in the Contextbrowser. (BR 87182) FIX: Multiple items can now be selected in the CoverManager. Thanks John Hughes (BR 87584) FIX: Various fixes for certain Artist/Album names, which had problems with cover support. FIX: Sorting the collection is now case-insensitive. (BR 84141) CHG: Symlink infinite recursion check for collection scan. FIX: Show all accessible cover images in the tooltip. (BR 87283) FIX: Clicking an album in the ContextBrowser adds items in the correct order, now. (BR 87733) VERSION 1.1-beta1: ADD: Wizard for configuring amaroK on first startup. CHG: Made it possible to use the next/previous buttons when amaroK is not playing. ADD: DCOP call to switch Random Mode on or off. (BR 84460) ADD: DCOP call to retrieve current track's cover image. (BR 85364) FIX: Problem with cover-saving for certain artist/album names. (BR 84171) FIX: Show contextual information for songs, even if they are not in the current collection instead of an ugly empty box. ADD: GstEngine: Support for custom output plugin parameters. (BR 83949) ADD: CoverManager - for downloading and managing album cover images. CHG: Refactored engine plugin interface. Each engine can now provide specific configuration GUIs. ADD: As-you-type search for FileBrowser. ADD: Seeking with mousewheel in playerwindow. REM: Stream-Browser. ADD: New meta-info dialog, with editable tags and MusicBrainz support. ADD: Inline-tag editing auto-completion based on the Collection Database. ADD: Deleting files physically from playlist context menu. (BR 75208) ADD: Fadeouts for GStreamer-Engine. ADD: New Playlist Browser, organizes multiple playlists, and offers smart playlist functionality. ADD: Support for redirected streams and streams with no specified port. ADD: KIO support for GStreamer engine. Allows playing media via all protocols supported by KIO (ftp, audiocd, fish, etc). ADD: SearchBrowser operation can now be aborted. ADD: Progressbar in CollectionBrowser informs about scan progress, and a button was added for aborting the scan. (BR 83019) ADD: Playlist sliders (volume and position) now move directly when clicked outside of the handle. (BR 83611) ADD: Untagged tracks now go into Collection too, listed as "unknown". ADD: Automatic album cover fetching is back and improved. ADD: Option for automatically switching to Context when playback is started. CHG: Stream timeout value is now determined from KDE user settings. ADD: Support for password-protected streams, by wef . FIX: GStreamer engine must not allow non-audio filetypes in playlist. ADD: Icon for "Menu" button in toolbar. Improves Usability. VERSION 1.0.2: ADD: xine-engine plugin, audio only. FIX: aRts-engine: Compatibility with newer aRts versions improved. FIX: aRts-engine: Streams sometimes stopping shortly after playback was started. (BR 84417) CHG: Increased stream connect timeout to 12 seconds. VERSION 1.0.1: FIX: Short dropouts after starting a stream with GStreamer. FIX: amaroK starting invisible when systray icon is disabled. FIX: Playlist analyzer looks freaky on some systems. (BR 83671) FIX: Display filename in title column for wav files. (BR 83650) FIX: Don't show crash dialog when no engine plugins are found. FIX: Compile issue for KDE < 3.2.1 users. Sorry :( VERSION 1.0: FIX: Plugin versions are validated. Prevents crashes with ancient plugins. FIX: Configure now checks for gtk/gdk headers for the XMMSwrapper. REM: Removed cover download feature for this release. FIX: Do not crash if an unreadable dir is added to the collection. FIX: Check database-sanity on startup and recreate broken tables (BR 83205). FIX: CollectionBrowser was broken, when amaroK was running "localized". FIX: TitleProxy hogging 100% CPU when unable to connect to server. CHG: Bumped GStreamer requirement to 0.8.1. ADD: Glowing player window icons. ADD: amaroK finally remembers if it was hidden on exit. ADD: OSDPreview now has snap to regions. FIX: Newly shown columns in playlist can now be resized. FIX: BR 82020: next/prev buttons disabled when they shouldn't be. ADD: ToolbarAnalyzer remembers it's framerate, allowed fps: {50, 40, 30, 20}. ADD: Full streaming audio support for GStreamer engine. FIX: Don't allow user to get into a situation where there is no Menu. ADD: Using Welcome-page power-links you can switch between XMMS and amaroK mode. CHG: New icons and splash screen, by Roman Becker . ADD: Allow the current GL analyzer to be detached/attached from the main window with the 'd' key. FIX: Filtering the collection now searches the second category, too (BR 81681). FIX: Filter in playlist was only working for the first argument. CHG: Collection-Monitor now processes removed dirs in a thread. ADD: Added a switch to toggle OSD's text-shadow. (BR 82011). ADD: More detailed track information dialog for Collection Browser. FIX: Track length was always 0 for certain filetypes (e.g. mod, wav) (BR 82673). FIX: Gst engine refusing to add certain filetypes to the playlist, when the engine was idle (BR 82713). FIX: Rare playlist redraw bug, which resulted in messed up items. VERSION 1.0-beta4: ADD: CollectionDB now caches and rescales images. This binds cover art usage in amaroK to the collection, but offers greatly improved speed for cover retrieval and uses less memory. FIX: Cover not shown in ContextBrowser, when song gets played for the first time ever (BR 81241). ADD: Cover art fetcher, downloads album cover images from amazon.com. ADD: Configure->Playback->Device && default device option for audiosinks. ADD: ContextBrowser now also shows your overall-favorites and the newest tracks in your collection. Therefor I had to reset the statistics, sorry. FIX: Decode %-encoded characters in filenames, like %2f for a slash. (BR 74576). CHG: Songs you click in ContextBrowser will now directly start to play and won't be added to the playlist, if they already are there. FIX: "Start Scan" menu-entry gets disabled while scanning. (BR 81619). FIX: Directories with non-ascii chars don't get scanned (CB) in multibyte locales. CHG: Enhanced "Fill-Down" feature for track column (auto-increment) (BR 81194). FIX: Closing xmms-visualizations freezes amaroK (BR 81326). FIX: CollectionBrowser does not sort by tracknumber (BR 79600). FIX: ContextBrowser's URLRequests need to be escaped. FIX: Always show OSD (if enabled) on volume changes. FIX: Filtering the collection using tokens with number(s) at the beginning or end failed. (BR 81621). FIX: FileBrowser didn't remember its current folder (BR 81816). ADD: Expand/collapse items by doubleclicking in Collection (BR 81710). FIX: Allow OSD still to be shown via shortcut when disabled (BR 80388). FIX: Collection: live-monitoring dirs for changes works again. FIX: Changing volume by mousewheel on systray icon works again. ADD: Collection automatically rescans itself on startup. ADD: "Add to Playlist" feature in CollectionBrowser, appends tracks to playlist. ADD: Clear button for CollectionBrowser search. FIX: Problem with invisible "Play next" marker in playlist. FIX: Don't try to create sql-tables on every startup, but only on sql-scheme (DATABASE_VERSION) changes. FIX: Display splash screen on correct desktop with Xinerama. CHG: CollectionBrowser filter now works in "search-as-you-type" mode. FIX: Prevent TitleProxy from showing the same metadata over and over. FIX: Compatibility bugfixes to TitleProxy, thanks to Daniel Molkentin . I think we've now got 100% Shoutcast compatibility. ADD: Allow changing volume by using the mousewheel anywhere on the toolbar. FIX: Wheel-scrolling toolbar's volume slider doesn't change volume (BR 81155). FIX: ContextBrowser is now shown in proper colors for every scheme. CHG: Added track's physical location to the Meta Information dialog. FIX: Show last playtime in localtime instead of UTC. FIX: ContextBrowser not showing all items for current album. FIX: Not all SQL queries were "string-escaped". ADD: Added statistics database, which keeps track of how often and when you play a specific song. VERSION 1.0-beta3: ADD: Additional volume slider for playlist window. ADD: ContextBrowser shows you images and information to the current song/artist. It depends on the collection and is presented as an HTML widget. CHG: Improved color handling and visual feedback in the GUI. ADD: Global shortcut for play/pause action, as requested by multimedia-keyboard users (BR 79541). CHG: Small player-window can be switched off now. FIX: CollectionBrowser out of order after scanning. FIX: TitleProxy partly rewritten. Should be more compatible with many streams and not be able to freeze the app any longer. FIX: When playing a stream with title streaming activated, the track is not marked as playing (BR 79999). FIX: Invoking "Track Information" in Collection Browser sometimes crashed the application (BR 80266). FIX: In CollectionBrowser's folder setup dialog pressing cancel did not abort (BR 80451). Thanks to Michael Pyne for patch. ADD: Option for selecting sound output system (OSS/Alsa). Currently only used with GStreamer engine. CHG: Extended and updated handbook, thanks to Mike Diehl . ADD: Context menu item "Make Playlist" in Collection Browser generates new playlists on the fly, without the need for drag-and-drop. CHG: Renamed several files and folders in the source code tree, resulting in improved code accessibility. VERSION 1.0-beta2: FIX: Crash on AMD64 due to assumption about pointer size. CHG: SQLite library sourcecode now included with amaroK. CHG: The collection-thread now inserts its data in a temporary database while scanning, which allows us to safely use the collection in the meantime. This is done by two concurrent sqlite-connections (thread-safe). Wrote a new class named CollectionDB, which handles the database communication for the collection. ADD: URLDrag from Playlist, so you can drag and drop to xmms. Doesn't work with the FileBrowser yet, but it will! CHG: CollectionBrowser now fills the database inside of a thread, resulting in improved performance. ADD: Mini track-position slider in statusbar. FIX: Don't try to crossfade with engines that do not support this feature. ADD: XMMS visualization plugins can be configured with their GUI. FIX: Collection filtering had some regressions FIX: Loader on some systems not able to start amaroK. FIX: Switching engines at runtime breaking volume control. FIX: GstEngine skipping tracks directly after starting, when crossfading enabled. CHG: Database system now works with linked tables. Saves hdd-space and cpu-time. CHG: If you remove the current song from the playlist, we don't define the next song anymore, but let it be randomly selected (only when random mode is on!) CHG: Random Mode now respects the playlist filter and only picks items, which are currently visible in the playlist. Also removed a crash situation. CHG: Removed the search-token index. Searching now iterates through the playlist, offering direct and specific access to the metadata. FIX: Bug where fill-down would cause lots of extra tags to be written when a search is in progress (BR 79482). FIX: Defect in plugin framework code, leading to a crash on some systems during engine plugin initialization. FIX: Restoring current playlist on startup (BR 79436, BR 79439). ADD: Searching the Collection with a filter. FIX: BrowserWin's QLabels are painted white in amaroK's own color scheme. VERSION 1.0-beta1: ADD: Search Browser - search stuff on your hdd ADD: song count on playlist statusbar ADD: support for XMMS visualization plugins ADD: Collection Browser - a database powered music collection manager ADD: Playlist toolbar is now configurable ADD: toolbar analyzer in playlist window ADD: use XML playlists internally within amaroK so tags don't have to be loaded/reloaded all the time. Makes undo/redo much quicker. FIX: non latin1 locale issues with loading directories and tags (thanks Leo Zhu) ADD: clicking shuffle will sort the playlist by the nextQueue first, and randomise the rest ADD: Play Next can now handle several songs through a queue. The queue can be manipulated by using the context menu or by CTRL+right clicking. ADD: much improved gstreamer engine, now working with visualizations CHG: GstEngine requires gstreamer-0.8 FIX: Show move pointer instead of hand when moving preview OSD. ADD: sorting by artist subsorts by album and track, sorting by album subsorts by track, enjoy! ADD: browserTabs float over the playlist when in set to not overlap FIX: communication loader<-->amarok failing on FreeBSD FIX: loader forgetting to close socket descriptors FIX: FileBrowser remembers that state of its view between sessions CHG: converted engines to plugins. they are now dynamically loaded at runtime ADD: plugin framework CHG: made amaroK aRts-independent. with the --without-arts configure switch it's possible to build the app without aRts support, using only NMM or GST ADD: Shift drag appends items to the end of the playlist. FIX: startup notification icon staying on screen when amaroK started by loader FIX: amaroK showing the "X" icon instead of the correct one VERSION 0.9: CHG: playlistBrowser removed until next release FIX: playerWidget font is now configurable, you need to start new track for the scrolling marquee to get updated. Default font is used by default. FIX: fixed several stability issues concerning stream-playback ADD: whatsthis for all configurable options. FIX: amaroK registering with dcop as "amarok-PID". it's back to just "amarok" now. FIX: OSD not updating correctly when changing volume VERSION 0.9-beta3: ADD: "Show Current Track" button in playlist. ADD: Volume OSD when changing with mousewheel over trayicon. CHG: software volume mixer uses a logarithmic function to make the scale more natural ADD: Global shortcuts to display OSD and increase/decrease volume. (Win+o and Win+KP_Add/KP_Subtract by default, respectively) ADD: DCOP calls to control OSD and playback volume ADD: ported config-GUI for audio decoders to new engine (works currently with modplug_artsplugin) FIX: show correct track-length when playing .mod or .sid with aRts-engine ADD: loader application, starts and controls amaroK. it reduces the lag when handing command line arguments to amaroK and makes the splash load faster ADD: playlist items, which couldn't be opened / read (for some reason) will be marked with a grey background color ADD: pasting clipboard selection into playlist with MidButton, X11-style CHG: refined on-screen-display with more polished look FIX: skipping broken/non-existant tracks CHG: If the current song is paused, the Play Button will resume, not restart it. FIX: respect "hide playlist with main window" and playlist minimize/hide behaviour. ADD: new OSD configuration options: bgcolor, screen position VERSION 0.9-beta2: CHG: some look-and-feel polishing in the main player window ADD: option to turn off analyzers ADD: splash-screen shown during program startup (optional) FIX: made stream playback with TitleProxy more stable (by using an unbuffered socket) ADD: show stream metadata in on-screen-display CHG: transformed "EQ" button into a togglebutton, which can also hide the effect browser ADD: new OpenGL analyzer, contributed by Enrico Ros FIX: FreeBSD compile fixes, contributed by Markus Brueffer FIX: rewritten configure: checks properly for kdemultimedia presence, and adds --without-opengl and --without-gstreamer arguments VERSION 0.9-beta1: ADD: display warning when artsd is not running with realtime priority ADD: Audioproperties are loaded as you scroll the playlist and get saved to playlist files ADD: If trackname column is hidden, the title column will show the trackname until a title tag can replace it. If no title tag is found the trackname stays. CHG: Pressing "back" in Random Mode now works as expected and walks backwards through the list of recently played songs. ADD: TitleProxy searches for a free local port (contributed by Stefan Gehn) CHG: Random Mode now stores the recently played songs in a buffer, which prevents playing the same songs too often. ADD: "Play Next" context menu option ADD: selected aRts-effects will be remembered on next program start, including settings FIX: sort numerical playlist columns in correct order ADD: logarithmic fading algorithm makes crossfading smoother ADD: Select a series of tracks, start inline tag-editing a tag and amaroK will prompt you to edit that tag for all tracks one-by-one. Also available: fill-down. ADD: improved crossfading: will fade out smoothly when the stop button is pressed FIX: O(n) behavior for playlist scrolling fixed ADD: setting to make playlist colours the KDE defaults ADD: support for tag-editing directly in playlist CHG: replaced old FileBrowser with the comfortable fileselector from KDevelop CHG: analyzers now powered by a new, more flexible FFT routine ADD: hide/show selected playlist columns CHG: upgrade streambrowser to kderadiostation 0.5 FIX: many streams not loading from browser and AddItem dialog CHG: amaroK moved out of kdenonbeta. we are now member of KDE Extra Gear 1 ADD: on-screen-display (OSD), shows an overlay with information on the currently playing track CHG: use KMultiTabBar for browser selection CHG: migrated settings system to KConfig XT ADD: playlist columns for length and bitrate ADD: merged new audio engine in. this provides a generic interface class, with multiple backends. right now there is a backend for aRts and one for GStreamer (still rudimentary) ==BEGIN KDE 3.2 DEPENDENCY== VERSION 0.8.3: FIX: build issue VERSION 0.8.2: ADD: added Hide/Show Playlist global shortcut (thanks gogo) CHG: mousewheel over trayicon behaviour changed CHG: search tokens can now be entered in random order ("Presley Elvis" will find "Elvis Presley") FIX: qt 3.1 compile issues VERSION 0.8.1: FIX: compilation problem with KDE < 3.1.3 VERSION 0.8.0: FIX: KDE 3.1 compatibility re-gained ADD: hitting return in the search field of the playlist starts playback of the first visible playlist entry (Qt >=3.2 only) FIX: fixed crash bug in playlist searching FIX: fixed crash bug when removing playlist-items CHG: new layout has been adopted ADD: added accepting files dropped onto systray icon FIX: significant reduction in memory consumption for PlaylistItems FIX: hardware mixer works again CHG: replaced sliders with custom slider class, which fits better in our design FIX: exchanged c32-app-amarok.png with the correct (active) version FIX: amarok.desktop file. now we show up in the k-menu again. FIX: crossfading aRts module. the fading is now much smoother than before FIX: crossfading bug. before the fix amaroK sometimes mixed up the two xfade sources, so it sort of faded in reverse (==crap) ADD: tag reading in separate thread ADD: re-added m_optCrossFade, so we don't lose the crossfade length on switching it on/off. set default crossfade length to 2500. CHG: "Title Streaming" on by default CHG: integrated streambrowser into playlist window ADD: added dcop implementation for url adding. Relevant diffs for mediacontrol are available. FIX: libamarokarts detection code ADD: added long-awaited DCOP methods for manipulating the playback. This also adds integration with kdeaddons/kicker-applets/mediacontrol. CHG: moved DCOP handler to a separate class/file ADD: threaded playlist insertion FIX: removed bugs and waste code keyhandling in browser*, it mostly works as expected now with various keypresses going to the correct places FIX: cleaned the playlist class's public interface, also fixed some unreported bugs in process (inconsistent recursive behavior), please keep the encapsulation, it's a good thing (tm) FIX: tweaked undo/redo behavior CHG: exchanged old player icons with new ones made by Alper Ayazoglu a.k.a. cubon ADD: clicking on EQ button activates effect selection widget ADD: KJanusWidget as a sidebar for filebrowser mode selection FIX: pushing enter in lineedit goes up a level ADD: a stream browser, can only DnD, separate window, not great yet FIX: finally fixed the ancient "annoying-noise-when-pressing-pause" bug FIX: should keep track of currently played item no matter what you do to the playlist, has a nice side effect of remembering the last played song, too. FIX: write undo for Shuffle FIX: the expandbutton doesn't fire events when it has had its stack expanded (behaviour a-la Winamp Classic) FIX: crash when pressing right mouse button while stream is connecting ADD: show bitrate for streams with icecast support FIX: save stream names as #EXTINF in m3u files ADD: bug report dialog ADD: proxy for decoding shoutcast/icecast metadata (experimental!) ADD: amaroK now in bugs.kde.org ADD: configurable delay after each track. currently 0-10 seconds in 1 sec increments but could easily be made to use finder increments if ppl want - piggz (www.piggz.co.uk) ADD: viswidgetv2. it seems a lot smoother on my machine. its quite easy to tweak the dynamics is needed. is accessible the same as the other widgets, just click until it appears (though it looks the same as the original widget it just acts differently) - piggz (www.piggz.co.uk) ADD: combo with history and completion for dir/file chooser ADD: in configure.in.in for checking the version of TagLib, if compiled from CVS, if not, then show, that it uses bundled version of TagLib - Stormy FIX: font dialog sizing issues ADD: resume playback option. Using this means your track starts up again where you left it last time you quit amaroK. Excellent feature for us developers :-) VERSION 0.7.0: FIX: collection of fixes related to showing/raising/hiding the playlist when showing/raising/hiding the mainWidget FIX: by muesli: make playlist searches a bit faster at the expense of memory FIX: (partial fix) bitrate/samplerate font overlap at large font sizes change: less staccato loading of widgets change: pause makes the analyser bars fall to zero rather than just vanish ADD: xfade when starting tracks by doubleclick FIX: global shortcuts can now be changed FIX: tracks skipping randomly change: "BrowserWin Enabled" on by default change: "Save Playlist" on by default change: "Show Metainfo" on by default FIX: make loading playlist not block UI FIX: on startup load playlist after UI is shown change: "Software Mixer Only" on by default FIX: make timedisplay also work for streams FIX: volume slider adjusting FIX: when dropping tracks to PL, order will stay the same as in FileBrowser ADD: FileBrowser sortable by clicking on header ADD: analyzer that distorts a bitmap ADD: multiple analyzers now possible ADD: "Software Mixer Only" option Removed stale sigplay() Cleaned a couple "deprecated" warnings ADD: undo and redo playlist actions FIX: rewritten config dialog and moved into separate file ADD: started configurable colors change: spectrum analyser bars now have dynamics, ie. they move smoothly between values ADD: mouse wheel over systray icon changes the track, hold shift to change the volume change: rearranged menu order for systray (quit = last) change: moved volume slider to the right, lets see if this is better ADD: started a font selection page in settings FIX: Stream urls are now properly demangled/unescaped (%20 => space etc) VERSION 0.6.91: FIX: ExpandButton submenu now slightly delayed FIX: dropping items into playlist ADD: drop-target indicator line in PlaylistWidget, providing visual feedback ADD: tray menu ADD: random mode ADD: crossfading between tracks ADD: vertical lines between columns in Playlist ADD: alternating item colors in Playlist ADD: column "directory" in PlaylistWidget (for Grue:) ADD: sorting by clicking on column headers in PlaylistWidget FIX: rewrote directory reading code in BrowserWidget.cpp. code is now much more readable, and it also fixes a bug. ADD: additional columns in playlist for tags FIX: made metainfo reading algorithm faster change: switched to TagLib for metainfo reading ADD: button "play" in PlayerWidget.cpp is now a toggleButton ADD: tray icon FIX: playlist window is optionally hideable with main widget when iconified to tray VERSION 0.6.0: Release :) VERSION 0.6.0-PRE5: fixed: animated buttons don't get stuck anymore fixed: invoking help changed: MetaInfo reading now off by default. the slowdown was potentially confusing to new users added: documentation fixed: cleaned up Makefile.am a bit fixed: defined new APP_VERSION macro, since the old approach did not work with CVS changed: put amarok into KDE CVS (KDENONBETA) added: applied Stormchaser's button patch. the AmarokButtons now work in a more standard conform way. Thanks Stormchaser, blessed be :) VERSION 0.6.0-PRE4: added: buttons in playlist window for play, pause, stop, next, prev. a.k.a. stakker mode :) removed: "load" button. this functionality is now provided by "Add item" added: more sanity checks on pointers fixed: when track in playlist does not exist, we now skip to the next track fixed: all aRts references are freed correctly at program exit fixed: effects will not be forgotten any more when EffectWidget is closed VERSION 0.6.0-PRE3: fixed: crash when URLs were dropped onto filebrowser from other apps fixed: URL dialog now accepts remote files added: correct caption for ArtsConfigWidget added: "amaroK Handbook" menu entry, calling KHelpCenter changed: amarok gets installed into multimedia now fixed: PlayObject configuration VERSION 0.6.0-PRE2: changed: safety question at program exit now off by default removed: button "sub" - it was useless changed: clearing playlist does not stop playing anymore - for Grue ;) fixed: potential crash at startup added: menu option to configure PlayObject fixed: crash when removing currently playing track VERSION 0.6.0-PRE1: fixed: flicker in glowing item fixed: another memory leak in analyzer (hopefully the last one!) added: playlist widget can display metainfo instead of filenames added: repeat track / repeat playlist VERSION 0.5.2 - 0.5.2-DEV6: fixed: memory leak in analyzer code. added: shortcut for copying current title to the clipboard added: slider position can be changed by just clicking somewhere on the slider added: icon added: url can be entered directly above the filebrowser widget changed: removed the "jump" widget. you can now enter a filter string directly above the playlist widget added: playlists (.m3u and .pls) can now directly be dragged into the playlist added: support for .pls (audio/x-scpls) added: amarok is now completely network-transparent. any kind of folder, local as well as remote, can be browsed and played. added: check for libamarokarts. amarok won't crash anymore if it's not found added: the time display now has a mode for showing the remaining time, too fixed: crash when clearing playlist, after playlist has played till the end. clearing the playlist stops the playing now. added: new gfx in playerwidget fixed: progressbar sometimes not working, zero tracklength fixed: font of bitrate/frequency display too big on some systems added: command line options added: timedisplay is now updated during seeks added: saving window positions and size on exit added: due to popular request, I finally changed the behaviour of the "play" button. it's now possible to start a track on a fresh playlist without double-clicking an item. fixed: compile error on GCC 3.3.1 in visQueue.cpp. bugfix by thiago added: completely rewrote drag-and-drop code. works recursively now (optionally). plus dragging stuff from other applications into amaroK also works now. VERSION 0.5.1: added a Tip of the Day at startup to explain the user interface a bit added restarting of artsd on first program start to make sure it registers the new mcopclasses fixed possible compile error in viswidget.cpp amaroK uses much less CPU now than it used to. This was mainly achieved by using a new FFT-analyzer module, which I took from Noatuns "Winskin"-plugin, and modified slightly to my needs. Also some other optimizations were made, which improved the standby performance, when no song is playing. I'm still not satisfied with overall performance, tho, but it seems that most of the load is produced by the aRts code itself, so this will rather be difficult to improve. fixed crash when "next" or "previous" was pressed without a track loaded thanks to valgrind I was able to find and squish some serious bugs, most of which were related to pointers. to sum it up: pointers are evil. valgrind is great. lots of UI-changes in the main widget. uses a background pixmap now, a custom font and widget for the time-display, and generally looks better fixed issues with the liquid skin. unfortunately, there seems to be no way to display pushbuttons correctly with a black background under liquid. so, until I find a solution for that, the expandbutton widget doesn't look quite as cool as it used to. maybe I should ask mosfet about this.. VERSION 0.50: renamed 0.15 to 0.50 VERSION 0.15: playing streams now works! *yipeeee* fixed tons of bugs in aRts playing code. i think i got it right now. fixed loading and saving of playlists. can cope with all protocols now. fixed a bug in EffectWidget.cpp, that gave a compile error on some systems. Converting QString into std::string was not done correctly. Thanks to Whitehawk Stormchaser for that one :) changed project name to "amaroK" and built new project-file VERSION 0.14 (internal): implemented use of arts-software-mixing, in case hardware-mixing (/dev/mixer) doesn't work fixed crash when play was pressed without selecting a file changed the direction of the volume-slider. maximum is now at the top added automatic saving of current playlist on exit added previous/next track added two radiobuttons in the playerwidget for toggling the playlist/equalizer on and off. admitted, the equalizer doesn't yet exist, so it's just a dummy button :P added popup-menu for the playerwidget. opens on right mouse button. this menu finally replaces the ugly menubar. added some icons (from noatun) for the player-buttons instead of text added pause function changed most names in the source to comply with the (unofficial?) KDE c++ coding standard (using the prefix "m_" for member attributes and so on). This was real slave-work :/ cleaned up code in several classes fixed problem where subwidgets got keyboard focus and were drawn dark with the liquid style. switched off focus completely, since it's not needed for this type of application VERSION 0.13 (internal): added cute animated pushbuttons with sub-menus added saving playlists added dragging items inside of playlist widget added forward declarations in header files to reduce compile time added saving of browserwin/splitter size rewrote track information widget. used a html table for the text. looks much nicer now :) fixed sorting function fixed jump widget. removed huge memory leaks in the widget fixed flicker in analyzer widget tons of bugfixes in playing code. partly rewritten. seems to be much more stable now VERSION 0.12 (internal): added ChangeLog and TODO added grid under scope display added saving of options, like current directory and playlist added detection of mimetypes added adjusting volume by mousewheel added skipping to next track after playing added loads of sanity/safety checks bugfixes (tons of) in playlist code, partly rewritten bugfixes in scope code VERSION 0.1 - 0.11: internal versions, no changelog tried no less then 4 different sound interfaces: mpg123, smpeg, alsaplayer, and finally aRts diff --git a/src/atomicstring.cpp b/src/atomicstring.cpp index 6e6a0cbfdd..24ce66c5e7 100644 --- a/src/atomicstring.cpp +++ b/src/atomicstring.cpp @@ -1,242 +1,237 @@ /* Copyright (c) 2006 Gábor Lehel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #ifdef HAVE_STDINT_H #include #endif #include #include #include #include "atomicstring.h" #if __GNUC__ >= 3 // Golden ratio - arbitrary start value to avoid mapping all 0's to all 0's // or anything like that. const unsigned PHI = 0x9e3779b9U; // Copyright (c) Paul Hsieh // http://www.azillionmonkeys.com/qed/hash.html struct AtomicString::SuperFastHash { uint32_t operator()( const QString *string ) const { unsigned l = string->length(); const QChar *s = string->unicode(); uint32_t hash = PHI; uint32_t tmp; int rem = l & 1; l >>= 1; // Main loop for (; l > 0; l--) { hash += s[0].unicode(); tmp = (s[1].unicode() << 11) ^ hash; hash = (hash << 16) ^ tmp; s += 2; hash += hash >> 11; } // Handle end case if (rem) { hash += s[0].unicode(); hash ^= hash << 11; hash += hash >> 17; } // Force "avalanching" of final 127 bits hash ^= hash << 3; hash += hash >> 5; hash ^= hash << 2; hash += hash >> 15; hash ^= hash << 10; // this avoids ever returning a hash code of 0, since that is used to // signal "hash not computed yet", using a value that is likely to be // effectively the same as 0 when the low bits are masked if (hash == 0) hash = 0x80000000; return hash; } }; struct AtomicString::equal { bool operator()( const QString *a, const QString *b ) const { return *a == *b; } }; #endif class AtomicString::Data: public QString { public: uint refcount; Data(): refcount( 0 ) { } Data( const QString &s ): QString( s ), refcount( 0 ) { } }; AtomicString::AtomicString(): m_string( 0 ) { } AtomicString::AtomicString( const AtomicString &other ) { s_storeMutex.lock(); m_string = other.m_string; ref( m_string ); s_storeMutex.unlock(); } AtomicString::AtomicString( const QString &string ): m_string( 0 ) { if( string.isEmpty() ) return; Data *s = new Data( string ); // note: s is a shallow copy s_storeMutex.lock(); m_string = static_cast( *( s_store.insert( s ).first ) ); ref( m_string ); uint rc = s->refcount; if( rc && !isMainThread()) { // Inserted, and we are not in the main thread -- we need to make s a deep copy, // as this copy may be refcounted by the main thread outside our locks (QString &) (*s) = QDeepCopy( string ); } s_storeMutex.unlock(); if ( !rc ) delete( s ); // already present } AtomicString::~AtomicString() { s_storeMutex.lock(); deref( m_string ); s_storeMutex.unlock(); } QString AtomicString::string() const { if ( !m_string ) return QString(); // References to the stored string are only allowed to circulate in the main thread if ( isMainThread() ) return *m_string; else return deepCopy(); } QString AtomicString::deepCopy() const { if (m_string) return QString( m_string->unicode(), m_string->length() ); return QString(); } bool AtomicString::isEmpty() const { return !m_string; } const QString *AtomicString::ptr() const { if( m_string ) return m_string; return &QString::null; } uint AtomicString::refcount() const { if ( m_string ) { s_storeMutex.lock(); uint rc = m_string->refcount; s_storeMutex.unlock(); return rc; } return 0; } AtomicString &AtomicString::operator=( const AtomicString &other ) { if( m_string == other.m_string ) return *this; s_storeMutex.lock(); deref( m_string ); m_string = other.m_string; ref( m_string ); s_storeMutex.unlock(); return *this; } -bool AtomicString::operator==( const AtomicString &other ) const -{ - return m_string == other.m_string; -} - // needs to be called holding the lock inline void AtomicString::deref( Data *s ) { checkLazyDeletes(); // a good time to do this if( !s ) return; if( !( --s->refcount ) ) { s_store.erase( s ); // only the main thread is allowed to delete stored strings if ( isMainThread() ) delete s; else s_lazyDeletes.append(s); } } // needs to be called holding the lock inline void AtomicString::ref( Data *s ) { checkLazyDeletes(); // a good time to do this if( s ) s->refcount++; } // It is not necessary to hold the store mutex here. bool AtomicString::isMainThread() { // For isMainThread(), we could use QThread::currentThread(), except the // docs say it's unreliable. And in general QThreads don't like to be called from // app destructors. Good old pthreads will serve us well. As for Windows, these // two calls surely have equivalents; better yet we'll have QT4 and thread safe // QStrings by then. // Note that the the static local init is thread safe. static pthread_t main_thread = pthread_self(); return pthread_equal(pthread_self(), main_thread); } // call holding the store mutex inline void AtomicString::checkLazyDeletes() { // only the main thread is allowed to delete if ( isMainThread() ) { s_lazyDeletes.setAutoDelete(true); s_lazyDeletes.clear(); } } AtomicString::set_type AtomicString::s_store; QPtrList AtomicString::s_lazyDeletes; QMutex AtomicString::s_storeMutex; diff --git a/src/atomicstring.h b/src/atomicstring.h index 06707e605f..b486c01cc8 100644 --- a/src/atomicstring.h +++ b/src/atomicstring.h @@ -1,196 +1,201 @@ /* Copyright (c) 2006 Gábor Lehel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * A thin wrapper over QString which ensures only a single copy of string data * is stored for equivalent strings. As a side benefit, testing for equality * is reduced to a pointer comparison. Construction is slower than a QString, * as it must be checked for equality with existing strings. (A hash set is * used for this purpose. According to benchmarks, Paul Hsieh's SuperFastHash * (which is currently used -- see http://www.azillionmonkeys.com/qed/hash.html) * can hash 5 million 256 byte strings in 1.34s on a 1.62GHz Athlon XP.) For * other use, the overhead compared to a plain QString should be minimal. * * Added note: due to QString's thread unsafe refcounting, special precautions have to be * taken to avoid memory corruption, while still maintaining some level of efficiency. * We deepCopy strings, unless we are in the same thread that *first* used * AtomicStrings. Also, deletions from other threads are delayed until that first thread * calls AtomicString again. Thus, we would appear to leak memory if many AtomicStrings * are deleted in a different thread than the main thread, and the main thread would * never call AtomicString again. But this is unlikely since the GUI thread is the one * manipulating AtomicStrings mostly. You can call the static method * AtomicString::isMainString first thing in the app to make sure the GUI thread is * identified correctly. This workaround can be removed with QT4. * * @author Gábor Lehel */ #ifndef AMAROK_ATOMICSTRING_H #define AMAROK_ATOMICSTRING_H #include "config.h" #if __GNUC__ >= 3 #include #else #include #endif #include "amarok_export.h" #include #include #include class LIBAMAROK_EXPORT AtomicString { public: /** * Constructs an empty string. * @see isEmpty */ AtomicString(); /** * Constructs a copy of \p string. This operation takes constant time. * @param string the string to copy * @see operator= */ AtomicString( const AtomicString &other ); /** * Constructs a copy of \p string. * @param string the string to copy */ AtomicString( const QString &string ); /** * Destroys the string. * Note: this isn't virtual, to halve sizeof(). */ ~AtomicString(); /** * Makes this string a copy of \p string. This operation takes constant time. * @param string the string to copy */ AtomicString &operator=( const AtomicString &other ); /** * This operation takes constant time. * @return whether this string and \p string are equivalent */ - bool operator==( const AtomicString &other ) const; + bool operator==( const AtomicString &other ) const { return m_string == other.m_string; } + + + bool operator<( const AtomicString &other ) const { return m_string < other.m_string; } + +// bool operator!=( const AtomicString &other ) const { return m_string != other.m_string; } /** * Returns a reference to this string, avoiding copies if possible. * * @return the string. */ QString string() const; /** * Implicitly casts to a QString. * @return the string */ inline operator QString() const { return string(); } /** * Useful for threading. * @return a deep copy of the string */ QString deepCopy() const; /** * For convenience. Equivalent to isNull(). * @return whether the string is empty * @see isNull */ bool isEmpty() const; /** * For convenience. Equivalent to isEmpty(). * @return whether the string is empty * @see isEmpty */ inline bool isNull() const { return isEmpty(); } /** * Returns the internal pointer to the string. * Guaranteed to be equivalent for equivalent strings, and different for * different ones. This can be useful for certain kinds of hacks, but * shouldn't normally be used. * * Note: DO NOT COPY this pointer with QString() or QString=. It is not * thread safe to do it (QString internal refcount) * @return the internal pointer to the string */ const QString *ptr() const; /** * For convenience, so you can do atomicstring->QStringfunction(), * instead of atomicstring.string().QStringfunction(). The same warning * applies as for the above ptr() function. */ inline const QString *operator->() const { return ptr(); } /** * For debugging purposes. * @return the number of nonempty AtomicStrings equivalent to this one */ uint refcount() const; /** * If called first thing in the app, this makes sure that AtomicString optimizes * string usage for the main thread. * @return true if this thread is considered the "main thread". */ static bool isMainThread(); private: #if __GNUC__ >= 3 struct SuperFastHash; struct equal; typedef __gnu_cxx::hash_set set_type; #else struct less { bool operator()( const QString *a, const QString *b ) const { return *a < *b; } }; typedef std::set set_type; #endif class Data; friend class Data; void ref( Data* ); void deref( Data* ); static void checkLazyDeletes(); Data *m_string; // static data static set_type s_store; // main string store static QPtrList s_lazyDeletes; // strings scheduled for deletion // by main thread static QMutex s_storeMutex; // protects the static data above }; #endif diff --git a/src/playlist.cpp b/src/playlist.cpp index 626d51629b..5d079b0c48 100644 --- a/src/playlist.cpp +++ b/src/playlist.cpp @@ -1,4933 +1,4914 @@ /* Copyright 2002-2004 Mark Kretschmann, Max Howell, Christian Muehlhaeuser * Copyright 2005-2006 Seb Ruiz, Mike Diehl, Ian Monroe, Gábor Lehel, Alexandre Pereira de Oliveira * Licensed as described in the COPYING file found in the root of this distribution * Maintainer: Max Howell * NOTES * * The PlaylistWindow handles some Playlist events. Thanks! * This class has a QOBJECT but it's private so you can only connect via PlaylistWindow::PlaylistWindow * Mostly it's sensible to implement playlist functionality in this class * TODO Obtaining information about the playlist is currently hard, we need the playlist to be globally * available and have some more useful public functions */ #define DEBUG_PREFIX "Playlist" #include #include "amarok.h" #include "amarokconfig.h" #include "app.h" #include "debug.h" #include "collectiondb.h" #include "collectionbrowser.h" #include "columnlist.h" #include "deletedialog.h" #include "enginecontroller.h" #include "expression.h" #include "k3bexporter.h" #include "metabundle.h" #include "mountpointmanager.h" #include "osd.h" #include "playerwindow.h" #include "playlistitem.h" #include "playlistbrowser.h" #include "playlistbrowseritem.h" //for stream editor dialog #include "playlistloader.h" #include "playlistselection.h" #include "queuemanager.h" #include "prettypopupmenu.h" #include "scriptmanager.h" #include "sliderwidget.h" #include "starmanager.h" #include "statusbar.h" //for status messages #include "tagdialog.h" #include "threadmanager.h" #include "xspfplaylist.h" #include //for pow() in playNextTrack() #include //copyToClipboard(), slotMouseButtonPressed() #include #include #include //undo system #include //eventFilter() #include //showUsageMessage() #include #include //slotGlowTimer() #include //toolTipText() #include #include #include #include //addHybridTracks() #include //playNextTrack() #include #include #include #include //setOverrideCursor() #include #include //rename() #include #include //slotShowContextMenu() #include //deleteSelectedFiles() #include //setCurrentTrack() #include #include #include #include //random Mode #include //KGlobal::dirs() #include #include //::showContextMenu() #include extern "C" { #if KDE_VERSION < KDE_MAKE_VERSION(3,3,91) #include //ControlMask in contentsDragMoveEvent() #endif } #include "playlist.h" namespace Amarok { const DynamicMode *dynamicMode() { return Playlist::instance() ? Playlist::instance()->dynamicMode() : 0; } } typedef PlaylistIterator MyIt; ////////////////////////////////////////////////////////////////////////////////////////// /// CLASS TagWriter : Threaded tag-updating ////////////////////////////////////////////////////////////////////////////////////////// class TagWriter : public ThreadManager::Job { //TODO make this do all tags at once when you split playlist.cpp up public: TagWriter( PlaylistItem*, const QString &oldTag, const QString &newTag, const int, const bool updateView = true ); ~TagWriter(); bool doJob(); void completeJob(); private: PlaylistItem* const m_item; bool m_failed; QString m_oldTagString; QString m_newTagString; int m_tagType; bool m_updateView; }; ////////////////////////////////////////////////////////////////////////////////////////// /// Glow ////////////////////////////////////////////////////////////////////////////////////////// namespace Glow { namespace Text { static float dr, dg, db; static int r, g, b; } namespace Base { static float dr, dg, db; static int r, g, b; } static const uint STEPS = 13; static uint counter; static QTimer timer; inline void startTimer() { counter = 0; timer.start( 40 ); } inline void reset() { counter = 0; timer.stop(); } } ////////////////////////////////////////////////////////////////////////////////////////// /// CLASS Playlist ////////////////////////////////////////////////////////////////////////////////////////// Playlist *Playlist::s_instance = 0; Playlist::Playlist( QWidget *parent ) : KListView( parent, "ThePlaylist" ) , EngineObserver( EngineController::instance() ) , m_startupTime_t( QDateTime::currentDateTime().toTime_t() ) , m_oldestTime_t( CollectionDB::instance()->query( "SELECT MIN( createdate ) FROM statistics;" ).first().toInt() ) , m_currentTrack( 0 ) , m_marker( 0 ) , m_hoveredRating( 0 ) , m_firstColumn( 0 ) , m_totalCount( 0 ) , m_totalLength( 0 ) , m_selCount( 0 ) , m_selLength( 0 ) , m_visCount( 0 ) , m_visLength( 0 ) , m_total( 0 ) , m_itemCountDirty( false ) , m_undoButton( 0 ) , m_redoButton( 0 ) , m_clearButton( 0 ) , m_undoDir( Amarok::saveLocation( "undo/" ) ) , m_undoCounter( 0 ) , m_dynamicMode( 0 ) , m_stopAfterTrack( 0 ) , m_stopAfterMode( DoNotStop ) , m_showHelp( true ) , m_dynamicDirt( false ) , m_queueDirt( false ) , m_undoDirt( false ) , m_itemToReallyCenter( 0 ) , m_renameItem( 0 ) , m_lockStack( 0 ) , m_columnFraction( PlaylistItem::NUM_COLUMNS, 0 ) , m_oldRandom( 0 ) , m_oldRepeat( 0 ) , m_playlistName( i18n( "Untitled" ) ) , m_proposeOverwriting( false ) + , m_urlIndex( &PlaylistItem::url ) + { s_instance = this; connect( CollectionDB::instance(), SIGNAL(fileMoved(const QString&, const QString&, const QString&)), SLOT(updateEntriesUrl(const QString&, const QString&, const QString&)) ); connect( CollectionDB::instance(), SIGNAL(uniqueIdChanged(const QString&, const QString&, const QString&)), SLOT(updateEntriesUniqueId(const QString&, const QString&, const QString&)) ); connect( CollectionDB::instance(), SIGNAL(fileDeleted(const QString&, const QString&)), SLOT(updateEntriesStatusDeleted(const QString&, const QString&)) ); connect( CollectionDB::instance(), SIGNAL(fileAdded(const QString&, const QString&)), SLOT(updateEntriesStatusAdded(const QString&, const QString&)) ); connect( CollectionDB::instance(), SIGNAL(filesAdded(const QMap&)), SLOT(updateEntriesStatusAdded(const QMap&)) ); initStarPixmaps(); EngineController* const ec = EngineController::instance(); connect( ec, SIGNAL(orderPrevious()), SLOT(playPrevTrack()) ); connect( ec, SIGNAL(orderNext( const bool )), SLOT(playNextTrack( const bool )) ); connect( ec, SIGNAL(orderCurrent()), SLOT(playCurrentTrack()) ); connect( this, SIGNAL( itemCountChanged( int, int, int, int, int, int ) ), ec, SLOT( playlistChanged() ) ); setShowSortIndicator( true ); setDropVisualizer( false ); //we handle the drawing for ourselves setDropVisualizerWidth( 3 ); // FIXME: This doesn't work, and steals focus when an item is clicked twice. //setItemsRenameable( true ); setAcceptDrops( true ); setSelectionMode( QListView::Extended ); setAllColumnsShowFocus( true ); //setItemMargin( 1 ); //aesthetics setMouseTracking( true ); #if KDE_IS_VERSION( 3, 3, 91 ) setShadeSortColumn( true ); #endif for( int i = 0; i < MetaBundle::NUM_COLUMNS; ++i ) { addColumn( PlaylistItem::prettyColumnName( i ), 0 ); switch( i ) { case PlaylistItem::Title: case PlaylistItem::Artist: case PlaylistItem::Composer: case PlaylistItem::Year: case PlaylistItem::Album: case PlaylistItem::DiscNumber: case PlaylistItem::Track: case PlaylistItem::Bpm: case PlaylistItem::Genre: case PlaylistItem::Comment: case PlaylistItem::Score: case PlaylistItem::Rating: setRenameable( i, true ); continue; default: setRenameable( i, false ); } } setColumnWidth( PlaylistItem::Title, 200 ); setColumnWidth( PlaylistItem::Artist, 100 ); setColumnWidth( PlaylistItem::Album, 100 ); setColumnWidth( PlaylistItem::Length, 80 ); if( AmarokConfig::showMoodbar() ) setColumnWidth( PlaylistItem::Mood, 120 ); if( AmarokConfig::useRatings() ) setColumnWidth( PlaylistItem::Rating, PlaylistItem::ratingColumnWidth() ); setColumnAlignment( PlaylistItem::Length, Qt::AlignRight ); setColumnAlignment( PlaylistItem::Track, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::DiscNumber, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::Bpm, Qt::AlignRight ); setColumnAlignment( PlaylistItem::Year, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::Bitrate, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::SampleRate, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::Filesize, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::Score, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::Type, Qt::AlignCenter ); setColumnAlignment( PlaylistItem::PlayCount, Qt::AlignCenter ); connect( this, SIGNAL( doubleClicked( QListViewItem* ) ), this, SLOT( doubleClicked( QListViewItem* ) ) ); connect( this, SIGNAL( returnPressed( QListViewItem* ) ), this, SLOT( activate( QListViewItem* ) ) ); connect( this, SIGNAL( mouseButtonPressed( int, QListViewItem*, const QPoint&, int ) ), this, SLOT( slotMouseButtonPressed( int, QListViewItem*, const QPoint&, int ) ) ); connect( this, SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ), this, SLOT( slotQueueChanged( const PLItemList &, const PLItemList & ) ) ); connect( this, SIGNAL( itemRenamed( QListViewItem*, const QString&, int ) ), this, SLOT( writeTag( QListViewItem*, const QString&, int ) ) ); connect( this, SIGNAL( aboutToClear() ), this, SLOT( saveUndoState() ) ); connect( CollectionDB::instance(), SIGNAL( scoreChanged( const QString&, float ) ), this, SLOT( scoreChanged( const QString&, float ) ) ); connect( CollectionDB::instance(), SIGNAL( ratingChanged( const QString&, int ) ), this, SLOT( ratingChanged( const QString&, int ) ) ); connect( CollectionDB::instance(), SIGNAL( fileMoved( const QString&, const QString& ) ), this, SLOT( fileMoved( const QString&, const QString& ) ) ); connect( header(), SIGNAL( indexChange( int, int, int ) ), this, SLOT( columnOrderChanged() ) ), connect( &Glow::timer, SIGNAL(timeout()), SLOT(slotGlowTimer()) ); KActionCollection* const ac = Amarok::actionCollection(); KAction *copy = KStdAction::copy( this, SLOT( copyToClipboard() ), ac, "playlist_copy" ); KStdAction::selectAll( this, SLOT( selectAll() ), ac, "playlist_select_all" ); m_clearButton = new KAction( i18n( "clear playlist", "&Clear" ), Amarok::icon( "playlist_clear" ), 0, this, SLOT( clear() ), ac, "playlist_clear" ); m_undoButton = KStdAction::undo( this, SLOT( undo() ), ac, "playlist_undo" ); m_redoButton = KStdAction::redo( this, SLOT( redo() ), ac, "playlist_redo" ); m_undoButton ->setIcon( Amarok::icon( "undo" ) ); m_redoButton ->setIcon( Amarok::icon( "redo" ) ); new KAction( i18n( "&Repopulate" ), Amarok::icon( "playlist_refresh" ), 0, this, SLOT( repopulate() ), ac, "repopulate" ); new KAction( i18n( "S&huffle" ), "rebuild", CTRL+Key_H, this, SLOT( shuffle() ), ac, "playlist_shuffle" ); KAction *gotoCurrent = new KAction( i18n( "&Go To Current Track" ), Amarok::icon( "music" ), CTRL+Key_J, this, SLOT( showCurrentTrack() ), ac, "playlist_show" ); new KAction( i18n( "&Remove Duplicate && Dead Entries" ), 0, this, SLOT( removeDuplicates() ), ac, "playlist_remove_duplicates" ); new KAction( i18n( "&Queue Selected Tracks" ), Amarok::icon( "queue_track" ), CTRL+Key_D, this, SLOT( queueSelected() ), ac, "queue_selected" ); KToggleAction *stopafter = new KToggleAction( i18n( "&Stop Playing After Track" ), Amarok::icon( "stop" ), CTRL+ALT+Key_V, this, SLOT( toggleStopAfterCurrentItem() ), ac, "stop_after" ); { // KAction idiocy -- shortcuts don't work until they've been plugged into a menu KPopupMenu asdf; copy->plug( &asdf ); stopafter->plug( &asdf ); gotoCurrent->plug( &asdf ); copy->unplug( &asdf ); stopafter->unplug( &asdf ); gotoCurrent->unplug( &asdf ); } //ensure we update action enabled states when repeat Playlist is toggled connect( ac->action( "repeat" ), SIGNAL(activated( int )), SLOT(updateNextPrev()) ); connect( ac->action( "repeat" ), SIGNAL( activated( int ) ), SLOT( generateInfo() ) ); connect( ac->action( "favor_tracks" ), SIGNAL( activated( int ) ), SLOT( generateInfo() ) ); connect( ac->action( "random_mode" ), SIGNAL( activated( int ) ), SLOT( generateInfo() ) ); // undostates are written in chronological order, so this is a clever way to get them back in the correct order :) QStringList undos = m_undoDir.entryList( QString("*.xml"), QDir::Files, QDir::Time ); foreach( undos ) m_undoList.append( m_undoDir.absPath() + '/' + (*it) ); m_undoCounter = m_undoList.count(); m_undoButton->setEnabled( !m_undoList.isEmpty() ); m_redoButton->setEnabled( false ); engineStateChanged( EngineController::engine()->state() ); //initialise state of UI paletteChange( palette() ); //sets up glowColors restoreLayout( KGlobal::config(), "PlaylistColumnsLayout" ); // Sorting must be disabled when current.xml is being loaded. See BUG 113042 KListView::setSorting( NO_SORT ); //use base so we don't saveUndoState() too setDynamicMode( 0 ); m_smartResizing = Amarok::config( "PlaylistWindow" )->readBoolEntry( "Smart Resizing", true ); columnOrderChanged(); //cause the column fractions to be updated, but in a safe way, ie no specific column columnResizeEvent( header()->count(), 0, 0 ); //do after you resize all the columns connect( header(), SIGNAL(sizeChange( int, int, int )), SLOT(columnResizeEvent( int, int, int )) ); connect( this, SIGNAL( contentsMoving( int, int ) ), SLOT( slotContentsMoving() ) ); connect( App::instance(), SIGNAL( useScores( bool ) ), this, SLOT( slotUseScores( bool ) ) ); connect( App::instance(), SIGNAL( useRatings( bool ) ), this, SLOT( slotUseRatings( bool ) ) ); connect( App::instance(), SIGNAL( moodbarPrefs( bool, bool, int, bool ) ), this, SLOT( slotMoodbarPrefs( bool, bool, int, bool ) ) ); Amarok::ToolTip::add( this, viewport() ); header()->installEventFilter( this ); renameLineEdit()->installEventFilter( this ); setTabOrderedRenaming( false ); m_filtertimer = new QTimer( this ); connect( m_filtertimer, SIGNAL(timeout()), this, SLOT(setDelayedFilter()) ); connect( MountPointManager::instance(), SIGNAL(mediumConnected( int )), SLOT(mediumChange( int )) ); connect( MountPointManager::instance(), SIGNAL(mediumRemoved( int )), SLOT(mediumChange( int )) ); m_clicktimer = new QTimer( this ); connect( m_clicktimer, SIGNAL(timeout()), this, SLOT(slotSingleClick()) ); } Playlist::~Playlist() { saveLayout( KGlobal::config(), "PlaylistColumnsLayout" ); if( AmarokConfig::savePlaylist() && m_lockStack == 0 ) saveXML( defaultPlaylistPath() ); //speed up quit a little safeClear(); //our implementation is slow Amarok::ToolTip::remove( viewport() ); blockSignals( true ); //might help s_instance = 0; } //////////////////////////////////////////////////////////////////////////////// /// Media Handling //////////////////////////////////////////////////////////////////////////////// void Playlist::mediumChange( int deviceid ) // SLOT { Q_UNUSED( deviceid ); for( QListViewItem *it = firstChild(); it; it = it->nextSibling() ) { PlaylistItem *p = dynamic_cast( it ); if( p ) { bool exist = p->exists(); if( exist != p->checkExists() ) { p->setFilestatusEnabled( p->checkExists() ); p->update(); } } } } void -Playlist::insertMedia( KURL::List list, int options ) +Playlist::insertMedia( const KURL::List &list, int options ) { if( list.isEmpty() ) { Amarok::StatusBar::instance()->shortMessage( i18n("Attempted to insert nothing into playlist.") ); return; // don't add empty items } const bool isPlaying = EngineController::engine()->state() == Engine::Playing; if( isPlaying ) options &= ~Playlist::StartPlay; bool directPlay = options & (Playlist::DirectPlay | Playlist::StartPlay); if( options & Replace ) clear(); else options |= Playlist::Colorize; PlaylistItem *after = lastItem(); - if( options & Queue ) - { - KURL::List addMe = list; - KURL::List::Iterator jt; - - // add any songs not in the playlist to it. - for( MyIt it( this, MyIt::All ); *it; ++it ) { - jt = addMe.find( (*it)->url() ); + KURL::List addMe; + QPtrList alreadyHave; - if ( jt != addMe.end() ) { - addMe.remove( jt ); //don't want to add a track which is already present in the playlist - } - } + // Filter out duplicates + foreachType( KURL::List, list ) { + PlaylistItem *item = m_urlIndex.getFirst( *it ); + if ( item ) + alreadyHave.append( item ); + else + addMe.append( *it ); + } + if( options & Queue ) + { if ( addMe.isEmpty() ) // all songs to be queued are already in the playlist { - // find the songs and queue them. - for (MyIt it( this, MyIt::All ); *it; ++it ) { - jt = list.find( (*it)->url() ); - - if ( jt != list.end() ) - { - queue( *it, false, false ); - list.remove( jt ); - } - } + // queue all the songs + foreachType( QPtrList, alreadyHave ) + queue( *it, false, false ); + return; } else { // We add the track after the last track on queue, or after current if the queue is empty after = m_nextTracks.isEmpty() ? currentTrack() : m_nextTracks.getLast(); // If there's no tracks on the queue, and there's no current track, fall back to the last item if ( !after ) after = lastItem(); - - insertMediaInternal( addMe, after, options ); } - return; - } else if( options & Unique ) { - //passing by value is quick for QValueLists, though it is slow - //if we change the list, but this is unlikely - KURL::List::Iterator jt; - int alreadyOnPlaylist = 0; - for( MyIt it( this, MyIt::All ); *it; ++it ) { - jt = list.find( (*it)->url() ); - - if ( jt != list.end() ) { - if ( directPlay && jt == list.begin() ) { - directPlay = false; - activate( *it ); - } - - list.remove( jt ); - alreadyOnPlaylist++; - } - } + int alreadyOnPlaylist = alreadyHave.count(); if ( alreadyOnPlaylist ) - Amarok::StatusBar::instance()->shortMessage( i18n("One track was already in the playlist, so it was not added.", "%n tracks were already in the playlist, so they were not added.", alreadyOnPlaylist ) ); + { + if (directPlay) activate( alreadyHave.getFirst() ); + Amarok::StatusBar::instance()->shortMessage( + i18n("One track was already in the playlist, so it was not added.", + "%n tracks were already in the playlist, so they were not added.", + alreadyOnPlaylist ) ); + } } - insertMediaInternal( list, after, options ); + insertMediaInternal( addMe, after, options ); } void Playlist::insertMediaInternal( const KURL::List &list, PlaylistItem *after, int options ) { if ( !list.isEmpty() ) { setSorting( NO_SORT ); // prevent association with something that is about to be deleted // TODO improve the playlist with a list of items that are volatile or something while( after && after->url().isEmpty() ) after = static_cast( after->itemAbove() ); ThreadManager::instance()->queueJob( new UrlLoader( list, after, options ) ); } } void Playlist::insertMediaSql( const QString& sql, int options ) { const bool isPlaying = EngineController::engine()->state() == Engine::Playing; if( isPlaying ) options &= ~Playlist::StartPlay; // TODO Implement more options PlaylistItem *after = 0; if ( options & Replace ) clear(); if ( options & Append ) after = lastItem(); setSorting( NO_SORT ); ThreadManager::instance()->queueJob( new SqlLoader( sql, after, options ) ); } void Playlist::addDynamicModeTracks( uint songCount ) { if( songCount < 1 ) return; int currentPos = 0; for( MyIt it( this, MyIt::Visible ); *it; ++it ) { if( m_currentTrack && *it == m_currentTrack ) break; else if( !m_currentTrack && (*it)->isDynamicEnabled() ) break; ++currentPos; } currentPos++; int required = currentPos + dynamicMode()->upcomingCount(); // currentPos handles currentTrack int remainder = totalTrackCount(); if( required > remainder ) songCount = required - remainder; DynamicMode *m = modifyDynamicMode(); KURL::List tracksToInsert = m->retrieveTracks( songCount ); Playlist::instance()->finishedModifying( m ); insertMedia( tracksToInsert, Playlist::Unique ); } /** * @param songCount : Number of tracks to be shown after the current track */ void Playlist::adjustDynamicUpcoming( bool saveUndo ) { /** * If m_currentTrack exists, we iterate until we find it * Else, we iterate until we find an item which is enabled **/ MyIt it( this, MyIt::Visible ); //Notice we'll use this up to the end of the function! //Skip previously played for( ; *it; ++it ) { if( m_currentTrack && *it == m_currentTrack ) break; else if( !m_currentTrack && (*it)->isDynamicEnabled() ) break; } //Skip current if( m_currentTrack ) ++it; int x = 0; for ( ; *it && x < dynamicMode()->upcomingCount() ; ++it, ++x ); if ( x < dynamicMode()->upcomingCount() ) { addDynamicModeTracks( dynamicMode()->upcomingCount() - x ); } if( saveUndo ) saveUndoState(); } /** * @param songCount : Number of tracks to be shown before the current track */ void Playlist::adjustDynamicPrevious( uint songCount, bool saveUndo ) { int current = currentTrackIndex(); int x = current - songCount; QPtrList list; int y=0; for( QListViewItemIterator it( firstChild() ); y < x ; list.prepend( *it ), ++it, y++ ); if( list.isEmpty() ) return; if ( saveUndo ) saveUndoState(); //remove the items for( QListViewItem *item = list.first(); item; item = list.next() ) { removeItem( static_cast( item ) ); delete item; } } void Playlist::setDynamicHistory( bool enable /*false*/ ) { if( !m_currentTrack ) return; for( PlaylistIterator it( this, PlaylistIterator::All ) ; *it ; ++it ) { if( *it == m_currentTrack ) break; //avoid repainting if we can. if( (*it)->isDynamicEnabled() == enable ) { (*it)->setDynamicEnabled( !enable ); (*it)->update(); } } } QString Playlist::defaultPlaylistPath() //static { return Amarok::saveLocation() + "current.xml"; } void Playlist::restoreSession() { KURL url; if ( Amarok::config()->readBoolEntry( "First 1.4 Run", true ) ) { // On first startup of 1.4, we load a special playlist with an intro track url.setPath( locate( "data", "amarok/data/firstrun.m3u" ) ); Amarok::config()->writeEntry( "First 1.4 Run", false ); } else url.setPath( Amarok::saveLocation() + "current.xml" ); // check it exists, because on the first ever run it doesn't and // it looks bad to show "some URLs were not suitable.." on the // first ever-run if( QFile::exists( url.path() ) ) { ThreadManager::instance()->queueJob( new UrlLoader( url, 0, 0 ) ); } } /* The following two functions (saveLayout(), restoreLayout()), taken from klistview.cpp, are largely Copyright (C) 2000 Reginald Stadlbauer Copyright (C) 2000,2003 Charles Samuels Copyright (C) 2000 Peter Putzer */ void Playlist::saveLayout(KConfig *config, const QString &group) const { KConfigGroupSaver saver(config, group); QStringList names, widths, order; const int colCount = columns(); QHeader* const thisHeader = header(); for (int i = 0; i < colCount; ++i) { names << PlaylistItem::exactColumnName(i); widths << QString::number(columnWidth(i)); order << QString::number(thisHeader->mapToIndex(i)); } config->writeEntry("ColumnsVersion", 1); config->writeEntry("ColumnNames", names); config->writeEntry("ColumnWidths", widths); config->writeEntry("ColumnOrder", order); config->writeEntry("SortColumn", columnSorted()); config->writeEntry("SortAscending", ascendingSort()); } void Playlist::restoreLayout(KConfig *config, const QString &group) { KConfigGroupSaver saver(config, group); int version = config->readNumEntry("ColumnsVersion", 0); QValueList iorder; //internal ordering if( version ) { QStringList names = config->readListEntry("ColumnNames"); for( int i = 0, n = names.count(); i < n; ++i ) { bool found = false; for( int ii = i; ii < PlaylistItem::NUM_COLUMNS; ++ii ) //most likely, it's where we left it { if( names[i] == PlaylistItem::exactColumnName(ii) ) { iorder.append(ii); found = true; break; } } if( !found ) { for( int ii = 0; ii < i; ++ii ) //but maybe it's not if( names[i] == PlaylistItem::exactColumnName(ii) ) { iorder.append(ii); found = true; break; } } if( !found ) return; //oops? -- revert to the default. } } else { int oldorder[] = { 0, 1, 2, 5, 4, 9, 8, 7, 10, 12, 13, 15, 16, 11, 17, 18, 19, 3, 6, 20 }; for( int i = 0; i != 20; ++i ) iorder.append(oldorder[i]); } QStringList cols = config->readListEntry("ColumnWidths"); int i = 0; { // scope the iterators QStringList::ConstIterator it = cols.constBegin(); const QStringList::ConstIterator itEnd = cols.constEnd(); for (; it != itEnd; ++it) setColumnWidth(iorder[i++], (*it).toInt()); } // move sections in the correct sequence: from lowest to highest index position // otherwise we move a section from an index, which modifies // all index numbers to the right of the moved one cols = config->readListEntry("ColumnOrder"); const int colCount = columns(); for (i = 0; i < colCount; ++i) // final index positions from lowest to highest { QStringList::ConstIterator it = cols.constBegin(); const QStringList::ConstIterator itEnd = cols.constEnd(); int section = 0; for (; (it != itEnd) && (iorder[(*it).toInt()] != i); ++it, ++section) ; if ( it != itEnd ) { // found the section to move to position i header()->moveSection(iorder[section], i); } } if ( config->hasKey("SortColumn") ) { const int sort = config->readNumEntry("SortColumn"); if( sort >= 0 && uint(sort) < iorder.count() ) setSorting(iorder[config->readNumEntry("SortColumn")], config->readBoolEntry("SortAscending", true)); } if( !AmarokConfig::useScores() ) hideColumn( PlaylistItem::Score ); if( !AmarokConfig::useRatings() ) hideColumn( PlaylistItem::Rating ); if( !AmarokConfig::showMoodbar() ) hideColumn( PlaylistItem::Mood ); } void Playlist::addToUniqueMap( const QString uniqueid, PlaylistItem* item ) { QPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) list = m_uniqueMap[uniqueid]; else list = new QPtrList(); list->append( item ); if( !m_uniqueMap.contains( uniqueid ) ) m_uniqueMap[uniqueid] = list; } void Playlist::removeFromUniqueMap( const QString uniqueid, PlaylistItem* item ) { if( !m_uniqueMap.contains( uniqueid ) ) return; QPtrList *list; list = m_uniqueMap[uniqueid]; list->remove( item ); //don't care about return value if( list->isEmpty() ) { delete list; m_uniqueMap.remove( uniqueid ); } } void Playlist::updateEntriesUrl( const QString &oldUrl, const QString &newUrl, const QString &uniqueid ) { // Make sure the MoodServer gets this signal first! MoodServer::instance()->slotFileMoved( oldUrl, newUrl ); QPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) { list = m_uniqueMap[uniqueid]; PlaylistItem *item; for( item = list->first(); item; item = list->next() ) { item->setUrl( KURL( newUrl ) ); item->setFilestatusEnabled( item->checkExists() ); } } } void Playlist::updateEntriesUniqueId( const QString &/*url*/, const QString &oldid, const QString &newid ) { QPtrList *list, *oldlist; if( m_uniqueMap.contains( oldid ) ) { list = m_uniqueMap[oldid]; m_uniqueMap.remove( oldid ); PlaylistItem *item; for( item = list->first(); item; item = list->next() ) { item->setUniqueId( newid ); item->readTags(); } if( !m_uniqueMap.contains( newid ) ) m_uniqueMap[newid] = list; else { oldlist = m_uniqueMap[newid]; for( item = list->first(); item; item = list->next() ) oldlist->append( item ); delete list; } } } void Playlist::updateEntriesStatusDeleted( const QString &/*absPath*/, const QString &uniqueid ) { QPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) { list = m_uniqueMap[uniqueid]; PlaylistItem *item; for( item = list->first(); item; item = list->next() ) item->setFilestatusEnabled( false ); } } void Playlist::updateEntriesStatusAdded( const QString &absPath, const QString &uniqueid ) { QPtrList *list; if( m_uniqueMap.contains( uniqueid ) ) { list = m_uniqueMap[uniqueid]; PlaylistItem *item; for( item = list->first(); item; item = list->next() ) { if( absPath != item->url().path() ) item->setPath( absPath ); //in case the UID was the same, but the path has changed item->setFilestatusEnabled( true ); } } } void Playlist::updateEntriesStatusAdded( const QMap &map ) { QMap*> uniquecopy( m_uniqueMap ); QMap*>::Iterator it; for( it = uniquecopy.begin(); it != uniquecopy.end(); ++it ) { if( map.contains( it.key() )) { updateEntriesStatusAdded( map[it.key()], it.key() ); uniquecopy.remove( it ); } } for( it = uniquecopy.begin(); it != uniquecopy.end(); ++it ) updateEntriesStatusDeleted( QString::null, it.key() ); } //////////////////////////////////////////////////////////////////////////////// /// Current Track Handling //////////////////////////////////////////////////////////////////////////////// void Playlist::playNextTrack( bool forceNext ) { PlaylistItem *item = currentTrack(); if( !m_visCount || stopAfterMode() == StopAfterCurrent ) { if( dynamicMode() && m_visCount ) { item->setDynamicEnabled( false ); advanceDynamicTrack(); m_dynamicDirt = false; } setStopAfterMode(DoNotStop); EngineController::instance()->stop(); if( !AmarokConfig::randomMode() ) { item = MyIt::nextVisible( item ); while( item && ( !checkFileStatus( item ) || !item->exists() ) ) item = MyIt::nextVisible( item ); setCurrentTrack( item ); } return; } if( !Amarok::repeatTrack() || forceNext ) { if( !m_nextTracks.isEmpty() ) { item = m_nextTracks.first(); m_nextTracks.remove(); if ( dynamicMode() ) // move queued track to the top of the playlist, to prevent it from being played twice // this is done automatically by most queue changing functions, but not if the user manually moves the track moveItem( item, 0, m_currentTrack ); emit queueChanged( PLItemList(), PLItemList( item ) ); } else if( Amarok::entireAlbums() && m_currentTrack && m_currentTrack->nextInAlbum() ) item = m_currentTrack->nextInAlbum(); else if( Amarok::repeatAlbum() && repeatAlbumTrackCount() && ( repeatAlbumTrackCount() > 1 || !forceNext ) ) item = m_currentTrack->m_album->tracks.getFirst(); else if( AmarokConfig::randomMode() ) { QValueVector tracks; //make a list of everything we can play if( Amarok::randomAlbums() ) // add the first visible track from every unplayed album { for( ArtistAlbumMap::const_iterator it = m_albums.constBegin(), end = m_albums.constEnd(); it != end; ++it ) for( AlbumMap::const_iterator it2 = (*it).constBegin(), end2 = (*it).constEnd(); it2 != end2; ++it2 ) if( m_prevAlbums.findRef( *it2 ) == -1 ) { if ( (*it2)->tracks.getFirst() ) tracks.append( (*it2)->tracks.getFirst() ); } } else for( MyIt it( this ); *it; ++it ) if ( !m_prevTracks.containsRef( *it ) ) tracks.push_back( *it ); if( tracks.isEmpty() ) { //we have played everything item = 0; if( Amarok::randomAlbums() ) { if ( m_prevAlbums.count() <= 8 ) { m_prevAlbums.first(); while( m_prevAlbums.count() ) removeFromPreviousAlbums(); if( m_currentTrack ) { // don't add it to previous albums if we only have one album in the playlist // would loop infinitely otherwise QPtrList albums; for( PlaylistIterator it( this, PlaylistIterator::Visible ); *it && albums.count() <= 1; ++it ) if( albums.findRef( (*it)->m_album ) == -1 ) albums.append( (*it)->m_album ); if ( albums.count() > 1 ) appendToPreviousAlbums( m_currentTrack->m_album ); } } else { m_prevAlbums.first(); //set's current item to first item //keep 80 tracks in the previous list so item time user pushes play //we don't risk playing anything too recent while( m_prevAlbums.count() > 8 ) removeFromPreviousAlbums(); //removes current item } } else { if ( m_prevTracks.count() <= 80 ) { m_prevTracks.first(); while( m_prevTracks.count() ) removeFromPreviousTracks(); if( m_currentTrack ) { // don't add it to previous tracks if we only have one file in the playlist // would loop infinitely otherwise int count = 0; for( PlaylistIterator it( this, PlaylistIterator::Visible ); *it && count <= 1; ++it ) ++count; if ( count > 1 ) appendToPreviousTracks( m_currentTrack ); } } else { m_prevTracks.first(); //set's current item to first item //keep 80 tracks in the previous list so item time user pushes play //we don't risk playing anything too recent while( m_prevTracks.count() > 80 ) removeFromPreviousTracks(); //removes current item } } if( Amarok::repeatPlaylist() ) { playNextTrack(); return; } //else we stop via activate( 0 ) below } else { if( Amarok::favorNone() ) item = tracks.at( KApplication::random() % tracks.count() ); //is O(1) else { const uint currenttime_t = QDateTime::currentDateTime().toTime_t(); QValueVector weights( tracks.size() ); Q_INT64 total = m_total; if( Amarok::randomAlbums() ) { for( int i = 0, n = tracks.count(); i < n; ++i ) { weights[i] = tracks.at( i )->m_album->total; if( Amarok::favorLastPlay() ) { const int inc = int( float( ( currenttime_t - m_startupTime_t ) * tracks.at( i )->m_album->tracks.count() + 0.5 ) / tracks.at( i )->m_album->tracks.count() ); weights[i] += inc; total += inc; } } } else { for( int i = 0, n = tracks.count(); i < n; ++i ) { weights[i] = tracks.at( i )->totalIncrementAmount(); if( Amarok::favorLastPlay() ) weights[i] += currenttime_t - m_startupTime_t; } if( Amarok::favorLastPlay() ) total += ( currenttime_t - m_startupTime_t ) * weights.count(); } Q_INT64 random; if( Amarok::favorLastPlay() ) //really big huge numbers { Q_INT64 r = Q_INT64( ( KApplication::random() / pow( 2, sizeof( int ) * 8 ) ) * pow( 2, 64 ) ); random = r % total; } else random = KApplication::random() % total; int i = 0; for( int n = tracks.count(); i < n && random >= 0; ++i ) random -= weights.at( i ); item = tracks.at( i-1 ); } } } else if( item ) { item = MyIt::nextVisible( item ); while( item && ( !checkFileStatus( item ) || !item->exists() ) ) item = MyIt::nextVisible( item ); } else { item = *MyIt( this ); //ie. first visible item while( item && ( !checkFileStatus( item ) || !item->exists() ) ) item = item->nextSibling(); } if ( dynamicMode() && item != firstChild() ) { if( currentTrack() ) currentTrack()->setDynamicEnabled( false ); advanceDynamicTrack(); } if ( !item && Amarok::repeatPlaylist() ) item = *MyIt( this ); //ie. first visible item } if ( EngineController::engine()->loaded() ) activate( item ); else setCurrentTrack( item ); } //This is called before setCurrentItem( item ); void Playlist::advanceDynamicTrack() { int x = currentTrackIndex(); if( dynamicMode()->cycleTracks() ) { if( x >= dynamicMode()->previousCount() ) { PlaylistItem *first = firstChild(); removeItem( first ); delete first; } } const int upcomingTracks = childCount() - x - 1; // Just starting to play from stopped, don't append something needlessely // or, we have more than enough items in the queue. bool dontAppend = ( EngineController::instance()->engine()->state() == Engine::Empty ) || upcomingTracks > dynamicMode()->upcomingCount(); //keep upcomingTracks requirement, this seems to break StopAfterCurrent if( !dontAppend && stopAfterMode() != StopAfterCurrent ) { addDynamicModeTracks( 1 ); } m_dynamicDirt = true; } void Playlist::playPrevTrack() { PlaylistItem *item = currentTrack(); if( Amarok::entireAlbums() ) { item = 0; if( m_currentTrack ) { item = m_currentTrack->prevInAlbum(); if( !item && Amarok::repeatAlbum() && m_currentTrack->m_album->tracks.count() ) item = m_currentTrack->m_album->tracks.getLast(); } if( !item ) { PlaylistAlbum* a = m_prevAlbums.last(); while( a && !a->tracks.count() ) { removeFromPreviousAlbums(); a = m_prevAlbums.last(); } if( a ) { item = a->tracks.getLast(); removeFromPreviousAlbums(); } } if( !item ) { item = *static_cast(--MyIt( item )); while( item && !checkFileStatus( item ) ) item = *static_cast(--MyIt( item )); } } else { if ( !AmarokConfig::randomMode() || m_prevTracks.count() <= 1 ) { if( item ) { item = MyIt::prevVisible( item ); while( item && ( !checkFileStatus( item ) || !item->isEnabled() ) ) item = MyIt::prevVisible( item ); } else { item = *MyIt( this ); //ie. first visible item while( item && ( !checkFileStatus( item ) || !item->isEnabled() ) ) item = item->nextSibling(); } } else { // if enough songs in buffer, jump to the previous one m_prevTracks.last(); removeFromPreviousTracks(); //remove the track playing now item = m_prevTracks.last(); // we need to remove this item now, since it will be added in activate() again removeFromPreviousTracks(); } } if ( !item && Amarok::repeatPlaylist() ) item = *MyIt( lastItem() ); //TODO check this works! if ( EngineController::engine()->loaded() ) activate( item ); else setCurrentTrack( item ); } void Playlist::playCurrentTrack() { if ( !currentTrack() ) playNextTrack( Amarok::repeatTrack() ); //we must do this even if the above is correct //since the engine is not loaded the first time the user presses play //then calling the next() function wont play it activate( currentTrack() ); } void Playlist::setSelectedRatings( int rating ) { if( !m_selCount && currentItem() && currentItem()->isVisible() ) CollectionDB::instance()->setSongRating( currentItem()->url().path(), rating, true ); else for( MyIt it( this, MyIt::Selected ); *it; ++it ) CollectionDB::instance()->setSongRating( (*it)->url().path(), rating, true ); } void Playlist::queueSelected() { PLItemList in, out; QPtrList dynamicList; for( MyIt it( this, MyIt::Selected ); *it; ++it ) { // Dequeuing selection with dynamic doesn't work due to the moving of the track after the last queued if( dynamicMode() ) { ( !m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); dynamicList.append( *it ); } else { queue( *it, true ); ( m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); } } if( dynamicMode() ) { QListViewItem *item = dynamicList.first(); if( m_nextTracks.containsRef( static_cast(item) ) ) { for( item = dynamicList.last(); item; item = dynamicList.prev() ) queue( item, true ); } else { for( ; item; item = dynamicList.next() ) queue( item, true ); } } emit queueChanged( in, out ); } void Playlist::queue( QListViewItem *item, bool multi, bool invertQueue ) { #define item static_cast(item) const int queueIndex = m_nextTracks.findRef( item ); const bool isQueued = queueIndex != -1; if( isQueued ) { if( invertQueue ) { //remove the item, this is better way than remove( item ) m_nextTracks.remove( queueIndex ); //sets current() to next item if( dynamicMode() ) // we move the item after the last queued item to preserve the ordered 'queue'. { PlaylistItem *after = m_nextTracks.last(); if( after ) moveItem( item, 0, after ); } } } else if( !dynamicMode() ) m_nextTracks.append( item ); else // Dynamic mode { PlaylistItem *after; m_nextTracks.isEmpty() ? after = m_currentTrack : after = m_nextTracks.last(); if( !after ) { after = firstChild(); while( after && !after->isDynamicEnabled() ) { if( after->nextSibling()->isDynamicEnabled() ) break; after = after->nextSibling(); } } if( item->isDynamicEnabled() && item != m_currentTrack ) { this->moveItem( item, 0, after ); m_nextTracks.append( item ); } else { /// we do the actual queuing through customEvent, since insertMedia is threaded m_queueDirt = true; insertMediaInternal( item->url(), after ); } } if( !multi ) { if( isQueued ) //no longer { if( invertQueue ) emit queueChanged( PLItemList(), PLItemList( item ) ); } else emit queueChanged( PLItemList( item ), PLItemList() ); } #undef item } void Playlist::sortQueuedItems() // used by dynamic mode { PlaylistItem *last = m_currentTrack; for( PlaylistItem *item = m_nextTracks.first(); item; item = m_nextTracks.next() ) { if( item->itemAbove() != last ) item->moveItem( last ); last = item; } } void Playlist::setStopAfterCurrent( bool on ) { PlaylistItem *prev_stopafter = m_stopAfterTrack; if( on ) { setStopAfterItem( m_currentTrack ); } else { setStopAfterMode( DoNotStop ); } if( m_stopAfterTrack ) m_stopAfterTrack->update(); if( prev_stopafter ) prev_stopafter->update(); } void Playlist::setStopAfterItem( PlaylistItem *item ) { if( !item ) { setStopAfterMode( DoNotStop ); return; } else if( item == m_currentTrack ) setStopAfterMode( StopAfterCurrent ); else if( item == m_nextTracks.getLast() ) setStopAfterMode( StopAfterQueue ); else setStopAfterMode( StopAfterQueue ); m_stopAfterTrack = item; } void Playlist::toggleStopAfterCurrentItem() { PlaylistItem *item = currentItem(); if( !item && m_selCount == 1 ) item = *MyIt( this, MyIt::Visible | MyIt::Selected ); if( !item ) return; PlaylistItem *prev_stopafter = m_stopAfterTrack; if( m_stopAfterTrack == item ) { m_stopAfterTrack = 0; setStopAfterMode( DoNotStop ); } else { setStopAfterItem( item ); item->setSelected( false ); item->update(); } if( prev_stopafter ) prev_stopafter->update(); } void Playlist::toggleStopAfterCurrentTrack() { PlaylistItem *item = currentTrack(); if( !item ) return; PlaylistItem *prev_stopafter = m_stopAfterTrack; if( m_stopAfterTrack == item ) { setStopAfterMode( DoNotStop ); Amarok::OSD::instance()->OSDWidget::show( i18n("Stop Playing After Track: Off") ); } else { setStopAfterItem( item ); item->setSelected( false ); item->update(); Amarok::OSD::instance()->OSDWidget::show( i18n("Stop Playing After Track: On") ); } if( prev_stopafter ) prev_stopafter->update(); } void Playlist::setStopAfterMode( int mode ) { PlaylistItem *prevStopAfter = m_stopAfterTrack; m_stopAfterMode = mode; switch( mode ) { case DoNotStop: m_stopAfterTrack = 0; break; case StopAfterCurrent: m_stopAfterTrack = m_currentTrack; break; case StopAfterQueue: m_stopAfterTrack = m_nextTracks.count() ? m_nextTracks.getLast() : m_currentTrack; break; } if( prevStopAfter ) prevStopAfter->update(); if( m_stopAfterTrack ) m_stopAfterTrack->update(); } int Playlist::stopAfterMode() { if ( m_stopAfterMode != DoNotStop && m_stopAfterTrack && m_stopAfterTrack == m_currentTrack ) { m_stopAfterMode = StopAfterCurrent; } return m_stopAfterMode; } void Playlist::generateInfo() { m_albums.clear(); if( Amarok::entireAlbums() ) for( MyIt it( this, MyIt::All ); *it; ++it ) (*it)->refAlbum(); m_total = 0; if( Amarok::entireAlbums() || AmarokConfig::favorTracks() ) for( MyIt it( this, MyIt::Visible ); *it; ++it ) (*it)->incrementTotals(); } void Playlist::doubleClicked( QListViewItem *item ) { /* We have to check if the item exists before calling activate, otherwise clicking on an empty playlist space would stop playing (check BR #105106)*/ if( item && m_hoveredRating != item ) activate( item ); } void Playlist::slotCountChanged() { if( m_itemCountDirty ) emit itemCountChanged( totalTrackCount(), m_totalLength, m_visCount, m_visLength, m_selCount, m_selLength ); m_itemCountDirty = false; } bool Playlist::checkFileStatus( PlaylistItem * item ) { //DEBUG_BLOCK //debug() << "uniqueid of item = " << item->uniqueId() << ", url = " << item->url().path() << endl; if( !item->checkExists() ) { //debug() << "not found, finding new url" << endl; QString path = QString::null; if( !item->uniqueId().isEmpty() ) { path = CollectionDB::instance()->urlFromUniqueId( item->uniqueId() ); //debug() << "found path = " << path << endl; } else { //debug() << "Setting uniqueid of item and trying again" << endl; item->setUniqueId(); if( !item->uniqueId().isEmpty() ) path = CollectionDB::instance()->urlFromUniqueId( item->uniqueId() ); } if( !path.isEmpty() ) { item->setUrl( KURL( path ) ); if( item->checkExists() ) item->setFilestatusEnabled( true ); else item->setFilestatusEnabled( false ); } else item->setFilestatusEnabled( false ); } else if( !item->isFilestatusEnabled() ) item->setFilestatusEnabled( true ); bool returnValue = item->isFilestatusEnabled(); return returnValue; } void Playlist::activate( QListViewItem *item ) { ///item will be played if possible, the playback may be delayed ///so we start the glow anyway and hope //All internal requests for playback should come via //this function please! if( !item ) { //we have reached the end of the playlist EngineController::instance()->stop(); setCurrentTrack( 0 ); Amarok::OSD::instance()->OSDWidget::show( i18n("Playlist finished"), QImage( KIconLoader().iconPath( "amarok", -KIcon::SizeHuge ) ) ); return; } #define item static_cast(item) if ( !checkFileStatus( item ) ) { Amarok::StatusBar::instance()->shortMessage( i18n("Local file does not exist.") ); return; } if( dynamicMode() && !m_dynamicDirt && !Amarok::repeatTrack() ) { if( m_currentTrack && item->isDynamicEnabled() ) this->moveItem( item, 0, m_currentTrack ); else { MyIt it( this, MyIt::Visible ); bool hasHistory = false; if ( *it && !(*it)->isDynamicEnabled() ) { hasHistory = true; for( ; *it && !(*it)->isDynamicEnabled() ; ++it ); } if( item->isDynamicEnabled() ) { hasHistory ? this->moveItem( item, 0, *it ) : this->moveItem( item, 0, 0 ); } else // !item->isDynamicEnabled() { hasHistory ? insertMediaInternal( item->url(), *it ): insertMediaInternal( item->url(), 0 ); m_dynamicDirt = true; return; } } if( m_currentTrack && m_currentTrack != item ) m_currentTrack->setDynamicEnabled( false ); advanceDynamicTrack(); } if( Amarok::entireAlbums() ) { if( !item->nextInAlbum() ) appendToPreviousAlbums( item->m_album ); } else appendToPreviousTracks( item ); //if we are playing something from the next tracks //list, remove it from the list if( m_nextTracks.removeRef( item ) ) emit queueChanged( PLItemList(), PLItemList( item ) ); //looks bad painting selected and glowing //only do when user explicitly activates an item though item->setSelected( false ); setCurrentTrack( item ); m_dynamicDirt = false; //use PlaylistItem::MetaBundle as it also updates the audioProps EngineController::instance()->play( *item ); #undef item } QPair Playlist::toolTipText( QWidget*, const QPoint &pos ) const { PlaylistItem *item = static_cast( itemAt( pos ) ); if( !item ) return QPair( QString::null, QRect() ); const QPoint contentsPos = viewportToContents( pos ); const int col = header()->sectionAt( contentsPos.x() ); if( item == m_renameItem && col == m_renameColumn ) return QPair( QString::null, QRect() ); QString text; if( col == PlaylistItem::Rating ) text = item->ratingDescription( item->rating() ); else text = item->text( col ); QRect irect = itemRect( item ); const int headerPos = header()->sectionPos( col ); irect.setLeft( headerPos - 1 ); irect.setRight( headerPos + header()->sectionSize( col ) ); static QFont f; static int minbearing = 1337 + 666; //can be 0 or negative, 2003 is less likely if( minbearing == 2003 || f != font() ) { f = font(); //getting your bearings can be expensive, so we cache them minbearing = fontMetrics().minLeftBearing() + fontMetrics().minRightBearing(); } int itemWidth = irect.width() - itemMargin() * 2 + minbearing - 2; if( item->pixmap( col ) ) itemWidth -= item->pixmap( col )->width(); if( item == m_currentTrack ) { if( col == m_firstColumn ) itemWidth -= 12; if( col == mapToLogicalColumn( numVisibleColumns() - 1 ) ) itemWidth -= 12; } if( col != PlaylistItem::Rating && fontMetrics().width( text ) <= itemWidth ) return QPair( QString::null, QRect() ); QRect globalRect( viewport()->mapToGlobal( irect.topLeft() ), irect.size() ); QSimpleRichText t( text, font() ); int dright = QApplication::desktop()->screenGeometry( qscrollview() ).topRight().x(); t.setWidth( dright - globalRect.left() ); if( col == PlaylistItem::Rating ) globalRect.setRight( kMin( dright, kMax( globalRect.left() + t.widthUsed(), globalRect.left() + ( StarManager::instance()->getGreyStar()->width() + 1 ) * ( ( item->rating() + 1 ) / 2 ) ) ) ); else globalRect.setRight( kMin( globalRect.left() + t.widthUsed(), dright ) ); globalRect.setBottom( globalRect.top() + kMax( irect.height(), t.height() ) - 1 ); if( ( col == PlaylistItem::Rating && PlaylistItem::ratingAtPoint( contentsPos.x() ) <= item->rating() + 1 ) || ( col != PlaylistItem::Rating ) ) { text = text.replace( "&", "&" ).replace( "<", "<" ).replace( ">", ">" ); if( item->isCurrent() ) { text = QString("%1").arg( text ); Amarok::ToolTip::s_hack = 1; //HACK for precise positioning } return QPair( text, globalRect ); } return QPair( QString::null, QRect() ); } void Playlist::activateByIndex( int index ) { QListViewItem* item = itemAtIndex( index ); if ( item ) activate(item); } void Playlist::setCurrentTrack( PlaylistItem *item ) { ///mark item as the current track and make it glow PlaylistItem *prev = m_currentTrack; //FIXME best method would be to observe usage, especially don't shift if mouse is moving nearby if( item && ( !prev || prev == currentItem() ) && !renameLineEdit()->isVisible() && m_selCount < 2 ) { if( !prev ) //if nothing is current and then playback starts, we must show the currentTrack ensureItemCentered( item ); //handles 0 gracefully else { const int prevY = itemPos( prev ); const int prevH = prev->height(); // check if the previous track is visible if( prevY <= contentsY() + visibleHeight() && prevY + prevH >= contentsY() ) { // in random mode always jump, if previous track is visible if( AmarokConfig::randomMode() ) ensureItemCentered( item ); else if( prev && prev == currentItem() ) setCurrentItem( item ); //FIXME would be better to just never be annoying // so if the user caused the track change, always show the new track // but if it is automatic be careful // if old item in view then try to keep the new one near the middle const int y = itemPos( item ); const int h = item->height(); const int vh = visibleHeight(); const int amount = h * 3; int d = y - contentsY(); if( d > 0 ) { d += h; d -= vh; if( d > 0 && d <= amount ) // scroll down setContentsPos( contentsX(), y - vh + amount ); } else if( d >= -amount ) // scroll up setContentsPos( contentsX(), y - amount ); } } } m_currentTrack = item; if ( m_currentTrack ) m_currentTrack->setIsNew(false); if ( prev ) { //reset to normal height prev->invalidateHeight(); prev->setup(); //remove pixmap in first column prev->setPixmap( m_firstColumn, QPixmap() ); } updateNextPrev(); setCurrentTrackPixmap(); Glow::reset(); slotGlowTimer(); } int Playlist::currentTrackIndex( bool onlyCountVisible ) { int index = 0; for( MyIt it( this, onlyCountVisible ? MyIt::Visible : MyIt::All ); *it; ++it ) { if ( *it == m_currentTrack ) return index; ++index; } return -1; } int Playlist::totalTrackCount() const { return m_totalCount; } BundleList Playlist::nextTracks() const { BundleList list; for( QPtrListIterator it( m_nextTracks ); *it; ++it ) list << (**it); return list; } uint Playlist::repeatAlbumTrackCount() const { if ( m_currentTrack && m_currentTrack->m_album ) return m_currentTrack->m_album->tracks.count(); else return 0; } const DynamicMode* Playlist::dynamicMode() const { return m_dynamicMode; } DynamicMode* Playlist::modifyDynamicMode() { DynamicMode *m = m_dynamicMode; if( !m ) return 0; m_dynamicMode = new DynamicMode( *m ); return m; } void Playlist::finishedModifying( DynamicMode *mode ) { DynamicMode *m = m_dynamicMode; setDynamicMode( mode ); delete m; } void Playlist::setCurrentTrackPixmap( int state ) { if( !m_currentTrack ) return; QString pixmap = QString::null; if( state < 0 ) state = EngineController::engine()->state(); if( state == Engine::Paused ) pixmap = "currenttrack_pause"; else if( state == Engine::Playing ) pixmap = "currenttrack_play"; m_currentTrack->setPixmap( m_firstColumn, pixmap.isNull() ? QPixmap() : Amarok::getPNG( pixmap ) ); PlaylistItem::setPixmapChanged(); } PlaylistItem* Playlist::restoreCurrentTrack() { ///It is always possible that the current track has been lost ///eg it was removed and then reinserted, here we check const KURL url = EngineController::instance()->playingURL(); if ( !(m_currentTrack && ( m_currentTrack->url() == url || !m_currentTrack->url().isEmpty() && url.isEmpty() ) ) ) { PlaylistItem* item; for( item = firstChild(); item && item->url() != url; item = item->nextSibling() ) {} setCurrentTrack( item ); //set even if NULL } if( m_currentTrack && EngineController::instance()->engine()->state() == Engine::Playing && !Glow::timer.isActive() ) Glow::startTimer(); return m_currentTrack; } void Playlist::countChanged() { if( !m_itemCountDirty ) { m_itemCountDirty = true; QTimer::singleShot( 0, this, SLOT( slotCountChanged() ) ); } } bool Playlist::isTrackAfter() const { ///Is there a track after the current track? //order is carefully crafted, remember count() is O(n) //TODO randomMode will end if everything is in prevTracks return !currentTrack() && !isEmpty() || !m_nextTracks.isEmpty() || currentTrack() && currentTrack()->itemBelow() || totalTrackCount() > 1 && ( AmarokConfig::randomMode() || Amarok::repeatPlaylist() || Amarok::repeatAlbum() && repeatAlbumTrackCount() > 1 ); } bool Playlist::isTrackBefore() const { //order is carefully crafted, remember count() is O(n) return !isEmpty() && ( currentTrack() && (currentTrack()->itemAbove() || Amarok::repeatPlaylist() && totalTrackCount() > 1) || AmarokConfig::randomMode() && totalTrackCount() > 1 ); } void Playlist::updateNextPrev() { Amarok::actionCollection()->action( "play" )->setEnabled( !isEmpty() ); Amarok::actionCollection()->action( "prev" )->setEnabled( isTrackBefore() ); Amarok::actionCollection()->action( "next" )->setEnabled( isTrackAfter() ); Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !isEmpty() ); Amarok::actionCollection()->action( "playlist_show" )->setEnabled( m_currentTrack ); if( m_currentTrack ) // ensure currentTrack is shown at correct height m_currentTrack->setup(); } //////////////////////////////////////////////////////////////////////////////// /// EngineObserver Reimplementation //////////////////////////////////////////////////////////////////////////////// void Playlist::engineNewMetaData( const MetaBundle &bundle, bool trackChanged ) { if ( !bundle.podcastBundle() ) { if ( m_currentTrack && !trackChanged ) { //if the track hasn't changed then this is a meta-data update if( stopAfterMode() == StopAfterCurrent || !m_nextTracks.isEmpty() ) Playlist::instance()->playNextTrack( true ); //this is a hack, I repeat a hack! FIXME FIXME //we do it because often the stream title is from the pls file and is informative //we don't want to lose it when we get the meta data else if ( m_currentTrack->artist().isEmpty() ) { QString comment = m_currentTrack->title(); m_currentTrack->copyFrom( bundle ); m_currentTrack->setComment( comment ); } else m_currentTrack->copyFrom( bundle ); } else //ensure the currentTrack is set correctly and highlight it restoreCurrentTrack(); } else //ensure the currentTrack is set correctly and highlight it restoreCurrentTrack(); if( m_currentTrack ) m_currentTrack->filter( m_filter ); } void Playlist::engineStateChanged( Engine::State state, Engine::State /*oldState*/ ) { switch( state ) { case Engine::Playing: Amarok::actionCollection()->action( "pause" )->setEnabled( true ); Amarok::actionCollection()->action( "stop" )->setEnabled( true ); Glow::startTimer(); break; case Engine::Paused: Amarok::actionCollection()->action( "pause" )->setEnabled( false ); Amarok::actionCollection()->action( "stop" )->setEnabled( true ); Glow::reset(); if( m_currentTrack ) slotGlowTimer(); //update glow state break; case Engine::Empty: Amarok::actionCollection()->action( "pause" )->setEnabled( false ); Amarok::actionCollection()->action( "stop" )->setEnabled( false ); //leave the glow state at full colour Glow::reset(); if ( m_currentTrack ) { //remove pixmap in all columns QPixmap null; for( int i = 0; i < header()->count(); i++ ) m_currentTrack->setPixmap( i, null ); PlaylistItem::setPixmapChanged(); if( stopAfterMode() == StopAfterCurrent ) setStopAfterMode( DoNotStop ); //reset glow state slotGlowTimer(); } case Engine::Idle: slotGlowTimer(); break; } //POSSIBLYAHACK //apparently you can't rely on EngineController::engine()->state() == state here, so pass it explicitly setCurrentTrackPixmap( state ); } //////////////////////////////////////////////////////////////////////////////// /// KListView Reimplementation //////////////////////////////////////////////////////////////////////////////// void Playlist::appendMedia( const QString &path ) { appendMedia( KURL::fromPathOrURL( path ) ); } void Playlist::appendMedia( const KURL &url ) { insertMedia( KURL::List( url ) ); } void Playlist::clear() //SLOT { if( isLocked() || renameLineEdit()->isVisible() ) return; disableDynamicMode(); emit aboutToClear(); //will saveUndoState() setCurrentTrack( 0 ); m_prevTracks.clear(); m_prevAlbums.clear(); if (m_stopAfterTrack) { m_stopAfterTrack = 0; if ( stopAfterMode() != StopAfterCurrent ) { setStopAfterMode( DoNotStop ); } } const PLItemList prev = m_nextTracks; m_nextTracks.clear(); emit queueChanged( PLItemList(), prev ); // Update player button states Amarok::actionCollection()->action( "play" )->setEnabled( false ); Amarok::actionCollection()->action( "prev" )->setEnabled( false ); Amarok::actionCollection()->action( "next" )->setEnabled( false ); Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( false ); ThreadManager::instance()->abortAllJobsNamed( "TagWriter" ); // something to bear in mind, if there is any event in the loop // that depends on a PlaylistItem, we are about to crash Amarok // never unlock() the Playlist until it is safe! safeClear(); m_total = 0; m_albums.clear(); setPlaylistName( i18n( "Untitled" ) ); } /** * Workaround for Qt 3.3.5 bug in QListView::clear() * @see http://lists.kde.org/?l=kde-devel&m=113113845120155&w=2 * @see BUG 116004 */ void Playlist::safeClear() { /* 3.3.5 and 3.3.6 have bad KListView::clear() functions. 3.3.5 forgets to clear the pointer to the highlighted item. 3.3.6 forgets to clear the pointer to the last dragged item */ if ( strcmp( qVersion(), "3.3.5" ) == 0 || strcmp( qVersion(), "3.3.6" ) == 0 ) { bool block = signalsBlocked(); blockSignals( true ); clearSelection(); QListViewItem *c = firstChild(); QListViewItem *n; while( c ) { n = c->nextSibling(); if ( !static_cast( c )->isEmpty() ) //avoid deleting markers delete c; c = n; } blockSignals( block ); triggerUpdate(); } else KListView::clear(); } void Playlist::setSorting( int col, bool b ) { saveUndoState(); KListView::setSorting( col, b ); } void Playlist::setColumnWidth( int col, int width ) { KListView::setColumnWidth( col, width ); //FIXME this is because Qt doesn't by default disable resizing width 0 columns. GRRR! //NOTE default column sizes are stored in default amarokrc so that restoreLayout() in ctor will // call this function. This is necessary because addColumn() doesn't call setColumnWidth() GRRR! header()->setResizeEnabled( width != 0, col ); } void Playlist::rename( QListViewItem *item, int column ) //SLOT { if( !item ) return; switch( column ) { case PlaylistItem::Artist: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->artistList() ); break; case PlaylistItem::Album: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->albumList() ); break; case PlaylistItem::Genre: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->genreList() ); break; case PlaylistItem::Composer: renameLineEdit()->completionObject()->setItems( CollectionDB::instance()->composerList() ); break; default: renameLineEdit()->completionObject()->clear(); break; } renameLineEdit()->completionObject()->setCompletionMode( KGlobalSettings::CompletionPopupAuto ); renameLineEdit()->completionObject()->setIgnoreCase( true ); m_editOldTag = static_cast(item)->exactText( column ); if( m_selCount <= 1 ) { if( currentItem() ) currentItem()->setSelected( false ); item->setSelected( true ); } setCurrentItem( item ); KListView::rename( item, column ); m_renameItem = item; m_renameColumn = column; static_cast(item)->setIsBeingRenamed( true ); } void Playlist::writeTag( QListViewItem *qitem, const QString &, int column ) //SLOT { const bool dynamicEnabled = static_cast(qitem)->isDynamicEnabled(); if( m_itemsToChangeTagsFor.isEmpty() ) m_itemsToChangeTagsFor.append( static_cast( qitem ) ); const QString newTag = static_cast( qitem )->exactText( column ); for( PlaylistItem *item = m_itemsToChangeTagsFor.first(); item; item = m_itemsToChangeTagsFor.next() ) { if( !checkFileStatus( item ) ) continue; const QString oldTag = item == qitem ? m_editOldTag : item->exactText(column); if( column == PlaylistItem::Score ) CollectionDB::instance()->setSongPercentage( item->url().path(), newTag.toInt() ); else if( column == PlaylistItem::Rating ) CollectionDB::instance()->setSongRating( item->url().path(), newTag.toInt() ); else if (oldTag != newTag) ThreadManager::instance()->queueJob( new TagWriter( item, oldTag, newTag, column ) ); else if( item->deleteAfterEditing() ) { removeItem( item ); delete item; } } if( dynamicMode() ) static_cast(qitem)->setDynamicEnabled( dynamicEnabled ); m_itemsToChangeTagsFor.clear(); m_editOldTag = QString::null; } void Playlist::columnOrderChanged() //SLOT { const uint prevColumn = m_firstColumn; //determine first visible column for ( m_firstColumn = 0; m_firstColumn < header()->count(); m_firstColumn++ ) if ( header()->sectionSize( header()->mapToSection( m_firstColumn ) ) ) break; //convert to logical column m_firstColumn = header()->mapToSection( m_firstColumn ); //force redraw of currentTrack if( m_currentTrack ) { m_currentTrack->setPixmap( prevColumn, QPixmap() ); setCurrentTrackPixmap(); } QResizeEvent e( size(), QSize() ); viewportResizeEvent( &e ); emit columnsChanged(); } void Playlist::paletteChange( const QPalette &p ) { using namespace Glow; QColor fg; QColor bg; { using namespace Base; const uint steps = STEPS+5+5; //so we don't fade all the way to base, and all the way up to highlight either fg = colorGroup().highlight(); bg = colorGroup().base(); dr = double(bg.red() - fg.red()) / steps; dg = double(bg.green() - fg.green()) / steps; db = double(bg.blue() - fg.blue()) / steps; r = fg.red() + int(dr*5.0); //we add 5 steps so the default colour is slightly different to highlight g = fg.green() + int(dg*5.0); b = fg.blue() + int(db*5.0); } { using namespace Text; const uint steps = STEPS + 5; //so we don't fade all the way to base fg = colorGroup().highlightedText(); bg = colorGroup().text(); dr = double(bg.red() - fg.red()) / steps; dg = double(bg.green() - fg.green()) / steps; db = double(bg.blue() - fg.blue()) / steps; r = fg.red(); g = fg.green(); b = fg.blue(); } KListView::paletteChange( p ); counter = 0; // reset the counter or apparently the text lacks contrast slotGlowTimer(); // repaint currentTrack marker } void Playlist::contentsDragEnterEvent( QDragEnterEvent *e ) { QString data; QCString subtype; QTextDrag::decode( e, data, subtype ); e->accept( e->source() == viewport() || subtype == "amarok-sql" || subtype == "uri-list" || //this is to prevent DelayedUrlLists from performing their queries KURLDrag::canDecode( e ) ); } void Playlist::contentsDragMoveEvent( QDragMoveEvent* e ) { if( !e->isAccepted() ) return; #if KDE_IS_VERSION( 3, 3, 91 ) const bool ctrlPressed = KApplication::keyboardMouseState() & Qt::ControlButton; #else const bool ctrlPressed = KApplication::keyboardModifiers() & ControlMask; #endif //Get the closest item _before_ the cursor const QPoint p = contentsToViewport( e->pos() ); QListViewItem *item = itemAt( p ); if( !item || ctrlPressed ) item = lastItem(); else if( p.y() - itemRect( item ).top() < (item->height()/2) ) item = item->itemAbove(); if( item != m_marker ) { //NOTE this if block prevents flicker slotEraseMarker(); m_marker = item; //NOTE this is the correct place to set m_marker viewportPaintEvent( 0 ); } } void Playlist::contentsDragLeaveEvent( QDragLeaveEvent* ) { slotEraseMarker(); } void Playlist::contentsDropEvent( QDropEvent *e ) { DEBUG_BLOCK //NOTE parent is always 0 currently, but we support it in case we start using trees QListViewItem *parent = 0; QListViewItem *after = m_marker; if( m_marker && !( static_cast(m_marker)->isDynamicEnabled() ) && dynamicMode() ) { // If marker is disabled, and there is a current track, or marker is not the last enabled track // don't allow inserting if ( m_currentTrack || ( m_marker->itemBelow() && !( static_cast(m_marker->itemBelow())->isDynamicEnabled() ) ) ) { slotEraseMarker(); return; } } if( !after ) findDrop( e->pos(), parent, after ); //shouldn't happen, but you never know! slotEraseMarker(); if ( e->source() == viewport() ) { setSorting( NO_SORT ); //disableSorting and saveState() movableDropEvent( parent, after ); if( dynamicMode() && static_cast(after)->isDynamicEnabled() ) { QPtrList items = selectedItems(); QListViewItem *item; for( item = items.first(); item; item = items.next() ) static_cast(item)->setDynamicEnabled( true ); } } else { QString data; QCString subtype; QTextDrag::decode( e, data, subtype ); debug() << "QTextDrag::subtype(): " << subtype << endl; if( subtype == "amarok-sql" ) { setSorting( NO_SORT ); QString query = data.section( "\n", 1 ); ThreadManager::instance()->queueJob( new SqlLoader( query, after ) ); } else if( subtype == "dynamic" ) { // Deserialize pointer DynamicEntry* entry = reinterpret_cast( data.toULongLong() ); loadDynamicMode( entry ); } else if( KURLDrag::canDecode( e ) ) { debug() << "KURLDrag::canDecode" << endl; KURL::List list; KURLDrag::decode( e, list ); insertMediaInternal( list, static_cast( after ) ); } else e->ignore(); } updateNextPrev(); } QDragObject* Playlist::dragObject() { DEBUG_THREAD_FUNC_INFO KURL::List list; for( MyIt it( this, MyIt::Selected ); *it; ++it ) { const PlaylistItem *item = static_cast( *it ); const KURL url = item->url(); list += url; } KURLDrag *drag = new KURLDrag( list, viewport() ); drag->setPixmap( CollectionDB::createDragPixmap( list ), QPoint( CollectionDB::DRAGPIXMAP_OFFSET_X, CollectionDB::DRAGPIXMAP_OFFSET_Y ) ); return drag; } #include void Playlist::viewportPaintEvent( QPaintEvent *e ) { if( e ) KListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent() if ( m_marker ) { QPainter p( viewport() ); p.fillRect( drawDropVisualizer( 0, 0, m_marker ), QBrush( colorGroup().highlight().dark(), QBrush::Dense4Pattern ) ); } else if( m_showHelp && isEmpty() ) { QPainter p( viewport() ); QString minimumText(i18n( "
" "

The Playlist

" "This is the playlist. " "To create a listing, " "drag tracks from the browser-panels on the left, " "drop them here and then double-click them to start playback." "
" ) ); QSimpleRichText *t = new QSimpleRichText( minimumText + i18n( "
" "

The Browsers

" "The browsers are the source of all your music. " "The collection-browser holds your collection. " "The playlist-browser holds your pre-set playlistings. " "The file-browser shows a file-selector which you can use to access any music on your computer. " "
" ), QApplication::font() ); if ( t->width()+30 >= viewport()->width() || t->height()+30 >= viewport()->height() ) { // too big for the window, so let's cut part of the text delete t; t = new QSimpleRichText( minimumText, QApplication::font()); if ( t->width()+30 >= viewport()->width() || t->height()+30 >= viewport()->height() ) { //still too big, giving up return; } } const uint w = t->width(); const uint h = t->height(); const uint x = (viewport()->width() - w - 30) / 2 ; const uint y = (viewport()->height() - h - 30) / 2 ; p.setBrush( colorGroup().background() ); p.drawRoundRect( x, y, w+30, h+30, (8*200)/w, (8*200)/h ); t->draw( &p, x+15, y+15, QRect(), colorGroup() ); delete t; } } static uint negativeWidth = 0; void Playlist::viewportResizeEvent( QResizeEvent *e ) { if ( !m_smartResizing ) { KListView::viewportResizeEvent( e ); return; } //only be clever with the sizing if there is not many items //TODO don't allow an item to be made too small (ie less than 50% of ideal width) //makes this much quicker header()->blockSignals( true ); const double W = (double)e->size().width() - negativeWidth; for( uint c = 0; c < m_columnFraction.size(); ++c ) { switch( c ) { case PlaylistItem::Track: case PlaylistItem::Bitrate: case PlaylistItem::SampleRate: case PlaylistItem::Filesize: case PlaylistItem::Score: case PlaylistItem::Rating: case PlaylistItem::Type: case PlaylistItem::PlayCount: case PlaylistItem::Length: case PlaylistItem::Year: case PlaylistItem::DiscNumber: case PlaylistItem::Bpm: break; //these columns retain their width - their items tend to have uniform size default: if( m_columnFraction[c] > 0 ) setColumnWidth( c, int(W * m_columnFraction[c]) ); } } header()->blockSignals( false ); //ensure that the listview scrollbars are updated etc. triggerUpdate(); } void Playlist::columnResizeEvent( int col, int oldw, int neww ) { if ( !m_smartResizing ) return; //prevent recursion header()->blockSignals( true ); //qlistview is stupid sometimes if ( neww < 0 ) setColumnWidth( col, 0 ); if ( neww == 0 ) { //the column in question has been hidden //we need to adjust the other columns to fit const double W = (double)width() - negativeWidth; for( uint c = 0; c < m_columnFraction.size(); ++c ) { if( c == (uint)col ) continue; switch( c ) { case PlaylistItem::Track: case PlaylistItem::Bitrate: case PlaylistItem::SampleRate: case PlaylistItem::Filesize: case PlaylistItem::Score: case PlaylistItem::Rating: case PlaylistItem::Type: case PlaylistItem::PlayCount: case PlaylistItem::Length: case PlaylistItem::Year: case PlaylistItem::DiscNumber: case PlaylistItem::Bpm: break; default: if( m_columnFraction[c] > 0 ) setColumnWidth( c, int(W * m_columnFraction[c]) ); } } } else if( oldw != 0 ) { //adjust the size of the column on the right side of this one for( int section = col, index = header()->mapToIndex( section ); index < header()->count(); ) { section = header()->mapToSection( ++index ); if ( header()->sectionSize( section ) ) { int newSize = header()->sectionSize( section ) + oldw - neww; if ( newSize > 5 ) { setColumnWidth( section, newSize ); //we only want to adjust one column! break; } } } } header()->blockSignals( false ); negativeWidth = 0; uint w = 0; //determine width excluding the columns that have static size for( uint x = 0; x < m_columnFraction.size(); ++x ) { switch( x ) { case PlaylistItem::Track: case PlaylistItem::Bitrate: case PlaylistItem::SampleRate: case PlaylistItem::Filesize: case PlaylistItem::Score: case PlaylistItem::Rating: case PlaylistItem::Type: case PlaylistItem::PlayCount: case PlaylistItem::Length: case PlaylistItem::Year: case PlaylistItem::DiscNumber: case PlaylistItem::Bpm: break; default: w += columnWidth( x ); } negativeWidth += columnWidth( x ); } //determine the revised column fractions for( uint x = 0; x < m_columnFraction.size(); ++x ) m_columnFraction[x] = (double)columnWidth( x ) / double(w); //negative width is an important property, honest! negativeWidth -= w; //we have to do this after we have established negativeWidth and set the columnFractions if( neww == 0 || oldw == 0 ) { //then this column has been inserted or removed, we need to update all the column widths QResizeEvent e( size(), QSize() ); viewportResizeEvent( &e ); emit columnsChanged(); } } bool Playlist::eventFilter( QObject *o, QEvent *e ) { #define me static_cast(e) #define ke static_cast(e) if( o == header() && e->type() == QEvent::MouseButtonPress && me->button() == Qt::RightButton ) { enum { HIDE = 1000, SELECT, CUSTOM, SMARTRESIZING }; const int mouseOverColumn = header()->sectionAt( me->pos().x() ); KPopupMenu popup; if( mouseOverColumn >= 0 ) popup.insertItem( i18n("&Hide %1").arg( columnText( mouseOverColumn ) ), HIDE ); //TODO KPopupMenu sub; for( int i = 0; i < columns(); ++i ) //columns() references a property if( !columnWidth( i ) ) sub.insertItem( columnText( i ), i, i + 1 ); sub.setItemVisible( PlaylistItem::Score, AmarokConfig::useScores() ); sub.setItemVisible( PlaylistItem::Rating, AmarokConfig::useRatings() ); sub.setItemVisible( PlaylistItem::Mood, AmarokConfig::showMoodbar() ); popup.insertItem( i18n("&Show Column" ), &sub ); popup.insertItem( i18n("Select &Columns..."), SELECT ); popup.insertItem( i18n("&Fit to Width"), SMARTRESIZING ); popup.setItemChecked( SMARTRESIZING, m_smartResizing ); int col = popup.exec( static_cast(e)->globalPos() ); switch( col ) { case HIDE: { hideColumn( mouseOverColumn ); QResizeEvent e( size(), QSize() ); viewportResizeEvent( &e ); } break; case SELECT: ColumnsDialog::display(); break; case CUSTOM: addCustomColumn(); break; case SMARTRESIZING: m_smartResizing = !m_smartResizing; Amarok::config( "PlaylistWindow" )->writeEntry( "Smart Resizing", m_smartResizing ); if ( m_smartResizing ) columnResizeEvent( 0, 0, 0 ); //force refit. FIXME: It doesn't work perfectly break; default: if( col != -1 ) { adjustColumn( col ); header()->setResizeEnabled( true, col ); } } //determine first visible column again, since it has changed columnOrderChanged(); //eat event return true; } // not in slotMouseButtonPressed because we need to disable normal usage. if( o == viewport() && e->type() == QEvent::MouseButtonPress && me->state() == Qt::ControlButton && me->button() == RightButton ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); if( !item ) return true; item->isSelected() ? queueSelected(): queue( item ); return true; //yum! } // trigger in-place tag editing else if( o == viewport() && e->type() == QEvent::MouseButtonPress && me->button() == LeftButton ) { m_clicktimer->stop(); m_itemToRename = 0; int col = header()->sectionAt( viewportToContents( me->pos() ).x() ); if( col != PlaylistItem::Rating ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); bool edit = item && item->isSelected() && selectedItems().count()==1 && (me->state() & ~LeftButton) == 0 && item->url().isLocalFile(); if( edit ) { m_clickPos = me->pos(); m_itemToRename = item; m_columnToRename = col; //return true; } } } else if( o == viewport() && e->type() == QEvent::MouseButtonRelease && me->button() == LeftButton ) { int col = header()->sectionAt( viewportToContents( me->pos() ).x() ); if( col != PlaylistItem::Rating ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); if( item == m_itemToRename && me->pos() == m_clickPos ) { m_clicktimer->start( int( QApplication::doubleClickInterval() ), true ); return true; } else { m_itemToRename = 0; } } } // avoid in-place tag editing upon double-clicks else if( e->type() == QEvent::MouseButtonDblClick && me->button() == Qt::LeftButton ) { m_itemToRename = 0; m_clicktimer->stop(); } // Toggle play/pause if user middle-clicks on current track else if( o == viewport() && e->type() == QEvent::MouseButtonPress && me->button() == MidButton ) { PlaylistItem *item = static_cast( itemAt( me->pos() ) ); if( item && item == m_currentTrack ) { EngineController::instance()->playPause(); return true; //yum! } } else if( o == renameLineEdit() && e->type() == 6 /*QEvent::KeyPress*/ && m_renameItem ) { const int visibleCols = numVisibleColumns(); int physicalColumn = visibleCols - 1; while( mapToLogicalColumn( physicalColumn ) != m_renameColumn && physicalColumn >= 0 ) physicalColumn--; if( physicalColumn < 0 ) { warning() << "the column counting code is wrong! tell illissius." << endl; return false; } int column = m_renameColumn; QListViewItem *item = m_renameItem; if( ke->state() & Qt::AltButton ) { if( ke->key() == Qt::Key_Up && m_visCount > 1 ) if( !( item = m_renameItem->itemAbove() ) ) { item = *MyIt( this, MyIt::Visible ); while( item->itemBelow() ) item = item->itemBelow(); } if( ke->key() == Qt::Key_Down && m_visCount > 1 ) if( !( item = m_renameItem->itemBelow() ) ) item = *MyIt( this, MyIt::Visible ); if( ke->key() == Qt::Key_Left ) do { if( physicalColumn == 0 ) physicalColumn = visibleCols - 1; else physicalColumn--; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); if( ke->key() == Qt::Key_Right ) do { if( physicalColumn == visibleCols - 1 ) physicalColumn = 0; else physicalColumn++; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); } if( ke->key() == Qt::Key_Tab ) do { if( physicalColumn == visibleCols - 1 ) { if( !( item = m_renameItem->itemBelow() ) ) item = *MyIt( this, MyIt::Visible ); physicalColumn = 0; } else physicalColumn++; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); if( ke->key() == Qt::Key_Backtab ) do { if( physicalColumn == 0 ) { if( !( item = m_renameItem->itemAbove() ) ) { item = *MyIt( this, MyIt::Visible ); while( item->itemBelow() ) item = item->itemBelow(); } physicalColumn = visibleCols - 1; } else physicalColumn--; column = mapToLogicalColumn( physicalColumn ); } while( !isRenameable( column ) ); if( item != m_renameItem || column != m_renameColumn ) { if( !item->isSelected() ) m_itemsToChangeTagsFor.clear(); //the item that actually got changed will get added back, in writeTag() m_renameItem->setText( m_renameColumn, renameLineEdit()->text() ); doneEditing( m_renameItem, m_renameColumn ); rename( item, column ); return true; } } else if( o == renameLineEdit() && ( e->type() == QEvent::Hide || e->type() == QEvent::Close ) ) { m_renameItem = 0; } //allow the header to process this return KListView::eventFilter( o, e ); #undef me #undef ke } void Playlist::slotSingleClick() { if( m_itemToRename ) { rename( m_itemToRename, m_columnToRename ); } m_itemToRename = 0; } void Playlist::customEvent( QCustomEvent *e ) { if( e->type() == (int)UrlLoader::JobFinishedEvent ) { refreshNextTracks( 0 ); PLItemList in, out; // Disable help if playlist is populated if ( !isEmpty() ) m_showHelp = false; if ( !m_queueList.isEmpty() ) { KURL::List::Iterator jt; for( MyIt it( this, MyIt::All ); *it; ++it ) { jt = m_queueList.find( (*it)->url() ); if ( jt != m_queueList.end() ) { queue( *it ); ( m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); m_queueList.remove( jt ); } } m_queueList.clear(); } if( m_dynamicDirt ) { PlaylistItem *after = m_currentTrack; if( !after ) { after = firstChild(); while( after && !after->isDynamicEnabled() ) after = after->nextSibling(); } else after = static_cast( after->itemBelow() ); if( after ) { PlaylistItem *prev = static_cast( after->itemAbove() ); if( prev && dynamicMode() ) prev->setDynamicEnabled( false ); activate( after ); if ( dynamicMode() && dynamicMode()->cycleTracks() ) adjustDynamicPrevious( dynamicMode()->previousCount() ); } } if( m_queueDirt ) { PlaylistItem *after = 0; m_nextTracks.isEmpty() ? after = m_currentTrack : after = m_nextTracks.last(); if( !after ) { after = firstChild(); while( after && !after->isDynamicEnabled() ) after = after->nextSibling(); } else after = static_cast( after->itemBelow() ); if( after ) { m_nextTracks.append( after ); in.append( after ); } m_queueDirt = false; } if( !in.isEmpty() || !out.isEmpty() ) emit queueChanged( in, out ); //force redraw of currentTrack marker, play icon, etc. restoreCurrentTrack(); } updateNextPrev(); } //////////////////////////////////////////////////////////////////////////////// /// Misc Public Methods //////////////////////////////////////////////////////////////////////////////// bool Playlist::saveM3U( const QString &path, bool relative ) const { QValueList urls; QValueList titles; QValueList lengths; for( MyIt it( firstChild(), MyIt::Visible ); *it; ++it ) { urls << (*it)->url(); titles << (*it)->title(); lengths << (*it)->length(); } return PlaylistBrowser::savePlaylist( path, urls, titles, lengths, relative ); } void Playlist::saveXML( const QString &path ) { DEBUG_BLOCK QFile file( path ); if( !file.open( IO_WriteOnly ) ) return; QString buffer; QTextStream stream( &buffer, IO_WriteOnly ); stream.setEncoding( QTextStream::UnicodeUTF8 ); stream << "\n"; QString dynamic; if( dynamicMode() ) { const QString title = ( dynamicMode()->title() ).replace( "&", "&" ) .replace( "<", "<" ) .replace( ">", ">" ); dynamic = QString(" dynamicMode=\"%1\"").arg( title ); } stream << QString( "\n" ) .arg( "Amarok" ).arg( Amarok::xmlVersion() ).arg( dynamic ); for( MyIt it( this, MyIt::All ); *it; ++it ) { const PlaylistItem *item = *it; if( item->isEmpty() ) continue; // Skip marker items and such QStringList attributes; const int queueIndex = m_nextTracks.findRef( item ); if ( queueIndex != -1 ) attributes << "queue_index" << QString::number( queueIndex + 1 ); else if ( item == currentTrack() ) attributes << "queue_index" << QString::number( 0 ); if( !item->isFilestatusEnabled() ) attributes << "filestatusdisabled" << "true"; if( !item->isDynamicEnabled() ) attributes << "dynamicdisabled" << "true"; if( m_stopAfterTrack == item ) attributes << "stop_after" << "true"; item->save( stream, attributes ); } stream << "\n"; QTextStream fstream( &file ); fstream.setEncoding( QTextStream::UnicodeUTF8 ); fstream << buffer; } void Playlist::burnPlaylist( int projectType ) { KURL::List list; QListViewItemIterator it( this ); for( ; it.current(); ++it ) { PlaylistItem *item = static_cast(*it); KURL url = item->url(); if( url.isLocalFile() ) list << url; } K3bExporter::instance()->exportTracks( list, projectType ); } void Playlist::burnSelectedTracks( int projectType ) { KURL::List list; QListViewItemIterator it( this, QListViewItemIterator::Selected ); for( ; it.current(); ++it ) { PlaylistItem *item = static_cast(*it); KURL url = item->url(); if( url.isLocalFile() ) list << url; } K3bExporter::instance()->exportTracks( list, projectType ); } void Playlist::addCustomMenuItem( const QString &submenu, const QString &itemTitle ) //for dcop { m_customSubmenuItem[submenu] << itemTitle; } bool Playlist::removeCustomMenuItem( const QString &submenu, const QString &itemTitle ) //for dcop { if( !m_customSubmenuItem.contains(submenu) ) return false; if( m_customSubmenuItem[submenu].remove( itemTitle ) != 0 ) { if( m_customSubmenuItem[submenu].count() == 0 ) m_customSubmenuItem.remove( submenu ); return true; return true; } else return false; } void Playlist::customMenuClicked(int id) //adapted from burnSelectedTracks { QString message = m_customIdItem[id]; QListViewItemIterator it( this, QListViewItemIterator::Selected ); for( ; it.current(); ++it ) { PlaylistItem *item = static_cast(*it); KURL url = item->url().url(); message += ' ' + url.url(); } ScriptManager::instance()->customMenuClicked( message ); } void Playlist::setDynamicMode( DynamicMode *mode ) //SLOT { // if mode == 0, then dynamic mode was just turned off. DynamicMode* const prev = m_dynamicMode; m_dynamicMode = mode; if( mode ) AmarokConfig::setLastDynamicMode( mode->title() ); emit dynamicModeChanged( mode ); if( mode ) { m_oldRandom = AmarokConfig::randomMode(); m_oldRepeat = AmarokConfig::repeat(); } Amarok::actionCollection()->action( "random_mode" )->setEnabled( !mode ); Amarok::actionCollection()->action( "repeat" )->setEnabled( !mode ); Amarok::actionCollection()->action( "playlist_shuffle" )->setEnabled( !mode ); Amarok::actionCollection()->action( "repopulate" )->setEnabled( mode ); if( prev && mode ) { if( prev->previousCount() != mode->previousCount() ) adjustDynamicPrevious( mode->previousCount(), true ); if( prev->upcomingCount() != mode->upcomingCount() ) adjustDynamicUpcoming( true ); } else if( !prev ) { if( mode ) adjustDynamicPrevious( mode->previousCount(), true ); setDynamicHistory( true ); // disable items! } else if( !mode ) // enable items again, dynamic mode is no more setDynamicHistory( false ); } void Playlist::loadDynamicMode( DynamicMode *mode ) //SLOT { saveUndoState(); setDynamicMode( mode ); if( isEmpty() ) repopulate(); } void Playlist::editActiveDynamicMode() //SLOT { if( !m_dynamicMode ) return; DynamicMode *m = modifyDynamicMode(); ConfigDynamic::editDynamicPlaylist( PlaylistWindow::self(), m ); m->rebuildCachedItemSet(); finishedModifying( m ); } void Playlist::disableDynamicMode() //SLOT { if( !m_dynamicMode ) return; setDynamicMode( 0 ); AmarokConfig::setRandomMode( m_oldRandom ); AmarokConfig::setRepeat( m_oldRepeat ); static_cast(Amarok::actionCollection()->action( "random_mode" ))->setCurrentItem( m_oldRandom ); static_cast(Amarok::actionCollection()->action( "repeat" ))->setCurrentItem( m_oldRepeat ); } void Playlist::rebuildDynamicModeCache() //SLOT { if( !m_dynamicMode ) return; DynamicMode *m = modifyDynamicMode(); m->rebuildCachedItemSet(); finishedModifying( m ); } void Playlist::repopulate() //SLOT { if( !m_dynamicMode ) return; // Repopulate the upcoming tracks MyIt it( this, MyIt::All ); QPtrList list; for( ; *it; ++it ) { PlaylistItem *item = static_cast(*it); int queueIndex = m_nextTracks.findRef( item ); bool isQueued = queueIndex != -1; bool isMarker = item->isEmpty(); // markers are used by playlistloader, and removing them is not good if( !item->isDynamicEnabled() || item == m_currentTrack || isQueued || isMarker ) continue; list.prepend( *it ); } saveUndoState(); //remove the items for( QListViewItem *item = list.first(); item; item = list.next() ) { removeItem( static_cast( item ) ); delete item; } //calling advanceDynamicTrack will remove an item too, which is undesirable //block signals to avoid saveUndoState being called blockSignals( true ); addDynamicModeTracks( dynamicMode()->upcomingCount() ); blockSignals( false ); } void Playlist::shuffle() //SLOT { if( dynamicMode() ) return; QPtrList list; setSorting( NO_SORT ); // shuffle only VISIBLE entries for( MyIt it( this ); *it; ++it ) list.append( *it ); // we do it in two steps because the iterator doesn't seem // to like it when we do takeItem and ++it in the same loop for( QListViewItem *item = list.first(); item; item = list.next() ) takeItem( item ); //shuffle KRandomSequence( (long)KApplication::random() ).randomize( &list ); //reinsert in new order for( QListViewItem *item = list.first(); item; item = list.next() ) insertItem( item ); updateNextPrev(); } void Playlist::removeSelectedItems() //SLOT { if( isLocked() ) return; //assemble a list of what needs removing //calling removeItem() iteratively is more efficient if they are in _reverse_ order, hence the prepend() PLItemList queued, list; int dontReplaceDynamic = 0; for( PlaylistIterator it( this, MyIt::Selected ); *it; ++it ) { if( !(*it)->isDynamicEnabled() ) dontReplaceDynamic++; ( m_nextTracks.contains( *it ) ? queued : list ).prepend( *it ); } if( (int)list.count() == childCount() ) { //clear() will saveUndoState for us. clear(); // faster return; } if( list.isEmpty() && queued.isEmpty() ) return; saveUndoState(); if( dynamicMode() ) { int currentTracks = childCount(); int minTracks = dynamicMode()->upcomingCount(); if( m_currentTrack ) currentTracks -= currentTrackIndex() + 1; int difference = currentTracks - minTracks; if( difference >= 0 ) difference -= list.count(); if( difference < 0 ) { addDynamicModeTracks( -difference ); } } //remove the items if( queued.count() ) { for( QListViewItem *item = queued.first(); item; item = queued.next() ) removeItem( static_cast( item ), true ); emit queueChanged( PLItemList(), queued ); for( QListViewItem *item = queued.first(); item; item = queued.next() ) delete item; } for( QListViewItem *item = list.first(); item; item = list.next() ) { removeItem( static_cast( item ) ); delete item; } updateNextPrev(); //NOTE no need to emit childCountChanged(), removeItem() does that for us //select next item in list setSelected( currentItem(), true ); } void Playlist::deleteSelectedFiles() //SLOT { if( isLocked() ) return; KURL::List urls; //assemble a list of what needs removing for( MyIt it( this, MyIt::Selected ); it.current(); urls << static_cast( *it )->url(), ++it ); if( DeleteDialog::showTrashDialog(this, urls) ) { CollectionDB::instance()->removeSongs( urls ); removeSelectedItems(); foreachType( KURL::List, urls ) CollectionDB::instance()->emitFileDeleted( (*it).path() ); QTimer::singleShot( 0, CollectionView::instance(), SLOT( renderView() ) ); } } void Playlist::removeDuplicates() //SLOT { // Remove dead entries: for( QListViewItemIterator it( this ); it.current(); ) { PlaylistItem* item = static_cast( *it ); const KURL url = item->url(); if ( url.isLocalFile() && !QFile::exists( url.path() ) ) { removeItem( item ); ++it; delete item; } else ++it; } // Remove dupes: QSortedList list; for( QListViewItemIterator it( this ); it.current(); ++it ) list.prepend( static_cast( it.current() ) ); list.sort(); QPtrListIterator it( list ); PlaylistItem *item; while( (item = it.current()) ) { const KURL &compare = item->url(); ++it; if ( *it && compare == it.current()->url() ) { removeItem( item ); delete item; } } } void Playlist::copyToClipboard( const QListViewItem *item ) const //SLOT { if( !item ) item = currentTrack(); if( item ) { const PlaylistItem* playlistItem = static_cast( item ); QString text = playlistItem->prettyTitle(); // For streams add the streamtitle too //TODO make prettyTitle do this if ( playlistItem->url().protocol() == "http" ) text.append( " :: " + playlistItem->url().url() ); // Copy both to clipboard and X11-selection QApplication::clipboard()->setText( text, QClipboard::Clipboard ); QApplication::clipboard()->setText( text, QClipboard::Selection ); Amarok::OSD::instance()->OSDWidget::show( i18n( "Copied: %1" ).arg( text ), QImage(CollectionDB::instance()->albumImage(*playlistItem )) ); } } void Playlist::undo() //SLOT { if( !isLocked() ) switchState( m_undoList, m_redoList ); } void Playlist::redo() //SLOT { if( !isLocked() ) switchState( m_redoList, m_undoList ); } void Playlist::updateMetaData( const MetaBundle &mb ) //SLOT { SHOULD_BE_GUI for( MyIt it( this, MyIt::All ); *it; ++it ) if( mb.url() == (*it)->url() ) { (*it)->copyFrom( mb ); (*it)->filter( m_filter ); } } void Playlist::adjustColumn( int n ) { if( n == PlaylistItem::Rating ) setColumnWidth( n, PlaylistItem::ratingColumnWidth() ); else if( n == PlaylistItem::Mood ) setColumnWidth( n, 120 ); else KListView::adjustColumn( n ); } void Playlist::showQueueManager() { DEBUG_BLOCK // Only show the dialog once if( QueueManager::instance() ) { QueueManager::instance()->raise(); return; } QueueManager dialog; if( dialog.exec() == QDialog::Accepted ) { changeFromQueueManager(dialog.newQueue()); } } void Playlist::changeFromQueueManager(QPtrList list) { PLItemList oldQueue = m_nextTracks; m_nextTracks = list; PLItemList in, out; // make sure we repaint items no longer queued for( PlaylistItem* item = oldQueue.first(); item; item = oldQueue.next() ) if( !m_nextTracks.containsRef( item ) ) out << item; for( PlaylistItem* item = m_nextTracks.first(); item; item = m_nextTracks.next() ) if( !oldQueue.containsRef( item ) ) in << item; emit queueChanged( in, out ); // repaint newly queued or altered queue items if( dynamicMode() ) sortQueuedItems(); else refreshNextTracks(); } void Playlist::setFilterSlot( const QString &query ) //SLOT { m_filtertimer->stop(); if( m_filter != query ) { m_prevfilter = m_filter; m_filter = query; } m_filtertimer->start( 50, true ); } void Playlist::setDelayedFilter() //SLOT { setFilter( m_filter ); //to me it seems sensible to do this, BUT if it seems annoying to you, remove it showCurrentTrack(); } void Playlist::setFilter( const QString &query ) //SLOT { const bool advanced = ExpressionParser::isAdvancedExpression( query ); MyIt it( this, ( !advanced && query.lower().contains( m_prevfilter.lower() ) ) ? MyIt::Visible : MyIt::All ); if( advanced ) { ParsedExpression parsed = ExpressionParser::parse( query ); QValueList visible = visibleColumns(); for(; *it; ++it ) (*it)->setVisible( (*it)->matchesParsedExpression( parsed, visible ) ); } else { // optimized path const QStringList terms = QStringList::split( ' ', query.lower() ); const MetaBundle::ColumnMask visible = getVisibleColumnMask(); for(; *it; ++it ) { (*it)->setVisible( (*it)->matchesFast(terms, visible)); } } if( m_filter != query ) { m_prevfilter = m_filter; m_filter = query; } updateNextPrev(); } void Playlist::scoreChanged( const QString &path, float score ) { for( MyIt it( this, MyIt::All ); *it; ++it ) { PlaylistItem *item = static_cast( *it ); if ( item->url().path() == path ) { item->setScore( score ); item->setPlayCount( CollectionDB::instance()->getPlayCount( path ) ); item->setLastPlay( CollectionDB::instance()->getLastPlay( path ).toTime_t() ); item->filter( m_filter ); } } } void Playlist::ratingChanged( const QString &path, int rating ) { for( MyIt it( this, MyIt::All ); *it; ++it ) { PlaylistItem *item = static_cast( *it ); if ( item->url().path() == path ) { item->setRating( rating ); item->filter( m_filter ); } } } void Playlist::fileMoved( const QString &srcPath, const QString &dstPath ) { // Make sure the MoodServer gets this signal first! MoodServer::instance()->slotFileMoved( srcPath, dstPath ); for( MyIt it( this, MyIt::All ); *it; ++it ) { PlaylistItem *item = static_cast( *it ); if ( item->url().path() == srcPath ) { item->setUrl( KURL::fromPathOrURL( dstPath ) ); item->filter( m_filter ); } } } void Playlist::appendToPreviousTracks( PlaylistItem *item ) { if( !m_prevTracks.containsRef( item ) ) { m_total -= item->totalIncrementAmount(); m_prevTracks.append( item ); } } void Playlist::appendToPreviousAlbums( PlaylistAlbum *album ) { if( !m_prevAlbums.containsRef( album ) ) { m_total -= album->total; m_prevAlbums.append( album ); } } void Playlist::removeFromPreviousTracks( PlaylistItem *item ) { if( item ) { if( m_prevTracks.removeRef( item ) ) m_total += item->totalIncrementAmount(); } else if( (item = m_prevTracks.current()) != 0 ) if( m_prevTracks.remove() ) m_total += item->totalIncrementAmount(); } void Playlist::removeFromPreviousAlbums( PlaylistAlbum *album ) { if( album ) { if( m_prevAlbums.removeRef( album ) ) m_total += album->total; } else if( (album = m_prevAlbums.current()) != 0 ) if( m_prevAlbums.remove() ) m_total += album->total; } void Playlist::showContextMenu( QListViewItem *item, const QPoint &p, int col ) //SLOT { //if clicked on an empty area enum { REPOPULATE, ENABLEDYNAMIC }; if( item == 0 ) { KPopupMenu popup; Amarok::actionCollection()->action("playlist_save")->plug( &popup ); Amarok::actionCollection()->action("playlist_clear")->plug( &popup ); DynamicMode *m = 0; if(dynamicMode()) popup.insertItem( SmallIconSet( Amarok::icon( "dynamic" ) ), i18n("Repopulate"), REPOPULATE); else { Amarok::actionCollection()->action("playlist_shuffle")->plug( &popup ); m = PlaylistBrowser::instance()->findDynamicModeByTitle( AmarokConfig::lastDynamicMode() ); if( m ) popup.insertItem( SmallIconSet( Amarok::icon( "dynamic" ) ), i18n("L&oad %1").arg( m->title().replace( '&', "&&" ) ), ENABLEDYNAMIC); } switch(popup.exec(p)) { case ENABLEDYNAMIC: loadDynamicMode( m ); break; case REPOPULATE: repopulate(); break; } return; } #define item static_cast(item) enum { PLAY, PLAY_NEXT, STOP_DONE, VIEW, EDIT, FILL_DOWN, COPY, CROP_PLAYLIST, SAVE_PLAYLIST, REMOVE, FILE_MENU, ORGANIZE, MOVE_TO_COLLECTION, COPY_TO_COLLECTION, DELETE, TRASH, REPEAT, LAST }; //keep LAST last const bool canRename = isRenameable( col ) && item->url().isLocalFile(); const bool isCurrent = (item == m_currentTrack); const bool isPlaying = EngineController::engine()->state() == Engine::Playing; const bool trackColumn = col == PlaylistItem::Track; const bool isLastFm = item->url().protocol() == "lastfm"; const QString tagName = columnText( col ); const QString tag = item->text( col ); uint itemCount = 0; for( MyIt it( this, MyIt::Selected ); *it; ++it ) itemCount++; PrettyPopupMenu popup; // if(itemCount==1) // popup.insertTitle( KStringHandler::rsqueeze( MetaBundle( item ).prettyTitle(), 50 )); // else // popup.insertTitle(i18n("1 Track", "%n Selected Tracks", itemCount)); if( isCurrent && isLastFm ) { KActionCollection *ac = Amarok::actionCollection(); if( ac->action( "skip" ) ) ac->action( "skip" )->plug( &popup ); if( ac->action( "love" ) ) ac->action( "love" )->plug( &popup ); if( ac->action( "ban" ) ) ac->action( "ban" )->plug( &popup ); popup.insertSeparator(); } if( !isCurrent || !isPlaying ) popup.insertItem( SmallIconSet( Amarok::icon( "play" ) ), isCurrent && isPlaying ? i18n( "&Restart" ) : i18n( "&Play" ), PLAY ); if( isCurrent && !isLastFm && isPlaying ) Amarok::actionCollection()->action( "pause" )->plug( &popup ); // Begin queue entry logic popup.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n("&Queue Selected Tracks"), PLAY_NEXT ); bool queueToggle = false; MyIt it( this, MyIt::Selected ); bool firstQueued = ( m_nextTracks.findRef( *it ) != -1 ); for( ++it ; *it; ++it ) { if ( ( m_nextTracks.findRef( *it ) != -1 ) != firstQueued ) { queueToggle = true; break; } } if( itemCount == 1 ) { if ( !firstQueued ) popup.changeItem( PLAY_NEXT, i18n( "&Queue Track" ) ); else popup.changeItem( PLAY_NEXT, SmallIconSet( Amarok::icon( "dequeue_track" ) ), i18n("&Dequeue Track") ); } else { if ( queueToggle ) popup.changeItem( PLAY_NEXT, i18n( "Toggle &Queue Status (1 track)", "Toggle &Queue Status (%n tracks)", itemCount ) ); else // remember, queueToggled only gets set to false if there are items queued and not queued. // so, if queueToggled is false, all items have the same queue status as the first item. if ( !firstQueued ) popup.changeItem( PLAY_NEXT, i18n( "&Queue Selected Tracks" ) ); else popup.changeItem( PLAY_NEXT, SmallIconSet( Amarok::icon( "dequeue_track" ) ), i18n("&Dequeue Selected Tracks") ); } // End queue entry logic bool afterCurrent = false; if( !m_nextTracks.isEmpty() ? m_nextTracks.getLast() : m_currentTrack ) for( MyIt it( !m_nextTracks.isEmpty() ? m_nextTracks.getLast() : m_currentTrack, MyIt::Visible ); *it; ++it ) if( *it == item ) { afterCurrent = true; break; } if( itemCount == 1 ) { Amarok::actionCollection()->action( "stop_after" )->plug( &popup ); dynamic_cast( Amarok::actionCollection()->action( "stop_after" ) )->setChecked( m_stopAfterTrack == item ); } if( isCurrent && itemCount == 1 ) { popup.insertItem( SmallIconSet( Amarok::icon( "repeat_track" ) ), i18n( "&Repeat Track" ), REPEAT ); popup.setItemChecked( REPEAT, Amarok::repeatTrack() ); } popup.insertSeparator(); if( itemCount > 1 ) { popup.insertItem( SmallIconSet( Amarok::icon( "playlist" ) ), i18n("&Set as Playlist (Crop)"), CROP_PLAYLIST ); popup.insertItem( SmallIconSet( Amarok::icon( "save" ) ), i18n("S&ave as Playlist..."), SAVE_PLAYLIST ); } popup.insertItem( SmallIconSet( Amarok::icon( "remove_from_playlist" ) ), i18n( "Re&move From Playlist" ), this, SLOT( removeSelectedItems() ), Key_Delete, REMOVE ); popup.insertSeparator(); KPopupMenu fileMenu; if( CollectionDB::instance()->isDirInCollection( item->url().directory() ) ) { fileMenu.insertItem( SmallIconSet( "filesaveas" ), i18n("&Organize File...", "&Organize %n Files...", itemCount), ORGANIZE ); } else { fileMenu.insertItem( SmallIconSet( "filesaveas" ), i18n("&Copy Track to Collection...", "&Copy %n Tracks to Collection...", itemCount), COPY_TO_COLLECTION ); fileMenu.insertItem( SmallIconSet( "filesaveas" ), i18n("&Move Track to Collection...", "&Move %n Tracks to Collection...", itemCount), MOVE_TO_COLLECTION ); } fileMenu.insertItem( SmallIconSet( Amarok::icon( "remove" ) ), i18n("&Delete File...", "&Delete %n Selected Files...", itemCount ), this, SLOT( deleteSelectedFiles() ), SHIFT+Key_Delete, DELETE ); popup.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n("Manage &Files"), &fileMenu, FILE_MENU ); if( itemCount == 1 ) popup.insertItem( SmallIconSet( Amarok::icon( "editcopy" ) ), i18n( "&Copy Tags to Clipboard" ), COPY ); if( itemCount > 1 ) popup.insertItem( trackColumn ? i18n("Iteratively Assign Track &Numbers") : i18n("&Write '%1' for Selected Tracks") .arg( KStringHandler::rsqueeze( tag, 30 ).replace( "&", "&&" ) ), FILL_DOWN ); popup.insertItem( SmallIconSet( Amarok::icon( "edit" ) ), (itemCount == 1 ? i18n( "&Edit Tag '%1'" ) : i18n( "&Edit '%1' Tag for Selected Tracks" )).arg( tagName ), EDIT ); popup.insertItem( SmallIconSet( Amarok::icon( "info" ) ) , item->url().isLocalFile() ? i18n( "Edit Track &Information...", "Edit &Information for %n Tracks...", itemCount): i18n( "Track &Information...", "&Information for %n Tracks...", itemCount) , VIEW ); popup.setItemEnabled( EDIT, canRename ); //only enable for columns that have editable tags popup.setItemEnabled( FILL_DOWN, canRename ); popup.setItemEnabled( REMOVE, !isLocked() ); // can't remove things when playlist is locked, popup.setItemEnabled( DELETE, !isLocked() && item->url().isLocalFile() ); popup.setItemEnabled( ORGANIZE, !isLocked() && item->isKioUrl() ); popup.setItemEnabled( MOVE_TO_COLLECTION, !isLocked() && item->isKioUrl() ); popup.setItemEnabled( COPY_TO_COLLECTION, !isLocked() && item->isKioUrl() ); popup.setItemEnabled( VIEW, item->url().isLocalFile() || itemCount == 1 ); // disable for CDAudio multiselection if( m_customSubmenuItem.count() > 0 ) popup.insertSeparator(); QValueList submenuTexts = m_customSubmenuItem.keys(); for( QValueList::Iterator keyIt =submenuTexts.begin(); keyIt != submenuTexts.end(); ++keyIt ) { KPopupMenu* menu; if( (*keyIt) == "root") menu = &popup; else { menu = new KPopupMenu(); popup.insertItem( *keyIt, menu); } foreach(m_customSubmenuItem[*keyIt]) { int id; if(m_customIdItem.isEmpty()) id=LAST; else id=m_customIdItem.keys().last()+1; menu->insertItem( (*it), id ); m_customIdItem[id]= (*keyIt) + ' ' + (*it); } } const QPoint pos( p.x() - popup.sidePixmapWidth(), p.y() + 3 ); int menuItemId = popup.exec( pos ); PLItemList in, out; switch( menuItemId ) { case PLAY: if( itemCount == 1 ) { //Restarting track on dynamic mode if( isCurrent && isPlaying && dynamicMode() ) m_dynamicDirt = true; activate( item ); } else { MyIt it( this, MyIt::Selected ); activate( *it ); ++it; for( int i = 0; *it; ++i, ++it ) { in.append( *it ); m_nextTracks.insert( i, *it ); } emit queueChanged( in, out ); } break; case PLAY_NEXT: queueSelected(); break; case VIEW: showTagDialog( selectedItems() ); break; case EDIT: // do this because QListView sucks, if track change occurs during // an edit event, the rename operation ends, BUT, the list is not // cleared because writeTag is never called. Q/K ListView sucks m_itemsToChangeTagsFor.clear(); if( !item->isSelected() ) m_itemsToChangeTagsFor.append( item ); else for( MyIt it( this, MyIt::Selected ); *it; ++it ) m_itemsToChangeTagsFor.append( *it ); rename( item, col ); break; case FILL_DOWN: //Spreadsheet like fill-down { QString newTag = item->exactText( col ); MyIt it( this, MyIt::Selected ); //special handling for track column uint trackNo = (*it)->track(); //we should start at the next row if we are doing track number //and the first row has a number set if ( trackColumn && trackNo > 0 ) ++it; ThreadManager::JobList jobs; bool updateView = true; for( ; *it; ++it ) { if ( trackColumn ) //special handling for track column newTag = QString::number( ++trackNo ); else if ( *it == item ) //skip the one we are copying continue; else if( col == PlaylistItem::Score ) { CollectionDB::instance()->setSongPercentage( (*it)->url().path(), newTag.toInt() ); continue; } else if( col == PlaylistItem::Rating ) { CollectionDB::instance()->setSongRating( (*it)->url().path(), newTag.toInt() ); continue; } if ( !(*it)->isEditing( col ) ) jobs.prepend( new TagWriter( *it, (*it)->exactText( col ), newTag, col, updateView ) ); updateView = false; } ThreadManager::instance()->queueJobs( jobs ); } break; case COPY: copyToClipboard( item ); break; case CROP_PLAYLIST: if( !isLocked() ) { //use "in" for the other just because it's there and not used otherwise for( MyIt it( this, MyIt::Unselected | MyIt::Visible ); *it; ++it ) ( m_nextTracks.containsRef( *it ) ? in : out ).append( *it ); if( !in.isEmpty() || !out.isEmpty() ) { saveUndoState(); for( PlaylistItem *it = out.first(); it; it = out.next() ) removeItem( it, true ); if( !out.isEmpty() ) emit queueChanged( PLItemList(), out ); for( PlaylistItem *it = out.first(); it; it = out.next() ) delete it; for( PlaylistItem *it = in.first(); it; it = in.next() ) { removeItem( it ); delete it; } } } break; case SAVE_PLAYLIST: saveSelectedAsPlaylist(); break; case REPEAT: // FIXME HACK Accessing AmarokConfig::Enum* yields compile errors with GCC 3.3. static_cast( Amarok::actionCollection()->action( "repeat" ) ) ->setCurrentItem( Amarok::repeatTrack() ? 0 /*AmarokConfig::EnumRepeat::Off*/ : 1 /*AmarokConfig::EnumRepeat::Track*/ ); break; case ORGANIZE: case MOVE_TO_COLLECTION: case COPY_TO_COLLECTION: { KURL::List list; for( QListViewItemIterator it( this, QListViewItemIterator::Selected ); it.current(); ++it ) { PlaylistItem *i= static_cast(*it); KURL url = i->url(); list << url; } bool organize = CollectionDB::instance()->isDirInCollection( item->url().directory() ); bool move = menuItemId==MOVE_TO_COLLECTION; CollectionView::instance()->organizeFiles( list, organize ? i18n( "Organize Files" ) : move ? i18n( "Move Tracks to Collection" ) : i18n( "Copy Tracks to Collection"), !organize && !move ); } break; default: if(menuItemId < LAST) break; customMenuClicked(menuItemId); break; } #undef item } //////////////////////////////////////////////////////////////////////////////// /// Misc Protected Methods //////////////////////////////////////////////////////////////////////////////// void Playlist::fontChange( const QFont &old ) { KListView::fontChange( old ); initStarPixmaps(); triggerUpdate(); } void Playlist::contentsMouseMoveEvent( QMouseEvent *e ) { if( e ) KListView::contentsMouseMoveEvent( e ); PlaylistItem *prev = m_hoveredRating; const QPoint pos = e ? e->pos() : viewportToContents( viewport()->mapFromGlobal( QCursor::pos() ) ); PlaylistItem *item = static_cast( itemAt( contentsToViewport( pos ) ) ); if( item && pos.x() > header()->sectionPos( PlaylistItem::Rating ) && pos.x() < header()->sectionPos( PlaylistItem::Rating ) + header()->sectionSize( PlaylistItem::Rating ) ) { m_hoveredRating = item; m_hoveredRating->updateColumn( PlaylistItem::Rating ); } else m_hoveredRating = 0; if( prev ) { if( m_selCount > 1 && prev->isSelected() ) QScrollView::updateContents( header()->sectionPos( PlaylistItem::Rating ) + 1, contentsY(), header()->sectionSize( PlaylistItem::Rating ) - 2, visibleHeight() ); else prev->updateColumn( PlaylistItem::Rating ); } } void Playlist::leaveEvent( QEvent *e ) { KListView::leaveEvent( e ); PlaylistItem *prev = m_hoveredRating; m_hoveredRating = 0; if( prev ) prev->updateColumn( PlaylistItem::Rating ); } void Playlist::contentsMousePressEvent( QMouseEvent *e ) { PlaylistItem *item = static_cast( itemAt( contentsToViewport( e->pos() ) ) ); int beginRatingSection = header()->sectionPos( PlaylistItem::Rating ); int endRatingSection = beginRatingSection + header()->sectionSize( PlaylistItem::Rating ); /// Conditions on setting the rating of an item if( item && !( e->state() & Qt::ControlButton || e->state() & Qt::ShiftButton ) && // skip if ctrl or shift held ( e->button() & Qt::LeftButton ) && // only on a left click ( e->pos().x() > beginRatingSection && e->pos().x() < endRatingSection ) ) // mouse over rating column { int rating = item->ratingAtPoint( e->pos().x() ); if( item->isSelected() ) setSelectedRatings( rating ); else // toggle half star CollectionDB::instance()->setSongRating( item->url().path(), rating, true ); } else KListView::contentsMousePressEvent( e ); } void Playlist::contentsWheelEvent( QWheelEvent *e ) { PlaylistItem* const item = static_cast( itemAt( contentsToViewport( e->pos() ) ) ); const int column = header()->sectionAt( e->pos().x() ); const int distance = header()->sectionPos( column ) + header()->sectionSize( column ) - e->pos().x(); const int maxdistance = fontMetrics().width( QString::number( m_nextTracks.count() ) ) + 7; if( item && column == m_firstColumn && distance <= maxdistance && item->isQueued() ) { const int n = e->delta() / 120, s = n / abs(n), pos = item->queuePosition(); PLItemList changed; for( int i = 1; i <= abs(n); ++i ) { const int dest = pos + s*i; if( kClamp( dest, 0, int( m_nextTracks.count() ) - 1 ) != dest ) break; PlaylistItem* const p = m_nextTracks.at( dest ); if( changed.findRef( p ) == -1 ) changed << p; if( changed.findRef( m_nextTracks.at( dest - s ) ) == -1 ) changed << m_nextTracks.at( dest - s ); m_nextTracks.replace( dest, m_nextTracks.at( dest - s ) ); m_nextTracks.replace( dest - s, p ); } for( int i = 0, n = changed.count(); i < n; ++i ) changed.at(i)->update(); } else KListView::contentsWheelEvent( e ); } //////////////////////////////////////////////////////////////////////////////// /// Misc Private Methods //////////////////////////////////////////////////////////////////////////////// void Playlist::lock() { if( m_lockStack == 0 ) { m_clearButton->setEnabled( false ); m_undoButton->setEnabled( false ); m_redoButton->setEnabled( false ); } m_lockStack++; } void Playlist::unlock() { Q_ASSERT( m_lockStack > 0 ); m_lockStack--; if( m_lockStack == 0 ) { m_clearButton->setEnabled( true ); m_undoButton->setEnabled( !m_undoList.isEmpty() ); m_redoButton->setEnabled( !m_redoList.isEmpty() ); } } int Playlist::numVisibleColumns() const { int r = 0, i = 1; for( const int n = columns(); i <= n; ++i) if( columnWidth( i - 1 ) ) ++r; return r; } QValueList Playlist::visibleColumns() const { QValueList r; for( int i = 0, n = columns(); i < n; ++i) if( columnWidth( i ) ) r.append( i ); return r; } MetaBundle::ColumnMask Playlist::getVisibleColumnMask() const { MetaBundle::ColumnMask mask = 0; for( int i = 0, n = columns(); i < n; ++i) if( columnWidth( i ) ) mask = mask | (1 << i); return mask; } int Playlist::mapToLogicalColumn( int physical ) const { int logical = header()->mapToSection( physical ); //skip hidden columns int n = 0; for( int i = 0; i <= physical; ++i ) if( !header()->sectionSize( header()->mapToSection( physical - i ) ) ) ++n; while( n ) { logical = header()->mapToSection( ++physical ); if( logical < 0 ) { logical = header()->mapToSection( physical - 1 ); break; } if( header()->sectionSize( logical ) ) --n; } return logical; } void Playlist::setColumns( QValueList order, QValueList visible ) { for( int i = order.count() - 1; i >= 0; --i ) header()->moveSection( order[i], i ); for( int i = 0; i < PlaylistItem::NUM_COLUMNS; ++i ) { if( visible.contains( i ) ) adjustColumn( i ); else hideColumn( i ); } columnOrderChanged(); } void Playlist::removeItem( PlaylistItem *item, bool multi ) { // NOTE we don't check isLocked() here as it is assumed that if you call this function you // really want to remove the item, there is no way the user can reach here without passing // a lock() check, (currently...) //this function ensures we don't have dangling pointers to items that are about to be removed //for some reason using QListView::takeItem() and QListViewItem::takeItem() was ineffective //NOTE we don't delete item for you! You must call delete item yourself :) //TODO there must be a way to do this without requiring notification from the item dtor! //NOTE orginally this was in ~PlaylistItem(), but that caused crashes due to clear() *shrug* //NOTE items already removed by takeItem() will crash if you call nextSibling() on them // taken items return 0 from listView() //FIXME if you remove a series of items including the currentTrack and all the nextTracks // then no new nextTrack will be selected and the playlist will resume from the begging // next time if( m_currentTrack == item ) { setCurrentTrack( 0 ); //ensure the playlist doesn't start at the beginning after the track that's playing ends //we don't need to do that in random mode, it's getting randomly selected anyways if( m_nextTracks.isEmpty() && !AmarokConfig::randomMode() ) { //*MyIt( item ) returns either "item" or if item is hidden, the next visible playlistitem PlaylistItem* const next = *MyIt( item ); if( next ) { m_nextTracks.append( next ); next->update(); } } } if( m_stopAfterTrack == item ) { m_stopAfterTrack = 0; //to be safe if (stopAfterMode() != StopAfterCurrent) setStopAfterMode( DoNotStop ); } //keep m_nextTracks queue synchronized if( m_nextTracks.removeRef( item ) && !multi ) emit queueChanged( PLItemList(), PLItemList( item ) ); //keep recent buffer synchronized removeFromPreviousTracks( item ); //removes all pointers to item updateNextPrev(); } void Playlist::ensureItemCentered( QListViewItem *item ) { if( !item ) return; //HACK -- apparently the various metrics aren't reliable while the UI is still updating & stuff m_itemToReallyCenter = item; QTimer::singleShot( 0, this, SLOT( reallyEnsureItemCentered() ) ); } void Playlist::reallyEnsureItemCentered() { if( QListViewItem *item = m_itemToReallyCenter ) { m_itemToReallyCenter = 0; if( m_selCount == 1 ) { PlaylistItem *previtem = *MyIt( this, MyIt::Selected ); if( previtem && previtem != item ) previtem->setSelected( false ); } setCurrentItem( item ); ensureVisible( contentsX(), item->itemPos() + item->height() / 2, 0, visibleHeight() / 2 ); triggerUpdate(); } } void Playlist::refreshNextTracks( int from ) { // This function scans the m_nextTracks list starting from the 'from' // position and from there on updates the progressive numbering on related // items and repaints them. In short it performs an update subsequent to // a renumbering/order changing at some point of the m_nextTracks list. //start on the 'from'-th item of the list for( PlaylistItem* item = (from == -1) ? m_nextTracks.current() : m_nextTracks.at( from ); item; item = m_nextTracks.next() ) { item->update(); } } void Playlist::saveUndoState() //SLOT { if( saveState( m_undoList ) ) { m_redoList.clear(); m_undoButton->setEnabled( true ); m_redoButton->setEnabled( false ); } } bool Playlist::saveState( QStringList &list ) { //used by undo system, save state of playlist to undo/redo list //do not change this! It's required by the undo/redo system to work! //if you must change this, fix undo/redo first. Ask me what needs fixing if( !isEmpty() ) { QString fileName; m_undoCounter %= AmarokConfig::undoLevels(); fileName.setNum( m_undoCounter++ ); fileName.prepend( m_undoDir.absPath() + '/' ); fileName.append( ".xml" ); if ( list.count() >= (uint)AmarokConfig::undoLevels() ) { m_undoDir.remove( list.first() ); list.pop_front(); } saveXML( fileName ); list.append( fileName ); // Reset isNew state of all items in the playlist (determines font coloring) PlaylistItem* item = static_cast( firstChild() ); while( item ) { item->setIsNew( false ); item = item->nextSibling(); } triggerUpdate(); return true; } return false; } void Playlist::switchState( QStringList &loadFromMe, QStringList &saveToMe ) { m_undoDirt = true; //switch to a previously saved state, remember current state KURL url; url.setPath( loadFromMe.last() ); loadFromMe.pop_back(); //save current state saveState( saveToMe ); //this is clear() minus some parts, for instance we don't want to cause a saveUndoState() here m_currentTrack = 0; disableDynamicMode(); Glow::reset(); m_prevTracks.clear(); m_prevAlbums.clear(); const PLItemList prev = m_nextTracks; m_nextTracks.clear(); emit queueChanged( PLItemList(), prev ); ThreadManager::instance()->abortAllJobsNamed( "TagWriter" ); safeClear(); m_total = 0; m_albums.clear(); insertMediaInternal( url, 0, 0 ); //because the listview is empty, undoState won't be forced m_undoButton->setEnabled( !m_undoList.isEmpty() ); m_redoButton->setEnabled( !m_redoList.isEmpty() ); if( dynamicMode() ) setDynamicHistory( true ); m_undoDirt = false; } void Playlist::saveSelectedAsPlaylist() { MyIt it( this, MyIt::Visible | MyIt::Selected ); if( !(*it) ) return; //safety const QString album = (*it)->album(), artist = (*it)->artist(); int suggestion = !album.stripWhiteSpace().isEmpty() ? 1 : !artist.stripWhiteSpace().isEmpty() ? 2 : 3; while( *it ) { if( suggestion == 1 && (*it)->album()->lower().stripWhiteSpace() != album.lower().stripWhiteSpace() ) suggestion = 2; if( suggestion == 2 && (*it)->artist()->lower().stripWhiteSpace() != artist.lower().stripWhiteSpace() ) suggestion = 3; if( suggestion == 3 ) break; ++it; } QString path = PlaylistDialog::getSaveFileName( suggestion == 1 ? album : suggestion == 2 ? artist : i18n( "Untitled" ) ); if( path.isEmpty() ) return; QValueList urls; QValueList titles; QValueList lengths; for( it = MyIt( this, MyIt::Visible | MyIt::Selected ); *it; ++it ) { urls << (*it)->url(); titles << (*it)->title(); lengths << (*it)->length(); } if( PlaylistBrowser::savePlaylist( path, urls, titles, lengths ) ) PlaylistWindow::self()->showBrowser( "PlaylistBrowser" ); } void Playlist::initStarPixmaps() { StarManager::instance()->reinitStars( fontMetrics().height(), itemMargin() ); } void Playlist::slotMouseButtonPressed( int button, QListViewItem *after, const QPoint &p, int col ) //SLOT { switch( button ) { case Qt::MidButton: { const QString path = QApplication::clipboard()->text( QClipboard::Selection ); const KURL url = KURL::fromPathOrURL( path ); if( url.isValid() ) insertMediaInternal( url, static_cast(after ? after : lastItem()) ); break; } case Qt::RightButton: showContextMenu( after, p, col ); break; default: ; } } void Playlist::slotContentsMoving() { Amarok::ToolTip::hideTips(); QTimer::singleShot( 0, this, SLOT( contentsMouseMoveEvent() ) ); } void Playlist::slotQueueChanged( const PLItemList &/*in*/, const PLItemList &out) { for( QPtrListIterator it( out ); *it; ++it ) (*it)->update(); refreshNextTracks( 0 ); updateNextPrev(); } void Playlist::slotUseScores( bool use ) { if( !use && columnWidth( MetaBundle::Score ) ) hideColumn( MetaBundle::Score ); } void Playlist::slotUseRatings( bool use ) { if( use && !columnWidth( MetaBundle::Rating ) ) adjustColumn( MetaBundle::Rating ); else if( !use && columnWidth( MetaBundle::Rating ) ) hideColumn( MetaBundle::Rating ); } // This gets called when the user presses "Ok" or "Apply" in the // config dialog. void Playlist::slotMoodbarPrefs( bool show, bool moodier, int alter, bool withMusic ) { (void) moodier; (void) alter; (void) withMusic; if( !show && columnWidth( MetaBundle::Mood ) ) hideColumn( MetaBundle::Mood ); // Reset all of our moodbars, since they may have been permanently // disabled before because the Moodbar was disabled. We need to // do this even if the column is hidden. if( show ) { // No need to call moodbar().load(), since that will happen // automatically next time it's displayed. We do have to // repaint so that they get displayed though. for( PlaylistIterator it( this, PlaylistIterator::All ) ; *it ; ++it ) { (*it)->moodbar().reset(); repaintItem(*it); } } } void Playlist::slotGlowTimer() //SLOT { if( !currentTrack() ) return; using namespace Glow; if( counter <= STEPS*2 ) { // 0 -> STEPS -> 0 const double d = (counter > STEPS) ? 2*STEPS-counter : counter; { using namespace Base; PlaylistItem::glowIntensity = d; PlaylistItem::glowBase = QColor( r, g, b ); } { using namespace Text; PlaylistItem::glowText = QColor( r + int(d*dr), g + int(d*dg), b + int(d*db) ); } if( currentTrack() ) currentTrack()->update(); } ++counter &= 63; //built in bounds checking with &= } void Playlist::slotRepeatTrackToggled( int /* mode */ ) { if( m_currentTrack ) m_currentTrack->update(); } void Playlist::slotEraseMarker() //SLOT { if( m_marker ) { const QRect spot = drawDropVisualizer( 0, 0, m_marker ); m_marker = 0; viewport()->repaint( spot, false ); } } void Playlist::showTagDialog( QPtrList items ) { /// the tag dialog was once modal, because we thought that damage would occur /// when passing playlist items into the editor and it was removed from the playlist. /// This is simply not the case, information is written to the URL, not the item. // Playlist::lock(); if( items.isEmpty() ) return; if ( items.count() == 1 ) { PlaylistItem *item = static_cast( items.first() ); bool isDaap = item->url().protocol() == "daap"; if ( !item->url().isLocalFile() && !isDaap ) { StreamEditor dialog( this, item->title(), item->url().prettyURL(), true ); if( item->url().protocol() == "cdda" ) dialog.setCaption( i18n( "CD Audio" ) ); else dialog.setCaption( i18n( "Remote Media" ) ); dialog.exec(); } else if ( isDaap ) // don't check if exists { // The tag dialog automatically disables the widgets if the file is not local, which it is not. TagDialog *dialog = new TagDialog( *item, item, instance() ); dialog->show(); } else if ( checkFileStatus( item ) ) { TagDialog *dialog = new TagDialog( *item, item, instance() ); dialog->show(); } else KMessageBox::sorry( this, i18n( "This file does not exist:" ) + ' ' + item->url().path() ); } else { //edit multiple tracks in tag dialog KURL::List urls; for( QListViewItem *item = items.first(); item; item = items.next() ) if ( item->isVisible() ) urls << static_cast( item )->url(); TagDialog *dialog = new TagDialog( urls, instance() ); dialog->show(); } // Playlist::unlock(); } - #include #include #include #include #include #include #include #include //usleep() // Moved outside the only function that uses it because // gcc 2.95 doesn't like class declarations there. class CustomColumnDialog : public KDialog { public: CustomColumnDialog( QWidget *parent ) : KDialog( parent ) { QLabel *textLabel1, *textLabel2, *textLabel3; QLineEdit *lineEdit1, *lineEdit2; QGroupBox *groupBox1; textLabel1 = new QLabel( i18n( "

You can create a custom column that runs a shell command against each item in the playlist. " "The shell command is run as the user nobody, this is for security reasons.\n" "

You can only run the command against local files for the time being. " "The fullpath is inserted at the position %f in the string. " "If you do not specify %f it is appended." ), this ); textLabel2 = new QLabel( i18n( "Column &name:" ), this ); textLabel3 = new QLabel( i18n( "&Command:" ), this ); lineEdit1 = new QLineEdit( this, "ColumnName" ); lineEdit2 = new QLineEdit( this, "Command" ); groupBox1 = new QGroupBox( 1, Qt::Vertical, i18n( "Examples" ), this ); groupBox1->layout()->setMargin( 11 ); new KActiveLabel( i18n( "file --brief %f\n" "ls -sh %f\n" "basename %f\n" "dirname %f" ), groupBox1 ); // buddies textLabel2->setBuddy( lineEdit1 ); textLabel3->setBuddy( lineEdit2 ); // layouts QHBoxLayout *layout1 = new QHBoxLayout( 0, 0, 6 ); layout1->addItem( new QSpacerItem( 181, 20, QSizePolicy::Expanding, QSizePolicy::Minimum ) ); layout1->addWidget( new KPushButton( KStdGuiItem::ok(), this, "OkButton" ) ); layout1->addWidget( new KPushButton( KStdGuiItem::cancel(), this, "CancelButton" ) ); QGridLayout *layout2 = new QGridLayout( 0, 2, 2, 0, 6 ); layout2->QLayout::add( textLabel2 ); layout2->QLayout::add( lineEdit1 ); layout2->QLayout::add( textLabel3 ); layout2->QLayout::add( lineEdit2 ); QVBoxLayout *Form1Layout = new QVBoxLayout( this, 11, 6, "Form1Layout"); Form1Layout->addWidget( textLabel1 ); Form1Layout->addWidget( groupBox1 ); Form1Layout->addLayout( layout2 ); Form1Layout->addLayout( layout1 ); Form1Layout->addItem( new QSpacerItem( 20, 231, QSizePolicy::Minimum, QSizePolicy::Expanding ) ); // properties setCaption( i18n("Add Custom Column") ); // connects connect( child( "OkButton" ), SIGNAL(clicked()), SLOT(accept()) ); connect( child( "CancelButton" ), SIGNAL(clicked()), SLOT(reject()) ); } QString command() { return static_cast(child("Command"))->text(); } QString name() { return static_cast(child("ColumnName"))->text(); } }; void Playlist::addCustomColumn() { CustomColumnDialog dialog( this ); if ( dialog.exec() == QDialog::Accepted ) { const int index = addColumn( dialog.name(), 100 ); QStringList args = QStringList::split( ' ', dialog.command() ); QStringList::Iterator pcf = args.find( "%f" ); if ( pcf == args.end() ) { //there is no %f, so add one on the end //TODO prolly this is confusing, instead ask the user if we should add one args += "%f"; --pcf; } debug() << args << endl; //TODO need to do it with a %u for url and %f for file //FIXME gets stuck it seems if you submit broken commands //FIXME issues with the column resize stuff that cause freezing in eventFilters for( MyIt it( this ); *it; ++it ) { if( (*it)->url().protocol() != "file" ) continue; *pcf = (*it)->url().path(); debug() << args << endl; QProcess p( args ); for( p.start(); p.isRunning(); /*kapp->processEvents()*/ ) ::usleep( 5000 ); (*it)->setExactText( index, p.readStdout() ); } } } #include #include TagWriter::TagWriter( PlaylistItem *item, const QString &oldTag, const QString &newTag, const int col, const bool updateView ) : ThreadManager::Job( "TagWriter" ) , m_item( item ) , m_failed( true ) , m_oldTagString( oldTag ) , m_newTagString( newTag ) , m_tagType( col ) , m_updateView( updateView ) { Playlist::instance()->lock(); item->setEditing( col ); } TagWriter::~TagWriter() { Playlist::instance()->unlock(); } bool TagWriter::doJob() { MetaBundle mb( m_item->url(), true ); switch ( m_tagType ) { case PlaylistItem::Title: mb.setTitle( m_newTagString ); break; case PlaylistItem::Artist: mb.setArtist( m_newTagString ); break; case PlaylistItem::Composer: if ( !mb.hasExtendedMetaInformation() ) return true; mb.setComposer( m_newTagString ); break; case PlaylistItem::DiscNumber: if ( !mb.hasExtendedMetaInformation() ) return true; mb.setDiscNumber( m_newTagString.toInt() ); break; case PlaylistItem::Bpm: if ( !mb.hasExtendedMetaInformation() ) return true; mb.setBpm( m_newTagString.toFloat() ); break; case PlaylistItem::Album: mb.setAlbum( m_newTagString ); break; case PlaylistItem::Year: mb.setYear( m_newTagString.toInt() ); break; case PlaylistItem::Comment: //FIXME how does this work for vorbis files? //Are we likely to overwrite some other comments? //Vorbis can have multiple comment fields.. mb.setComment( m_newTagString ); break; case PlaylistItem::Genre: mb.setGenre( m_newTagString ); break; case PlaylistItem::Track: mb.setTrack( m_newTagString.toInt() ); break; default: return true; } m_failed = !mb.save(); return true; } void TagWriter::completeJob() { switch( m_failed ) { case true: // we write a space for some reason I cannot recall m_item->setExactText( m_tagType, m_oldTagString.isEmpty() ? " " : m_oldTagString ); Amarok::StatusBar::instance()->longMessage( i18n( "Sorry, the tag for %1 could not be changed." ).arg( m_item->url().fileName() ), KDE::StatusBar::Sorry ); break; case false: m_item->setExactText( m_tagType, m_newTagString.isEmpty() ? " " : m_newTagString ); CollectionDB::instance()->updateURL( m_item->url().path(), m_updateView ); } m_item->setIsBeingRenamed( false ); m_item->filter( Playlist::instance()->m_filter ); if( m_item->deleteAfterEditing() ) { Playlist::instance()->removeItem( m_item ); delete m_item; } } #include "playlist.moc" diff --git a/src/playlist.h b/src/playlist.h index 0a42498aa0..1c1f48b737 100644 --- a/src/playlist.h +++ b/src/playlist.h @@ -1,487 +1,547 @@ /*************************************************************************** Playlist.h - description ------------------- begin : Don Dez 5 2002 copyright : (C) 2002 by Mark Kretschmann (C) 2005 Ian Monroe (C) 2005 by Gábor Lehel ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifndef AMAROK_PLAYLIST_H #define AMAROK_PLAYLIST_H #include #include "amarok_export.h" #include "amarokconfig.h" #include "amarokdcophandler.h" #include "engineobserver.h" //baseclass #include "dynamicmode.h" #include "playlistwindow.h" //friend #include "playlistitem.h" #include "metabundle.h" #include "tooltip.h" //baseclass #include "tracktooltip.h" #include //baseclass #include //KURL::List #include //stack allocated #include //stack allocated #include //stack allocated #include //stack allocated #include //stack allocated class KAction; class KActionCollection; -class MyAtomicString; class PlaylistItem; class PlaylistEntry; class PlaylistLoader; class PlaylistAlbum; class TagWriter; class QBoxLayout; class QLabel; class QTimer; class Medium; /** * @authors Mark Kretschmann && Max Howell * * Playlist inherits KListView privately and thus is no longer a ListView * Instead it is a part of PlaylistWindow and they interact in harmony. The change * was necessary as it is too dangerous to allow public access to PlaylistItems * due to the multi-threading environment. * * Unfortunately, since QObject is now inaccessible you have to connect slots * via one of PlaylistWindow's friend members or in Playlist * * If you want to add new playlist type functionality you should implement it * inside this class or inside PlaylistWindow. * */ +// template +// AtomicString Index::fieldString(const FieldType &field) { return AtomicString(field); } + +// template<> +// AtomicString Index::fieldString(const KURL &field); + + class Playlist : private KListView, public EngineObserver, public Amarok::ToolTipClient { Q_OBJECT public: ~Playlist(); LIBAMAROK_EXPORT static Playlist *instance() { return s_instance; } static QString defaultPlaylistPath(); static const int NO_SORT = 200; static const int Append = 1; /// inserts media after the last item in the playlist static const int Queue = 2; /// inserts media after the currentTrack static const int Clear = 4; /// clears the playlist first static const int Replace = Clear; static const int DirectPlay = 8; /// start playback of the first item in the list static const int Unique = 16; /// don't insert anything already in the playlist static const int StartPlay = 32; /// start playback of the first item in the list if nothing else playing static const int Colorize = 64; /// colorize newly added items static const int DefaultOptions = Append | Unique | StartPlay; // it's really just the *ListView parts we want to hide... QScrollView *qscrollview() const { return reinterpret_cast( const_cast( this ) ); } /** Add media to the playlist * @param options you can OR these together, see the enum * @param sql Sql program to execute */ - LIBAMAROK_EXPORT void insertMedia( KURL::List, int options = Append ); + LIBAMAROK_EXPORT void insertMedia( const KURL::List &, int options = Append ); void insertMediaSql( const QString& sql, int options = Append ); // Dynamic mode functions void addDynamicModeTracks( uint songCount ); void adjustDynamicUpcoming( bool saveUndo = false ); void adjustDynamicPrevious( uint songCount, bool saveUndo = false ); void advanceDynamicTrack(); void setDynamicHistory( bool enable = true ); void burnPlaylist ( int projectType = -1 ); void burnSelectedTracks( int projectType = -1 ); int currentTrackIndex( bool onlyCountVisible = true ); bool isEmpty() const { return childCount() == 0; } LIBAMAROK_EXPORT bool isTrackBefore() const; LIBAMAROK_EXPORT bool isTrackAfter() const; void restoreSession(); // called during initialisation void setPlaylistName( const QString &name, bool proposeOverwriting = false ) { m_playlistName = name; m_proposeOverwriting = proposeOverwriting; } void proposePlaylistName( const QString &name, bool proposeOverwriting = false ) { if( isEmpty() || m_playlistName==i18n("Untitled") ) m_playlistName = name; m_proposeOverwriting = proposeOverwriting; } const QString &playlistName() const { return m_playlistName; } bool proposeOverwriteOnSave() const { return m_proposeOverwriting; } bool saveM3U( const QString&, bool relative = AmarokConfig::relativePlaylist() ) const; void saveXML( const QString& ); int totalTrackCount() const; BundleList nextTracks() const; uint repeatAlbumTrackCount() const; //returns number of tracks from same album //as current track that are in playlist (may require Play Albums in Order on). //If the information is not available, returns 0. //const so you don't change it behind Playlist's back, use modifyDynamicMode() for that const DynamicMode *dynamicMode() const; //modify the returned DynamicMode, then finishedModifying() it when done DynamicMode *modifyDynamicMode(); //call this every time you modifyDynamicMode(), otherwise you'll get memory leaks and/or crashes void finishedModifying( DynamicMode *mode ); int stopAfterMode(); void addCustomMenuItem ( const QString &submenu, const QString &itemTitle ); void customMenuClicked ( int id ); bool removeCustomMenuItem( const QString &submenu, const QString &itemTitle ); void setFont( const QFont &f ) { KListView::setFont( f ); } //made public for convenience void unsetFont() { KListView::unsetFont(); } PlaylistItem *firstChild() const { return static_cast( KListView::firstChild() ); } PlaylistItem *lastItem() const { return static_cast( KListView::lastItem() ); } PlaylistItem *currentItem() const { return static_cast( KListView::currentItem() ); } int numVisibleColumns() const; QValueList visibleColumns() const; MetaBundle::ColumnMask getVisibleColumnMask() const; int mapToLogicalColumn( int physical ) const; // Converts physical PlaylistItem column position to logical QString columnText( int c ) const { return KListView::columnText( c ); }; void setColumns( QValueList order, QValueList visible ); /** Call this to prevent items being removed from the playlist, it is mostly for internal use only * Don't forget to unlock() !! */ void lock(); void unlock(); //reimplemented to save columns by name instead of index, to be more resilient to reorderings and such void saveLayout(KConfig *config, const QString &group) const; void restoreLayout(KConfig *config, const QString &group); //AFT-related functions bool checkFileStatus( PlaylistItem * item ); void addToUniqueMap( const QString uniqueid, PlaylistItem* item ); void removeFromUniqueMap( const QString uniqueid, PlaylistItem* item ); enum RequestType { Prev = -1, Current = 0, Next = 1 }; enum StopAfterMode { DoNotStop, StopAfterCurrent, StopAfterQueue, StopAfterOther }; class QDragObject *dragObject(); friend class PlaylistItem; friend class UrlLoader; friend class QueueManager; friend class QueueLabel; friend class PlaylistWindow; friend class ColumnList; friend void Amarok::DcopPlaylistHandler::removeCurrentTrack(); //calls removeItem() and currentTrack() friend void Amarok::DcopPlaylistHandler::removeByIndex( int ); //calls removeItem() friend class TagWriter; //calls removeItem() friend void PlaylistWindow::init(); //setting up connections etc. friend TrackToolTip::TrackToolTip(); friend bool PlaylistWindow::eventFilter( QObject*, QEvent* ); //for convenience we handle some playlist events here public: QPair toolTipText( QWidget*, const QPoint &pos ) const; signals: void aboutToClear(); void itemCountChanged( int newCount, int newLength, int visCount, int visLength, int selCount, int selLength ); void queueChanged( const PLItemList &queued, const PLItemList &dequeued ); void columnsChanged(); void dynamicModeChanged( const DynamicMode *newMode ); public slots: void activateByIndex(int); void addCustomColumn(); void appendMedia( const KURL &url ); void appendMedia( const QString &path ); void clear(); void copyToClipboard( const QListViewItem* = 0 ) const; void deleteSelectedFiles(); void ensureItemCentered( QListViewItem* item ); void playCurrentTrack(); void playNextTrack( const bool forceNext = true ); void playPrevTrack(); void queueSelected(); void setSelectedRatings( int rating ); void redo(); void removeDuplicates(); void removeSelectedItems(); void setDynamicMode( DynamicMode *mode ); void loadDynamicMode( DynamicMode *mode ); //saveUndoState() + setDynamicMode() void disableDynamicMode(); void editActiveDynamicMode(); void rebuildDynamicModeCache(); void repopulate(); void safeClear(); void scoreChanged( const QString &path, float score ); void ratingChanged( const QString &path, int rating ); void fileMoved( const QString &srcPath, const QString &dstPath ); void selectAll() { QListView::selectAll( true ); } void setFilter( const QString &filter ); void setFilterSlot( const QString &filter ); //uses a delay where applicable void setStopAfterCurrent( bool on ); void setStopAfterItem( PlaylistItem *item ); void toggleStopAfterCurrentItem(); void toggleStopAfterCurrentTrack(); void setStopAfterMode( int mode ); void showCurrentTrack() { ensureItemCentered( m_currentTrack ); } void showQueueManager(); void changeFromQueueManager(QPtrList list); void shuffle(); void undo(); void updateMetaData( const MetaBundle& ); void adjustColumn( int n ); void updateEntriesUrl( const QString &oldUrl, const QString &newUrl, const QString &uniqueid ); void updateEntriesUniqueId( const QString &url, const QString &oldid, const QString &newid ); void updateEntriesStatusDeleted( const QString &absPath, const QString &uniqueid ); void updateEntriesStatusAdded( const QString &absPath, const QString &uniqueid ); void updateEntriesStatusAdded( const QMap &map ); protected: virtual void fontChange( const QFont &old ); protected slots: void contentsMouseMoveEvent( QMouseEvent *e = 0 ); void leaveEvent( QEvent *e ); void contentsMousePressEvent( QMouseEvent *e ); void contentsWheelEvent( QWheelEvent *e ); private slots: void mediumChange( int ); void slotCountChanged(); void activate( QListViewItem* ); void columnOrderChanged(); void columnResizeEvent( int, int, int ); void doubleClicked( QListViewItem* ); void generateInfo(); //generates info for Random Albums /* the only difference multi makes is whether it emits queueChanged(). (if multi, then no) if you're queue()ing many items, consider passing true and emitting queueChanged() yourself. */ /* if invertQueue then queueing an already queued song dequeues it */ void queue( QListViewItem*, bool multi = false, bool invertQueue = true ); void saveUndoState(); void setDelayedFilter(); //after the delay is over void showContextMenu( QListViewItem*, const QPoint&, int ); void slotEraseMarker(); void slotGlowTimer(); void reallyEnsureItemCentered(); void slotMouseButtonPressed( int, QListViewItem*, const QPoint&, int ); void slotSingleClick(); void slotContentsMoving(); void slotRepeatTrackToggled( int mode ); void slotQueueChanged( const PLItemList &in, const PLItemList &out); void slotUseScores( bool use ); void slotUseRatings( bool use ); void slotMoodbarPrefs( bool show, bool moodier, int alter, bool withMusic ); void updateNextPrev(); void writeTag( QListViewItem*, const QString&, int ); private: Playlist( QWidget* ); Playlist( const Playlist& ); //not defined LIBAMAROK_EXPORT static Playlist *s_instance; void countChanged(); PlaylistItem *currentTrack() const { return m_currentTrack; } PlaylistItem *restoreCurrentTrack(); void insertMediaInternal( const KURL::List&, PlaylistItem*, int options = 0 ); bool isAdvancedQuery( const QString &query ); void refreshNextTracks( int = -1 ); void removeItem( PlaylistItem*, bool = false ); bool saveState( QStringList& ); void setCurrentTrack( PlaylistItem* ); void setCurrentTrackPixmap( int state = -1 ); void showTagDialog( QPtrList items ); void sortQueuedItems(); void switchState( QStringList&, QStringList& ); void saveSelectedAsPlaylist(); void initStarPixmaps(); //engine observer functions void engineNewMetaData( const MetaBundle&, bool ); void engineStateChanged( Engine::State, Engine::State = Engine::Empty ); /// KListView Overloaded functions void contentsDropEvent ( QDropEvent* ); void contentsDragEnterEvent( QDragEnterEvent* ); void contentsDragMoveEvent ( QDragMoveEvent* ); void contentsDragLeaveEvent( QDragLeaveEvent* ); #ifdef PURIST //KListView imposes hand cursor so override it void contentsMouseMoveEvent( QMouseEvent *e ) { QListView::contentsMouseMoveEvent( e ); } #endif void customEvent( QCustomEvent* ); bool eventFilter( QObject*, QEvent* ); void paletteChange( const QPalette& ); void rename( QListViewItem*, int ); void setColumnWidth( int, int ); void setSorting( int, bool = true ); void viewportPaintEvent( QPaintEvent* ); void viewportResizeEvent( QResizeEvent* ); void appendToPreviousTracks( PlaylistItem *item ); void appendToPreviousAlbums( PlaylistAlbum *album ); void removeFromPreviousTracks( PlaylistItem *item = 0 ); void removeFromPreviousAlbums( PlaylistAlbum *album = 0 ); - typedef QMap AlbumMap; - typedef QMap ArtistAlbumMap; + typedef QMap AlbumMap; + typedef QMap ArtistAlbumMap; ArtistAlbumMap m_albums; uint m_startupTime_t; //QDateTime::currentDateTime().toTime_t as of startup uint m_oldestTime_t; //the createdate of the oldest song in the collection /// ATTRIBUTES PlaylistItem *m_currentTrack; //the track that is playing QListViewItem *m_marker; //track that has the drag/drop marker under it PlaylistItem *m_hoveredRating; //if the mouse is hovering over the rating of an item //NOTE these container types were carefully chosen QPtrList m_prevAlbums; //the previously played albums in Entire Albums mode PLItemList m_prevTracks; //the previous history PLItemList m_nextTracks; //the tracks to be played after the current track QString m_filter; QString m_prevfilter; QTimer *m_filtertimer; PLItemList m_itemsToChangeTagsFor; bool m_smartResizing; int m_firstColumn; int m_totalCount; int m_totalLength; int m_selCount; int m_selLength; int m_visCount; int m_visLength; Q_INT64 m_total; //for Favor Tracks bool m_itemCountDirty; KAction *m_undoButton; KAction *m_redoButton; KAction *m_clearButton; QDir m_undoDir; QStringList m_undoList; QStringList m_redoList; uint m_undoCounter; DynamicMode *m_dynamicMode; KURL::List m_queueList; PlaylistItem *m_stopAfterTrack; int m_stopAfterMode; bool m_showHelp; bool m_dynamicDirt; //So we don't call advanceDynamicTrack() on activate() bool m_queueDirt; //When queuing disabled items, we need to place the marker on the newly inserted item bool m_undoDirt; //Make sure we don't repopulate the playlist when dynamic mode and undo() QListViewItem *m_itemToReallyCenter; QListViewItem *m_renameItem; int m_renameColumn; QTimer *m_clicktimer; QListViewItem *m_itemToRename; QPoint m_clickPos; int m_columnToRename; QMap m_customSubmenuItem; QMap m_customIdItem; bool isLocked() const { return m_lockStack > 0; } /// stack counter for PLaylist::lock() and unlock() int m_lockStack; QString m_editOldTag; //text before inline editing ( the new tag is written only if it's changed ) std::vector m_columnFraction; QMap*> m_uniqueMap; int m_oldRandom; int m_oldRepeat; QString m_playlistName; bool m_proposeOverwriting; -}; -class MyAtomicString: public AtomicString -{ -public: - MyAtomicString() { } - MyAtomicString(const QString &string): AtomicString( string ) { } - MyAtomicString(const AtomicString &other): AtomicString( other ) { } - bool operator<(const AtomicString &other) const { return ptr() < other.ptr(); } + // indexing stuff + // An index of playlist items by some field. The index is backed by AtomicStrings, to avoid + // duplication thread-safely. + template + class Index : private QMap > + { + public: + // constructors take the PlaylistItem getter to index by + Index( FieldType (PlaylistItem::*getter)( ) const) + : m_getter( getter ), m_useGetter( true ) { }; + Index( const FieldType &(PlaylistItem::*refGetter)() const) + : m_refGetter( refGetter ), m_useGetter( false ) { }; + + // we specialize this method, below, for KURLs + AtomicString fieldString( const FieldType &field) { return AtomicString( field ); } + + AtomicString keyOf( const PlaylistItem &item) { + return m_useGetter ? fieldString( ( item.*m_getter ) () ) + : fieldString( ( item.*m_refGetter ) () ); + } + + bool contains( const FieldType &key ) { return contains( fieldString( key ) ); } + + // Just first match, or NULL + PlaylistItem *getFirst( const FieldType &field ) { + Iterator it = find( fieldString( field ) ); + return it == end() || it.data().isEmpty() ? 0 : it.data().getFirst(); + } + + void add( PlaylistItem *item ) { + QPtrList &row = operator[]( keyOf( *item ) ); // adds one if needed + if ( !row.containsRef(item) ) row.append( item ); + } + + void remove( PlaylistItem *item ) { + Iterator it = find( keyOf( *item ) ); + if (it != end()) { + while ( it.data().removeRef( item ) ) { }; + if ( it.data().isEmpty() ) erase( it ); + } + } + + private: + FieldType (PlaylistItem::*m_getter) () const; + const FieldType &(PlaylistItem::*m_refGetter) () const; + bool m_useGetter; // because a valid *member can be zero in C++ + }; + + Index m_urlIndex; + // TODO: we can convert m_unique to this, to remove some code and for uniformity and thread + // safety + // TODO: we should just store the url() as AtomicString, it will save headaches (e.g. at least a + // crash with multicore enabled traces back to KURL refcounting) + //Index m_uniqueIndex; }; class PlaylistAlbum { public: PLItemList tracks; int refcount; Q_INT64 total; //for Favor Tracks PlaylistAlbum(): refcount( 0 ), total( 0 ) { } }; /** * Iterator class that only edits visible items! Preferentially always use * this! Invisible items should not be operated on! To iterate over all * items use MyIt::All as the flags parameter. MyIt::All cannot be OR'd, * sorry. */ class PlaylistIterator : public QListViewItemIterator { public: PlaylistIterator( QListViewItem *item, int flags = 0 ) //QListViewItemIterator is not great and doesn't allow you to see everything if you //mask both Visible and Invisible :( instead just visible items are returned : QListViewItemIterator( item, flags == All ? 0 : flags | Visible ) {} PlaylistIterator( QListView *view, int flags = 0 ) : QListViewItemIterator( view, flags == All ? 0 : flags | Visible ) {} //FIXME! Dirty hack for enabled/disabled items. enum IteratorFlag { Visible = QListViewItemIterator::Visible, All = QListViewItemIterator::Invisible }; inline PlaylistItem *operator*() { return static_cast( QListViewItemIterator::operator*() ); } /// @return the next visible PlaylistItem after item static PlaylistItem *nextVisible( PlaylistItem *item ) { PlaylistIterator it( item ); return (*it == item) ? *static_cast(++it) : *it; } static PlaylistItem *prevVisible( PlaylistItem *item ) { PlaylistIterator it( item ); return (*it == item) ? *static_cast(--it) : *it; } }; +// Specialization of Index::fieldString for URLs +template<> +inline AtomicString Playlist::Index::fieldString( const KURL &url ) +{ + return AtomicString( url.url() ); +} + #endif //AMAROK_PLAYLIST_H + diff --git a/src/playlistitem.cpp b/src/playlistitem.cpp index 155b10e337..40c914e7d7 100644 --- a/src/playlistitem.cpp +++ b/src/playlistitem.cpp @@ -1,1127 +1,1140 @@ /*************************************************************************** playlistitem.cpp - description ------------------- begin : Die Dez 3 2002 copyright : (C) 2002 by Mark Kretschmann email : markey@web.de copyright : (C) 2005 by Alexandre Oliveira email : aleprj@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 "PlaylistItem" #include #include "amarok.h" #include "amarokconfig.h" #include "collectiondb.h" #include "debug.h" #include "enginecontroller.h" #include "playlist.h" #include "sliderwidget.h" #include "starmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistitem.h" double PlaylistItem::glowIntensity; QColor PlaylistItem::glowText = Qt::white; QColor PlaylistItem::glowBase = Qt::white; bool PlaylistItem::s_pixmapChanged = false; PlaylistItem::PlaylistItem( QListView *listview, QListViewItem *item ) : KListViewItem( listview, item ) , m_album( 0 ) { KListViewItem::setVisible( false ); } PlaylistItem::PlaylistItem( const MetaBundle &bundle, QListViewItem *lvi, bool enabled ) : MetaBundle( bundle ), KListViewItem( lvi->listView(), lvi->itemAbove() ) , m_album( 0 ) , m_deleteAfterEdit( false ) , m_isBeingRenamed( false ) , m_isNew( true ) { setDragEnabled( true ); + Playlist::instance()->m_urlIndex.add( this ); if( !uniqueId().isEmpty() ) Playlist::instance()->addToUniqueMap( uniqueId(), this ); + refAlbum(); incrementCounts(); incrementLengths(); filter( listView()->m_filter ); listView()->countChanged(); setAllCriteriaEnabled( enabled ); } PlaylistItem::~PlaylistItem() { if( isEmpty() ) //constructed with the generic constructor, for PlaylistLoader's marker item return; decrementCounts(); decrementLengths(); derefAlbum(); listView()->countChanged(); if( listView()->m_hoveredRating == this ) listView()->m_hoveredRating = 0; Playlist::instance()->removeFromUniqueMap( uniqueId(), this ); + Playlist::instance()->m_urlIndex.remove(this); + + } ///////////////////////////////////////////////////////////////////////////////////// // PUBLIC METHODS ///////////////////////////////////////////////////////////////////////////////////// void PlaylistItem::setText( int column, const QString &text ) { if( column == Rating ) setExactText( column, QString::number( int( text.toFloat() * 2 ) ) ); else setExactText( column, text ); } QString PlaylistItem::text( int column ) const { if( column == Title && listView()->header()->sectionSize( Filename ) ) //don't show the filename twice return exactText( column ); else switch ( column ) { case Artist: case Composer: case Album: case Genre: case Comment: return exactText( column ); //HACK case Rating: return isEditing( column ) ? exactText( column ) : prettyText( column ); default: { if( column != Title && isEditing( column ) ) return editingText(); else return prettyText( column ); } } } void PlaylistItem::aboutToChange( const QValueList &columns ) { - bool totals = false, ref = false, length = false; + bool totals = false, ref = false, length = false, url = false; for( int i = 0, n = columns.count(); i < n; ++i ) switch( columns[i] ) { case Length: length = true; break; case Artist: case Album: ref = true; //note, no breaks - case Track: case Rating: case Score: case LastPlayed: totals = true; + case Track: case Rating: case Score: case LastPlayed: + totals = true; break; + case Filename: case Directory: url = true; break; } if ( length ) decrementLengths(); if( totals ) decrementTotals(); if( ref ) derefAlbum(); + if ( url ) + Playlist::instance()->m_urlIndex.remove(this); } void PlaylistItem::reactToChanges( const QValueList &columns ) { MetaBundle::reactToChanges(columns); - bool totals = false, ref = false, length = false; + bool totals = false, ref = false, length = false, url = false; for( int i = 0, n = columns.count(); i < n; ++i ) { if( columns[i] == Mood ) moodbar().reset(); if ( !length && columns[i] == Length ) { length = true; incrementLengths(); listView()->countChanged(); } switch( columns[i] ) { case Artist: case Album: ref = true; //note, no breaks - case Track: case Rating: case Score: case LastPlayed: totals = true; - default: updateColumn( columns[i] ); + case Track: case Rating: case Score: case LastPlayed: + totals = true; break; + case Filename: case Directory: url = true; } + updateColumn( columns[i] ); } + if ( url ) + Playlist::instance()->m_urlIndex.add(this); if( ref ) refAlbum(); if( totals ) incrementTotals(); } void PlaylistItem::filter( const QString &expression ) { setVisible( matchesExpression( expression, listView()->visibleColumns() ) ); } bool PlaylistItem::isCurrent() const { return this == listView()->currentTrack(); } bool PlaylistItem::isQueued() const { return queuePosition() != -1; } int PlaylistItem::queuePosition() const { return listView()->m_nextTracks.findRef( this ); } void PlaylistItem::setEnabled() { m_enabled = m_filestatusEnabled && m_dynamicEnabled; setDropEnabled( m_enabled ); // this forbids items to be dropped into a history queue. update(); } void PlaylistItem::setDynamicEnabled( bool enabled ) { m_dynamicEnabled = enabled; setEnabled(); } void PlaylistItem::setFilestatusEnabled( bool enabled ) { m_filestatusEnabled = enabled; checkExists(); setEnabled(); } void PlaylistItem::setAllCriteriaEnabled( bool enabled ) { m_filestatusEnabled = enabled; m_dynamicEnabled = enabled; checkExists(); setEnabled(); } void PlaylistItem::setSelected( bool selected ) { if( isEmpty() ) return; if( isVisible() ) { const bool prevSelected = isSelected(); KListViewItem::setSelected( selected ); if( prevSelected && !isSelected() ) { listView()->m_selCount--; listView()->m_selLength -= length(); listView()->countChanged(); } else if( !prevSelected && isSelected() ) { listView()->m_selCount++; listView()->m_selLength += length(); listView()->countChanged(); } } } void PlaylistItem::setVisible( bool visible ) { if( isEmpty() ) return; if( !visible && isSelected() ) { listView()->m_selCount--; listView()->m_selLength -= length(); KListViewItem::setSelected( false ); listView()->countChanged(); } const bool prevVisible = isVisible(); KListViewItem::setVisible( visible ); if( prevVisible && !isVisible() ) { listView()->m_visCount--; listView()->m_visLength -= length(); listView()->countChanged(); decrementTotals(); } else if( !prevVisible && isVisible() ) { listView()->m_visCount++; listView()->m_visLength += length(); listView()->countChanged(); incrementTotals(); } } void PlaylistItem::setEditing( int column ) { switch( column ) { case Title: case Artist: case Composer: case Album: case Genre: case Comment: setExactText( column, editingText() ); break; case Year: m_year = -1; break; case DiscNumber: m_discNumber = -1; break; case Track: m_track = -1; break; case Bpm: m_bpm = -1; break; case Length: m_length = -1; break; case Bitrate: m_bitrate = -1; break; case SampleRate: m_sampleRate = -1; break; case Score: m_score = -1; break; case Rating: m_rating = -1; break; case PlayCount: m_playCount = -1; break; case LastPlayed: m_lastPlay = 1; break; default: warning() << "Tried to set the text of an immutable or nonexistent column!" << endl; } update(); } bool PlaylistItem::isEditing( int column ) const { switch( column ) { case Title: case Artist: case Composer: case Album: case Genre: case Comment: //FIXME fix this hack! return exactText( column ) == editingText(); case Year: return m_year == -1; case DiscNumber: return m_discNumber == -1; case Track: return m_track == -1; case Bpm: return m_bpm == -1; case Length: return m_length == -1; case Bitrate: return m_bitrate == -1; case SampleRate: return m_sampleRate == -1; case Score: return m_score == -1; case Rating: return m_rating == -1; case PlayCount: return m_playCount == -1; case LastPlayed: return m_lastPlay == 1; default: return false; } } bool PlaylistItem::anyEditing() const { for( int i = 0; i < NUM_COLUMNS; i++ ) { if( isEditing( i ) ) return true; } return false; } int PlaylistItem::ratingAtPoint( int x ) //static { Playlist* const pl = Playlist::instance(); x -= pl->header()->sectionPos( Rating ); return kClamp( ( x - 1 ) / ( StarManager::instance()->getGreyStar()->width() + pl->itemMargin() ) + 1, 1, 5 ) * 2; } int PlaylistItem::ratingColumnWidth() //static { return StarManager::instance()->getGreyStar()->width() * 5 + Playlist::instance()->itemMargin() * 6; } void PlaylistItem::update() const { listView()->repaintItem( this ); } void PlaylistItem::updateColumn( int column ) const { const QRect r = listView()->itemRect( this ); if( !r.isValid() ) return; listView()->viewport()->update( listView()->header()->sectionPos( column ) - listView()->contentsX() + 1, r.y() + 1, listView()->header()->sectionSize( column ) - 2, height() - 2 ); } bool PlaylistItem::operator== ( const PlaylistItem & item ) const { return item.url() == this->url(); } bool PlaylistItem::operator< ( const PlaylistItem & item ) const { return item.url() < this->url(); } PlaylistItem* PlaylistItem::nextInAlbum() const { if( !m_album ) return 0; const int index = m_album->tracks.findRef( this ); if( index == int(m_album->tracks.count() - 1) ) return 0; if( index != -1 ) return m_album->tracks.at( index + 1 ); if( track() ) for( int i = 0, n = m_album->tracks.count(); i < n; ++i ) if( m_album->tracks.at( i )->track() > track() ) return m_album->tracks.at( i ); else for( QListViewItemIterator it( const_cast(this), QListViewItemIterator::Visible ); *it; ++it ) #define pit static_cast( *it ) if( pit != this && pit->m_album == m_album && !pit->track() ) return pit; #undef pit return 0; } PlaylistItem* PlaylistItem::prevInAlbum() const { if( !m_album ) return 0; const int index = m_album->tracks.findRef( this ); if( index == 0 ) return 0; if( index != -1 ) return m_album->tracks.at( index - 1 ); if( track() ) for( int i = m_album->tracks.count() - 1; i >= 0; --i ) if( m_album->tracks.at( i )->track() && m_album->tracks.at( i )->track() < track() ) return m_album->tracks.at( i ); else for( QListViewItemIterator it( const_cast(this), QListViewItemIterator::Visible ); *it; --it ) #define pit static_cast( *it ) if( pit != this && pit->m_album == m_album && !pit->track() ) return pit; #undef pit return 0; } ///////////////////////////////////////////////////////////////////////////////////// // PRIVATE METHODS ///////////////////////////////////////////////////////////////////////////////////// int PlaylistItem::compare( QListViewItem *i, int col, bool ascending ) const { #define i static_cast(i) if( Playlist::instance()->dynamicMode() && (isEnabled() != i->isEnabled()) ) return isEnabled() ? 1 : -1; //damn C++ and its lack of operator<=> #define cmp(a,b) ( (a < b ) ? -1 : ( a > b ) ? 1 : 0 ) switch( col ) { case Track: return cmp( track(), i->track() ); case Score: return cmp( score(), i->score() ); case Rating: return cmp( rating(), i->rating() ); case Length: return cmp( length(), i->length() ); case PlayCount: return cmp( playCount(), i->playCount() ); case LastPlayed: return cmp( lastPlay(), i->lastPlay() ); case Bitrate: return cmp( bitrate(), i->bitrate() ); case Bpm: return cmp( bpm(), i->bpm() ); case Filesize: return cmp( filesize(), i->filesize() ); case Mood: return cmp( moodbar_const().hueSort(), i->moodbar_const().hueSort() ); case Year: if( year() == i->year() ) return compare( i, Artist, ascending ); return cmp( year(), i->year() ); case DiscNumber: if( discNumber() == i->discNumber() ) return compare( i, Track, true ) * (ascending ? 1 : -1); return cmp( discNumber(), i->discNumber() ); } #undef cmp #undef i QString a = text( col ).lower(); QString b = i->text( col ).lower(); switch( col ) { case Type: a = a.rightJustify( b.length(), '0' ); b = b.rightJustify( a.length(), '0' ); break; case Artist: if( a == b ) //if same artist, try to sort by album return compare( i, Album, ascending ); else { if( a.startsWith( "the ", false ) ) a = a.mid( 4 ); if( b.startsWith( "the ", false ) ) b = b.mid( 4 ); } break; case Album: if( a == b ) //if same album, try to sort by track //TODO only sort in ascending order? return compare( i, DiscNumber, true ) * (ascending ? 1 : -1); break; } return QString::localeAwareCompare( a, b ); } void PlaylistItem::paintCell( QPainter *painter, const QColorGroup &cg, int column, int width, int align ) { //TODO add spacing on either side of items //p->translate( 2, 0 ); width -= 3; // Don't try to draw if width or height is 0, as this crashes Qt if( !painter || !listView() || width <= 0 || height() == 0 ) return; static const QImage currentTrackLeft = locate( "data", "amarok/images/currenttrack_bar_left.png" ); static const QImage currentTrackMid = locate( "data", "amarok/images/currenttrack_bar_mid.png" ); static const QImage currentTrackRight = locate( "data", "amarok/images/currenttrack_bar_right.png" ); if( column == Mood && !moodbar().dataExists() ) moodbar().load(); // Only has an effect the first time // The moodbar column can have text in it, like "Calculating". // moodbarType is 0 if column != Mood, 1 if we're displaying // a moodbar, and 2 if we're displaying text const int moodbarType = column != Mood ? 0 : moodbar().state() == Moodbar::Loaded ? 1 : 2; const QString colText = text( column ); const bool isCurrent = this == listView()->currentTrack(); QPixmap buf( width, height() ); QPainter p( &buf, true ); if( isCurrent ) { static paintCacheItem paintCache[NUM_COLUMNS]; // Convert intensity to string, so we can use it as a key const QString colorKey = QString::number( glowIntensity ); const bool cacheValid = paintCache[column].width == width && paintCache[column].height == height() && paintCache[column].text == colText && paintCache[column].font == painter->font() && paintCache[column].color == glowBase && paintCache[column].selected == isSelected() && !s_pixmapChanged; // If any parameter changed, we must regenerate all pixmaps if ( !cacheValid ) { for( int i = 0; i < NUM_COLUMNS; ++i) paintCache[i].map.clear(); s_pixmapChanged = false; } // Determine if we need to repaint the pixmap, or paint from cache if ( paintCache[column].map.find( colorKey ) == paintCache[column].map.end() ) { // Update painting cache paintCache[column].width = width; paintCache[column].height = height(); paintCache[column].text = colText; paintCache[column].font = painter->font(); paintCache[column].color = glowBase; paintCache[column].selected = isSelected(); QColor bg; if( isSelected() ) bg = listView()->colorGroup().highlight(); else bg = isAlternate() ? listView()->alternateBackground() : listView()->viewport()->backgroundColor(); buf.fill( bg ); // Draw column divider line p.setPen( listView()->viewport()->colorGroup().mid() ); p.drawLine( width - 1, 0, width - 1, height() - 1 ); // Here we draw the background bar graphics for the current track: // // Illustration of design, L = Left, M = Middle, R = Right: // int leftOffset = 0; int rightOffset = 0; int margin = listView()->itemMargin(); const float colorize = 0.8; const double intensity = 1.0 - glowIntensity * 0.021; // Left part if( column == listView()->m_firstColumn ) { QImage tmpImage = currentTrackLeft.smoothScale( 1, height(), QImage::ScaleMax ); KIconEffect::colorize( tmpImage, glowBase, colorize ); imageTransparency( tmpImage, intensity ); p.drawImage( 0, 0, tmpImage, 0, 0, tmpImage.width() - 1 ); //HACK leftOffset = tmpImage.width() - 1; //HACK Subtracting 1, to work around the black line bug margin += 6; } // Right part else if( column == Playlist::instance()->mapToLogicalColumn( Playlist::instance()->numVisibleColumns() - 1 ) ) { QImage tmpImage = currentTrackRight.smoothScale( 1, height(), QImage::ScaleMax ); KIconEffect::colorize( tmpImage, glowBase, colorize ); imageTransparency( tmpImage, intensity ); p.drawImage( width - tmpImage.width(), 0, tmpImage ); rightOffset = tmpImage.width(); margin += 6; } // Middle part // Here we scale the one pixel wide middel image to stretch to the full column width. QImage tmpImage = currentTrackMid.copy(); KIconEffect::colorize( tmpImage, glowBase, colorize ); imageTransparency( tmpImage, intensity ); tmpImage = tmpImage.smoothScale( width - leftOffset - rightOffset, height() ); p.drawImage( leftOffset, 0, tmpImage ); // Draw the pixmap, if present int leftMargin = margin; if ( pixmap( column ) ) { p.drawPixmap( leftMargin, height() / 2 - pixmap( column )->height() / 2, *pixmap( column ) ); leftMargin += pixmap( column )->width() + 2; } if( align != Qt::AlignCenter ) align |= Qt::AlignVCenter; if( column != Rating && moodbarType != 1 ) { // Draw the text static QFont font; static int minbearing = 1337 + 666; if( minbearing == 2003 || font != painter->font() ) { font = painter->font(); minbearing = painter->fontMetrics().minLeftBearing() + painter->fontMetrics().minRightBearing(); } const bool italic = font.italic(); int state = EngineController::engine()->state(); if( state == Engine::Playing || state == Engine::Paused ) font.setItalic( !italic ); p.setFont( font ); p.setPen( cg.highlightedText() ); // paint.setPen( glowText ); const int _width = width - leftMargin - margin + minbearing - 1; // -1 seems to be necessary const QString _text = KStringHandler::rPixelSqueeze( colText, painter->fontMetrics(), _width ); p.drawText( leftMargin, 0, _width, height(), align, _text ); font.setItalic( italic ); p.setFont( font ); } paintCache[column].map[colorKey] = buf; } else p.drawPixmap( 0, 0, paintCache[column].map[colorKey] ); if( column == Rating ) drawRating( &p ); if( moodbarType == 1 ) drawMood( &p, width, height() ); } else { const QColorGroup _cg = ( !exists() || !isEnabled() ) ? listView()->palette().disabled() : listView()->palette().active(); QColor bg = isSelected() ? _cg.highlight() : isAlternate() ? listView()->alternateBackground() : listView()->viewport()->backgroundColor(); #if KDE_IS_VERSION( 3, 3, 91 ) if( listView()->shadeSortColumn() && !isSelected() && listView()->columnSorted() == column ) { /* from klistview.cpp Copyright (C) 2000 Reginald Stadlbauer Copyright (C) 2000,2003 Charles Samuels Copyright (C) 2000 Peter Putzer */ if ( bg == Qt::black ) bg = QColor(55, 55, 55); // dark gray else { int h,s,v; bg.hsv(&h, &s, &v); if ( v > 175 ) bg = bg.dark(104); else bg = bg.light(120); } } #endif const QColor textc = isSelected() ? _cg.highlightedText() : _cg.text(); buf.fill( bg ); // Draw column divider line if( !isSelected() ) { p.setPen( listView()->viewport()->colorGroup().mid() ); p.drawLine( width - 1, 0, width - 1, height() - 1 ); } // Draw the pixmap, if present int margin = listView()->itemMargin(), leftMargin = margin; if ( pixmap( column ) ) { p.drawPixmap( leftMargin, height() / 2 - pixmap( column )->height() / 2, *pixmap( column ) ); leftMargin += pixmap( column )->width(); } if( align != Qt::AlignCenter ) align |= Qt::AlignVCenter; if( column == Rating ) drawRating( &p ); else if( moodbarType == 1 ) drawMood( &p, width, height() ); else { // Draw the text static QFont font; static int minbearing = 1337 + 666; //can be 0 or negative, 2003 is less likely if( minbearing == 2003 || font != painter->font() ) { font = painter->font(); //getting your bearings can be expensive, so we cache them minbearing = painter->fontMetrics().minLeftBearing() + painter->fontMetrics().minRightBearing(); } p.setFont( font ); p.setPen( ( m_isNew && isEnabled() && !isSelected() ) ? AmarokConfig::newPlaylistItemsColor() : textc ); const int _width = width - leftMargin - margin + minbearing - 1; // -1 seems to be necessary const QString _text = KStringHandler::rPixelSqueeze( colText, painter->fontMetrics(), _width ); p.drawText( leftMargin, 0, _width, height(), align, _text ); } } /// Track action symbols const int queue = listView()->m_nextTracks.findRef( this ) + 1; const bool stop = ( this == listView()->m_stopAfterTrack ); const bool repeat = Amarok::repeatTrack() && isCurrent; const uint num = ( queue ? 1 : 0 ) + ( stop ? 1 : 0 ) + ( repeat ? 1 : 0 ); static const QPixmap pixstop = Amarok::getPNG( "currenttrack_stop_small" ), pixrepeat = Amarok::getPNG( "currenttrack_repeat_small" ); //figure out if we are in the actual physical first column if( column == listView()->m_firstColumn && num ) { //margin, height const uint m = 2, h = height() - m; const QString str = QString::number( queue ); const uint qw = painter->fontMetrics().width( str ), sw = pixstop.width(), rw = pixrepeat.width(), qh = painter->fontMetrics().height(), sh = pixstop.height(), rh = pixrepeat.height(); //maxwidth const uint mw = kMax( qw, kMax( rw, sw ) ); //width of first & second column of pixmaps const uint w1 = ( num == 3 ) ? kMax( qw, rw ) : ( num == 2 && isCurrent ) ? kMax( repeat ? rw : 0, kMax( stop ? sw : 0, queue ? qw : 0 ) ) : ( num == 2 ) ? qw : queue ? qw : repeat ? rw : stop ? sw : 0, w2 = ( num == 3 ) ? sw : ( num == 2 && !isCurrent ) ? sw : 0; //phew //ellipse width, total width const uint ew = 16, tw = w1 + w2 + m * ( w2 ? 2 : 1 ); p.setBrush( cg.highlight() ); p.setPen( cg.highlight().dark() ); //TODO blend with background color p.drawEllipse( width - tw - ew/2, m / 2, ew, h ); p.drawRect( width - tw, m / 2, tw, h ); p.setPen( cg.highlight() ); p.drawLine( width - tw, m/2 + 1, width - tw, h - m/2 ); int x = width - m - mw, y = height() / 2, tmp = 0; const bool multi = ( isCurrent && num >= 2 ); if( queue ) { //draw the shadowed inner text //NOTE we can't set an arbituary font size or family, these settings are already optional //and user defaults should also take presidence if no playlist font has been selected //const QFont smallFont( "Arial", (playNext > 9) ? 9 : 12 ); //p->setFont( smallFont ); //TODO the shadow is hard to do well when using a dark font color //TODO it also looks cluttered for small font sizes //p->setPen( cg.highlightedText().dark() ); //p->drawText( width - w + 2, 3, w, h-1, Qt::AlignCenter, str ); if( !multi ) tmp = -(qh / 2); y += tmp; p.setPen( cg.highlightedText() ); p.drawText( x, y, -x + width, multi ? h/2 : qh, Qt::AlignCenter, str ); y -= tmp; if( isCurrent ) y -= height() / 2; else x -= m + w2; } if( repeat ) { if( multi ) tmp = (h/2 - rh)/2 + ( num == 2 && stop ? 0 : 1 ); else tmp = -(rh / 2); y += tmp; p.drawPixmap( x, y, pixrepeat ); y -= tmp; if( num == 3 ) { x -= m + w2 + 2; y = height() / 2; } else y -= height() / 2; } if( stop ) { if( multi && num != 3 ) tmp = m + (h/2 - sh)/2; else tmp = -(sh / 2); y += tmp; p.drawPixmap( x, y, pixstop ); y -= tmp; } } if( this != listView()->currentTrack() && !isSelected() ) { p.setPen( QPen( cg.mid(), 0, Qt::SolidLine ) ); p.drawLine( width - 1, 0, width - 1, height() - 1 ); } p.end(); painter->drawPixmap( 0, 0, buf ); } void PlaylistItem::drawRating( QPainter *p ) { int gray = 0; if( this == listView()->m_hoveredRating || ( isSelected() && listView()->m_selCount > 1 && listView()->m_hoveredRating && listView()->m_hoveredRating->isSelected() ) ) { const int pos = listView()->viewportToContents( listView()->viewport()->mapFromGlobal( QCursor::pos() ) ).x(); gray = ratingAtPoint( pos ); } drawRating( p, ( rating() + 1 ) / 2, gray / 2, rating() % 2 ); } void PlaylistItem::drawRating( QPainter *p, int stars, int greystars, bool half ) { int i = 1, x = 1; const int y = height() / 2 - StarManager::instance()->getGreyStar()->height() / 2; if( half ) i++; //We use multiple pre-colored stars instead of coloring here to keep things speedy for(; i <= stars; ++i ) { bitBlt( p->device(), x, y, StarManager::instance()->getStar( stars ) ); x += StarManager::instance()->getGreyStar()->width() + listView()->itemMargin(); } if( half ) { bitBlt( p->device(), x, y, StarManager::instance()->getHalfStar( stars ) ); x += StarManager::instance()->getGreyStar()->width() + listView()->itemMargin(); } for(; i <= greystars; ++i ) { bitBlt( p->device(), x, y, StarManager::instance()->getGreyStar() ); x += StarManager::instance()->getGreyStar()->width() + listView()->itemMargin(); } } #define MOODBAR_SPACING 2 // The distance from the moodbar pixmap to each side void PlaylistItem::drawMood( QPainter *p, int width, int height ) { // In theory, if AmarokConfig::showMoodbar() == false, then the // moodbar column should be hidden and we shouldn't be here. if( !AmarokConfig::showMoodbar() ) return; // Due to the logic of the calling code, this should always return true if( moodbar().dataExists() ) { QPixmap mood = moodbar().draw( width - MOODBAR_SPACING*2, height - MOODBAR_SPACING*2 ); p->drawPixmap( MOODBAR_SPACING, MOODBAR_SPACING, mood ); } else moodbar().load(); // This only has any effect the first time it's run // We don't have to listen for the jobEvent() signal since we // inherit MetaBundle, and the moodbar lets the MetaBundle know // about new data directly via moodbarJobEvent() below. } // This is run when a job starts or finishes void PlaylistItem::moodbarJobEvent( int newState ) { (void) newState; // want to redraw nomatter what the new state is if( AmarokConfig::showMoodbar() ) repaint(); // Don't automatically resort because it's annoying } void PlaylistItem::setup() { KListViewItem::setup(); // We make the current track item a bit taller than ordinary items if( this == listView()->currentTrack() ) setHeight( int( float( listView()->fontMetrics().height() ) * 1.53 ) ); } void PlaylistItem::paintFocus( QPainter* p, const QColorGroup& cg, const QRect& r ) { if( this != listView()->currentTrack() ) KListViewItem::paintFocus( p, cg, r ); } const QString &PlaylistItem::editingText() { static const QString text = i18n( "Writing tag..." ); return text; } /** * Changes the transparency (alpha component) of an image. * @param image Image to be manipulated. Must be true color (8 bit per channel). * @param factor > 1.0 == more transparency, < 1.0 == less transparency. */ void PlaylistItem::imageTransparency( QImage& image, float factor ) //static { uint *data = reinterpret_cast( image.bits() ); const int pixels = image.width() * image.height(); uint table[256]; register int c; // Precalculate lookup table for( int i = 0; i < 256; ++i ) { c = int( double( i ) * factor ); if( c > 255 ) c = 255; table[i] = c; } // Process all pixels. Highly optimized. for( int i = 0; i < pixels; ++i ) { c = data[i]; // Memory access is slow, so do it only once data[i] = qRgba( qRed( c ), qGreen( c ), qBlue( c ), table[qAlpha( c )] ); } } AtomicString PlaylistItem::artist_album() const { static const AtomicString various_artist = QString( "Various Artists (INTERNAL) [ASDF!]" ); if( compilation() == CompilationYes ) return various_artist; else return artist(); } void PlaylistItem::refAlbum() { if( Amarok::entireAlbums() ) { if( listView()->m_albums[artist_album()].find( album() ) == listView()->m_albums[artist_album()].end() ) listView()->m_albums[artist_album()][album()] = new PlaylistAlbum; m_album = listView()->m_albums[artist_album()][album()]; m_album->refcount++; } } void PlaylistItem::derefAlbum() { if( Amarok::entireAlbums() && m_album ) { m_album->refcount--; if( !m_album->refcount ) { if (!listView()->m_prevAlbums.removeRef( m_album )) warning() << "Unable to remove album reference from " << "listView.m_prevAlbums" << endl; listView()->m_albums[artist_album()].remove( album() ); if( listView()->m_albums[artist_album()].isEmpty() ) listView()->m_albums.remove( artist_album() ); delete m_album; } } } void PlaylistItem::incrementTotals() { if( Amarok::entireAlbums() && m_album ) { const uint prevCount = m_album->tracks.count(); if( !track() || !m_album->tracks.count() || ( m_album->tracks.getLast()->track() && m_album->tracks.getLast()->track() < track() ) ) m_album->tracks.append( this ); else for( int i = 0, n = m_album->tracks.count(); i < n; ++i ) if( m_album->tracks.at(i)->track() > track() || !m_album->tracks.at(i)->track() ) { m_album->tracks.insert( i, this ); break; } const Q_INT64 prevTotal = m_album->total; Q_INT64 total = m_album->total * prevCount; total += totalIncrementAmount(); m_album->total = Q_INT64( double( total + 0.5 ) / m_album->tracks.count() ); if( listView()->m_prevAlbums.findRef( m_album ) == -1 ) listView()->m_total = listView()->m_total - prevTotal + m_album->total; } else if( listView()->m_prevTracks.findRef( this ) == -1 ) listView()->m_total += totalIncrementAmount(); } void PlaylistItem::decrementTotals() { if( Amarok::entireAlbums() && m_album ) { const Q_INT64 prevTotal = m_album->total; Q_INT64 total = m_album->total * m_album->tracks.count(); if (!m_album->tracks.removeRef( this )) warning() << "Unable to remove myself from m_album" << endl; total -= totalIncrementAmount(); m_album->total = Q_INT64( double( total + 0.5 ) / m_album->tracks.count() ); if( listView()->m_prevAlbums.findRef( m_album ) == -1 ) listView()->m_total = listView()->m_total - prevTotal + m_album->total; } else if( listView()->m_prevTracks.findRef( this ) == -1 ) listView()->m_total -= totalIncrementAmount(); } int PlaylistItem::totalIncrementAmount() const { switch( AmarokConfig::favorTracks() ) { case AmarokConfig::EnumFavorTracks::Off: return 0; case AmarokConfig::EnumFavorTracks::HigherScores: return score() > 0.f ? static_cast( score() ) : 50; case AmarokConfig::EnumFavorTracks::HigherRatings: return rating() ? rating() : 5; // 2.5 case AmarokConfig::EnumFavorTracks::LessRecentlyPlayed: { if( lastPlay() ) return listView()->m_startupTime_t - lastPlay(); else if( listView()->m_oldestTime_t ) return ( listView()->m_startupTime_t - listView()->m_oldestTime_t ) * 2; else return listView()->m_startupTime_t - 1058652000; //july 20, 2003, when Amarok was first released. } default: return 0; } } void PlaylistItem::incrementCounts() { listView()->m_totalCount++; if( isSelected() ) { listView()->m_selCount++; } if( isVisible() ) { listView()->m_visCount++; incrementTotals(); } } void PlaylistItem::decrementCounts() { listView()->m_totalCount--; if( isSelected() ) { listView()->m_selCount--; } if( isVisible() ) { listView()->m_visCount--; decrementTotals(); } } void PlaylistItem::incrementLengths() { listView()->m_totalLength += length(); if( isSelected() ) { listView()->m_selLength += length(); } if( isVisible() ) { listView()->m_visLength += length(); } } void PlaylistItem::decrementLengths() { listView()->m_totalLength -= length(); if( isSelected() ) { listView()->m_selLength -= length(); } if( isVisible() ) { listView()->m_visLength -= length(); } } diff --git a/src/playlistloader.cpp b/src/playlistloader.cpp index 1b142db1a1..8f384d5e98 100644 --- a/src/playlistloader.cpp +++ b/src/playlistloader.cpp @@ -1,1123 +1,1113 @@ // Author: Max Howell (C) Copyright 2003-4 // Author: Mark Kretschmann (C) Copyright 2004 // .ram file support from Kaffeine 0.5, Copyright (C) 2004 by Jürgen Kofler (GPL 2 or later) // .asx file support added by Michael Seiwert Copyright (C) 2006 // .asx file support from Kaffeine, Copyright (C) 2004-2005 by Jürgen Kofler (GPL 2 or later) // .smil file support from Kaffeine 0.7 // .pls parser (C) Copyright 2005 by Michael Buesch // .xspf file support added by Mattias Fliesberg Copyright (C) 2006 // Copyright: See COPYING file that comes with this distribution // ///For pls and m3u specifications see: ///http://forums.winamp.com/showthread.php?s=dbec47f3a05d10a3a77959f17926d39c&threadid=65772 #define DEBUG_PREFIX "PlaylistLoader" #include "amarok.h" #include "collectiondb.h" #include "debug.h" #include "enginecontroller.h" #include "mountpointmanager.h" #include "mydirlister.h" #include "playlist.h" #include "playlistbrowser.h" #include "playlistitem.h" #include "playlistloader.h" #include "statusbar.h" #include "contextbrowser.h" #include "xspfplaylist.h" #include //::recurse() #include //::recurse() #include //::loadPlaylist() #include #include #include #include //::loadPlaylist() #include #include #include //TODO playlists within playlists, local or remote are legal entries in m3u and pls //TODO directories from inside playlists struct XMLData { MetaBundle bundle; int queue; bool stopafter; bool dynamicdisabled; bool filestatusdisabled; XMLData(): queue(-1), stopafter(false), dynamicdisabled(false), filestatusdisabled(false) { } }; class TagsEvent : public QCustomEvent { public: TagsEvent( const QValueList &x ) : QCustomEvent( 1001 ), xml( QDeepCopy >( x ) ) { } TagsEvent( const BundleList &bees ) : QCustomEvent( 1000 ), bundles( QDeepCopy( bees ) ) { for( BundleList::Iterator it = bundles.begin(), end = bundles.end(); it != end; ++it ) { (*it).detach(); /// @see MetaBundle for explanation of audioproperties < 0 if( (*it).length() <= 0 || (*it).bitrate() <= 0 ) (*it).readTags( TagLib::AudioProperties::Fast, 0 ); } } QValueList xml; BundleList bundles; }; UrlLoader::UrlLoader( const KURL::List &urls, QListViewItem *after, int options ) : ThreadManager::DependentJob( Playlist::instance(), "UrlLoader" ) , m_markerListViewItem( new PlaylistItem( Playlist::instance(), after ) ) , m_playFirstUrl( options & (Playlist::StartPlay | Playlist::DirectPlay) ) , m_coloring( options & Playlist::Colorize ) , m_options( options ) , m_block( "UrlLoader" ) , m_oldQueue( Playlist::instance()->m_nextTracks ) , m_xmlSource( 0 ) { connect( this, SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ), Playlist::instance(), SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ) ); Playlist::instance()->lock(); // prevent user removing items as this could be bad Amarok::OverrideCursor cursor; setDescription( i18n("Populating playlist") ); Amarok::StatusBar::instance()->newProgressOperation( this ) .setDescription( m_description ) .setStatus( i18n("Preparing") ) .setAbortSlot( this, SLOT(abort()) ) .setTotalSteps( 100 ); foreachType( KURL::List, urls ) { const KURL url = Amarok::mostLocalURL( *it ); // FIXME: url needs detach()ing const QString protocol = url.protocol(); if( protocol == "seek" ) continue; else if( ContextBrowser::hasContextProtocol( url ) ) { DEBUG_BLOCK debug() << "context expandurl" << endl; m_URLs += ContextBrowser::expandURL( url ); } else if( !MetaBundle::isKioUrl( url ) ) { m_URLs += url; } else if( protocol == "file" ) { if( QFileInfo( url.path() ).isDir() ) m_URLs += recurse( url ); else m_URLs += url; } // Note: remove for kde 4 - we don't need to be hacking around KFileDialog, // it has been fixed for kde 3.5.3 else if( protocol == "media" || url.url().startsWith( "system:/media/" ) ) { QString path = url.path( -1 ); if( url.url().startsWith( "system:/media/" ) ) path = path.mid( 6 ); // url looks like media:/device/path DCOPRef mediamanager( "kded", "mediamanager" ); QString device = path.mid( 1 ); // remove first slash const int slash = device.find( '/' ); const QString filePath = device.mid( slash ); // extract relative path device = device.left( slash ); // extract device DCOPReply reply = mediamanager.call( "properties(QString)", device ); if( reply.isValid() ) { const QStringList properties = reply; // properties[6] is the mount point KURL localUrl = KURL( properties[6] + filePath ); // add urls if( QFileInfo( localUrl.path() ).isDir() ) m_URLs += recurse( localUrl ); else m_URLs += localUrl; } } else if( PlaylistFile::isPlaylistFile( url ) ) { debug() << "remote playlist" << endl; new RemotePlaylistFetcher( url, after, m_options ); m_playFirstUrl = false; } else { // this is the best way I found for recursing if required // and not recusring if not required const KURL::List urls = recurse( url ); // recurse only works on directories, else it swallows the URL if( urls.isEmpty() ) m_URLs += url; else m_URLs += urls; } } } UrlLoader::~UrlLoader() { if( Playlist::instance() ) { Playlist::instance()->unlock(); delete m_markerListViewItem; } delete m_xmlSource; } bool UrlLoader::doJob() { setProgressTotalSteps( m_URLs.count() ); KURL::List urls; for( for_iterators( KURL::List, m_URLs ); it != end && !isAborted(); ++it ) { const KURL &url = *it; incrementProgress(); switch( PlaylistFile::format( url.fileName() ) ) { case PlaylistFile::XML: loadXml( url ); break; default: { PlaylistFile playlist( url.path() ); if( !playlist.isError() ) QApplication::postEvent( this, new TagsEvent( playlist.bundles()) ); else m_badURLs += url; } break; case PlaylistFile::NotPlaylist: (EngineController::canDecode( url ) ? urls : m_badURLs) += url; } if( urls.count() == OPTIMUM_BUNDLE_COUNT || it == last ) { QApplication::postEvent( this, new TagsEvent( CollectionDB::instance()->bundlesByUrls( urls ) ) ); urls.clear(); } } return true; } void UrlLoader::customEvent( QCustomEvent *e) { //DEBUG_BLOCK #define e static_cast(e) switch( e->type() ) { case 1000: foreachType( BundleList, e->bundles ) { - //passing by value is quick for QValueLists, though it is slow - //if we change the list, but this is unlikely - KURL::List::Iterator jt; int alreadyOnPlaylist = 0; PlaylistItem *item = 0; if( m_options & (Playlist::Unique | Playlist::Queue) ) { - for( PlaylistIterator jt( Playlist::instance(), PlaylistIterator::All ); *jt; ++jt ) - { - if( (*jt)->url() == (*it).url() ) - { - item = *jt; - break; - } - } + item = Playlist::instance()->m_urlIndex.getFirst( (*it).url() ); } if( item ) alreadyOnPlaylist++; else item = new PlaylistItem( *it, m_markerListViewItem, (*it).exists() ); if( m_options & Playlist::Queue ) Playlist::instance()->queue( item ); if( m_playFirstUrl && (*it).exists() ) { Playlist::instance()->activate( item ); m_playFirstUrl = false; } } break; case 1001: { foreachType( QValueList, e->xml ) { if( (*it).bundle.isEmpty() ) //safety continue; PlaylistItem* const item = new PlaylistItem( (*it).bundle, m_markerListViewItem ); item->setIsNew( m_coloring ); //TODO scrollbar position //TODO previous tracks queue //TODO current track position, even if user doesn't have resume playback turned on if( (*it).queue >= 0 ) { if( (*it).queue == 0 ) Playlist::instance()->setCurrentTrack( item ); else if( (*it).queue > 0 ) { PLItemList &m_nextTracks = Playlist::instance()->m_nextTracks; int count = m_nextTracks.count(); for( int c = count; c < (*it).queue; c++ ) // Append foo values and replace with correct values later. m_nextTracks.append( item ); m_nextTracks.replace( (*it).queue - 1, item ); } } if( (*it).stopafter ) Playlist::instance()->m_stopAfterTrack = item; if( (*it).filestatusdisabled || !( (*it).bundle.exists() ) ) item->setFilestatusEnabled( false ); if( (*it).dynamicdisabled ) item->setDynamicEnabled( false ); } break; } default: DependentJob::customEvent( e ); return; } #undef e } void UrlLoader::completeJob() { DEBUG_BLOCK const PLItemList &newQueue = Playlist::instance()->m_nextTracks; QPtrListIterator it( newQueue ); PLItemList added; for( it.toFirst(); *it; ++it ) if( !m_oldQueue.containsRef( *it ) ) added << (*it); if( !added.isEmpty() ) emit queueChanged( added, PLItemList() ); if ( !m_badURLs.isEmpty() ) { QString text = i18n("These media could not be loaded into the playlist: " ); debug() << "The following urls were not suitable for the playlist:" << endl; for ( uint it = 0; it < m_badURLs.count(); it++ ) { if( it < 5 ) text += QString("
%1").arg( m_badURLs[it].prettyURL() ); else if( it == 5 ) text += QString("
Plus %1 more").arg( m_badURLs.count() - it ); debug() << "\t" << m_badURLs[it] << endl; } Amarok::StatusBar::instance()->shortLongMessage( i18n("Some media could not be loaded (not playable)."), text ); } if( !m_dynamicMode.isEmpty() ) Playlist::instance()->setDynamicMode( PlaylistBrowser::instance()->findDynamicModeByTitle( m_dynamicMode ) ); //synchronous, ie not using eventLoop QApplication::sendEvent( dependent(), this ); } KURL::List UrlLoader::recurse( const KURL &url ) { typedef QMap FileMap; KDirLister lister( false ); lister.setAutoUpdate( false ); lister.setAutoErrorHandlingEnabled( false, 0 ); if ( !lister.openURL( url ) ) return KURL::List(); // Fucking KDirLister sometimes hangs on remote media, so we add a timeout const int timeout = 3000; // ms QTime watchdog; watchdog.start(); while( !lister.isFinished() && !isAborted() && watchdog.elapsed() < timeout ) kapp->eventLoop()->processEvents( QEventLoop::ExcludeUserInput ); KFileItemList items = lister.items(); //returns QPtrList, so we MUST only do it once! KURL::List urls; FileMap files; for( KFileItem *item = items.first(); item; item = items.next() ) { if( item->isFile() ) { files[item->name()] = item->url(); continue; } if( item->isDir() ) urls += recurse( item->url() ); } foreachType( FileMap, files ) // users often have playlist files that reflect directories // higher up, or stuff in this directory. Don't add them as // it produces double entries if( !PlaylistFile::isPlaylistFile( (*it).fileName() ) ) urls += *it; return urls; } namespace Amarok { // almost the same as UrlLoader::recurse, but global KURL::List recursiveUrlExpand( const KURL &url, int maxURLs ) { typedef QMap FileMap; if( url.protocol() != "file" || !QFileInfo( url.path() ).isDir() ) return KURL::List( url ); MyDirLister lister( false ); lister.setAutoUpdate( false ); lister.setAutoErrorHandlingEnabled( false, 0 ); if ( !lister.openURL( url ) ) return KURL::List(); // Fucking KDirLister sometimes hangs on remote media, so we add a timeout const int timeout = 3000; // ms QTime watchdog; watchdog.start(); while( !lister.isFinished() && watchdog.elapsed() < timeout ) kapp->eventLoop()->processEvents( QEventLoop::ExcludeUserInput ); KFileItemList items = lister.items(); //returns QPtrList, so we MUST only do it once! KURL::List urls; FileMap files; for( KFileItem *item = items.first(); item; item = items.next() ) { if( maxURLs >= 0 && (int)(urls.count() + files.count()) >= maxURLs ) break; if( item->isFile() && !PlaylistFile::isPlaylistFile( item->url().fileName() ) ) { files[item->name()] = item->url(); continue; } if( item->isDir() ) urls += recursiveUrlExpand( item->url(), maxURLs - urls.count() - files.count() ); } foreachType( FileMap, files ) // users often have playlist files that reflect directories // higher up, or stuff in this directory. Don't add them as // it produces double entries urls += *it; return urls; } KURL::List recursiveUrlExpand( const KURL::List &list, int maxURLs ) { KURL::List urls; foreachType( KURL::List, list ) { if( maxURLs >= 0 && (int)urls.count() >= maxURLs ) break; urls += recursiveUrlExpand( *it, maxURLs - urls.count() ); } return urls; } } // Amarok void UrlLoader::loadXml( const KURL &url ) { QFile file( url.path() ); if( !file.open( IO_ReadOnly ) ) { m_badURLs += url; return; } m_currentURL = url; delete m_xmlSource; m_xmlSource = new QXmlInputSource( file ); MyXmlLoader loader; connect( &loader, SIGNAL( newBundle( const MetaBundle&, const XmlAttributeList& ) ), this, SLOT( slotNewBundle( const MetaBundle&, const XmlAttributeList& ) ) ); connect( &loader, SIGNAL( playlistInfo( const QString&, const QString&, const QString& ) ), this, SLOT( slotPlaylistInfo( const QString&, const QString&, const QString& ) ) ); loader.load( m_xmlSource ); if( !m_xml.isEmpty() ) { QApplication::postEvent( this, new TagsEvent( m_xml ) ); m_xml.clear(); } if( !loader.lastError().isEmpty() ) { Amarok::StatusBar::instance()->longMessageThreadSafe( i18n( //TODO add a link to the path to the playlist "The XML in the playlist was invalid. Please report this as a bug to the Amarok " "developers. Thank you." ), KDE::StatusBar::Error ); ::error() << "[PLAYLISTLOADER]: Error in " << m_currentURL.prettyURL() << ": " << loader.lastError() << endl; } } void UrlLoader::slotNewBundle( const MetaBundle &bundle, const XmlAttributeList &atts ) { XMLData data; data.bundle = QDeepCopy( bundle ); for( int i = 0, n = atts.count(); i < n; ++i ) { if( atts[i].first == "queue_index" ) { bool ok = true; data.queue = atts[i].second.toInt( &ok ); if( !ok ) data.queue = -1; } else if( atts[i].first == "stop_after" ) data.stopafter = true; else if( atts[i].first == "dynamicdisabled" ) data.dynamicdisabled = true; else if( atts[i].first == "filestatusdisabled" ) data.filestatusdisabled = true; } data.bundle.checkExists(); m_xml.append( data ); if( m_xml.count() == OPTIMUM_BUNDLE_COUNT ) { QApplication::postEvent( this, new TagsEvent( m_xml ) ); m_xml.clear(); } } void UrlLoader::slotPlaylistInfo( const QString &, const QString &version, const QString &dynamicMode ) { if( version != Amarok::xmlVersion() ) { Amarok::StatusBar::instance()->longMessageThreadSafe( i18n( "Your last playlist was saved with a different version of Amarok than this one, " "and this version can no longer read it.\n" "You will have to create a new one.\n" "Sorry :(" ) ); static_cast( const_cast( sender() ) )->abort(); //HACK? return; } else m_dynamicMode = dynamicMode; } /// @class PlaylistFile PlaylistFile::PlaylistFile( const QString &path ) : m_path( path ) { QFile file( path ); if( !file.open( IO_ReadOnly ) ) { m_error = i18n( "Amarok could not open the file." ); return; } QTextStream stream( &file ); switch( format( m_path ) ) { case M3U: loadM3u( stream ); break; case PLS: loadPls( stream ); break; case XML: m_error = i18n( "This component of Amarok cannot translate XML playlists." ); return; case RAM: loadRealAudioRam( stream ); break; case ASX: loadASX( stream ); break; case SMIL: loadSMIL( stream ); break; case XSPF: loadXSPF( stream ); break; default: m_error = i18n( "Amarok does not support this playlist format." ); return; } if( m_error.isEmpty() && m_bundles.isEmpty() ) m_error = i18n( "The playlist did not contain any references to files." ); debug() << m_error << endl; } bool PlaylistFile::loadM3u( QTextStream &stream ) { const QString directory = m_path.left( m_path.findRev( '/' ) + 1 ); MetaBundle b; for( QString line; !stream.atEnd(); ) { line = stream.readLine(); if( line.startsWith( "#EXTINF" ) ) { const QString extinf = line.section( ':', 1 ); const int length = extinf.section( ',', 0, 0 ).toInt(); b.setTitle( extinf.section( ',', 1 ) ); b.setLength( length <= 0 ? /*MetaBundle::Undetermined HACK*/ -2 : length ); } else if( !line.startsWith( "#" ) && !line.isEmpty() ) { // KURL::isRelativeURL() expects absolute URLs to start with a protocol, so prepend it if missing QString url = line; if( url.startsWith( "/" ) ) url.prepend( "file://" ); if( KURL::isRelativeURL( url ) ) { KURL kurl( KURL::fromPathOrURL( directory + line ) ); kurl.cleanPath(); b.setPath( kurl.path() ); } else { b.setUrl( KURL::fromPathOrURL( line ) ); } // Ensure that we always have a title: use the URL as fallback if( b.title().isEmpty() ) b.setTitle( url ); m_bundles += b; b = MetaBundle(); } } return true; } bool PlaylistFile::loadPls( QTextStream &stream ) { // Counted number of "File#=" lines. unsigned int entryCnt = 0; // Value of the "NumberOfEntries=#" line. unsigned int numberOfEntries = 0; // Does the file have a "[playlist]" section? (as it's required by the standard) bool havePlaylistSection = false; QString tmp; QStringList lines; const QRegExp regExp_NumberOfEntries("^NumberOfEntries\\s*=\\s*\\d+$"); const QRegExp regExp_File("^File\\d+\\s*="); const QRegExp regExp_Title("^Title\\d+\\s*="); const QRegExp regExp_Length("^Length\\d+\\s*=\\s*\\d+$"); const QRegExp regExp_Version("^Version\\s*=\\s*\\d+$"); const QString section_playlist("[playlist]"); /* Preprocess the input data. * Read the lines into a buffer; Cleanup the line strings; * Count the entries manually and read "NumberOfEntries". */ while (!stream.atEnd()) { tmp = stream.readLine(); tmp = tmp.stripWhiteSpace(); if (tmp.isEmpty()) continue; lines.append(tmp); if (tmp.contains(regExp_File)) { entryCnt++; continue; } if (tmp == section_playlist) { havePlaylistSection = true; continue; } if (tmp.contains(regExp_NumberOfEntries)) { numberOfEntries = tmp.section('=', -1).stripWhiteSpace().toUInt(); continue; } } if (numberOfEntries != entryCnt) { warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. " << "NumberOfEntries=" << numberOfEntries << " counted=" << entryCnt << endl; /* Corrupt file. The "NumberOfEntries" value is * not correct. Fix it by setting it to the manually * counted number and go on parsing. */ numberOfEntries = entryCnt; } if (!numberOfEntries) return true; unsigned int index; bool ok = false; bool inPlaylistSection = false; Q_ASSERT(m_bundles.isEmpty()); m_bundles.insert(m_bundles.begin(), numberOfEntries, MetaBundle()); /* Now iterate through all beautified lines in the buffer * and parse the playlist data. */ QStringList::const_iterator i = lines.begin(), end = lines.end(); for ( ; i != end; ++i) { if (!inPlaylistSection && havePlaylistSection) { /* The playlist begins with the "[playlist]" tag. * Skip everything before this. */ if ((*i) == section_playlist) inPlaylistSection = true; continue; } if ((*i).contains(regExp_File)) { // Have a "File#=XYZ" line. index = loadPls_extractIndex(*i); if (index > numberOfEntries || index == 0) continue; tmp = (*i).section('=', 1).stripWhiteSpace(); m_bundles[index - 1].setUrl(KURL::fromPathOrURL(tmp)); // Ensure that if the entry has no title, we show at least the URL as title m_bundles[index - 1].setTitle(tmp); continue; } if ((*i).contains(regExp_Title)) { // Have a "Title#=XYZ" line. index = loadPls_extractIndex(*i); if (index > numberOfEntries || index == 0) continue; tmp = (*i).section('=', 1).stripWhiteSpace(); m_bundles[index - 1].setTitle(tmp); continue; } if ((*i).contains(regExp_Length)) { // Have a "Length#=XYZ" line. index = loadPls_extractIndex(*i); if (index > numberOfEntries || index == 0) continue; tmp = (*i).section('=', 1).stripWhiteSpace(); m_bundles[index - 1].setLength(tmp.toInt(&ok)); Q_ASSERT(ok); continue; } if ((*i).contains(regExp_NumberOfEntries)) { // Have the "NumberOfEntries=#" line. continue; } if ((*i).contains(regExp_Version)) { // Have the "Version=#" line. tmp = (*i).section('=', 1).stripWhiteSpace(); // We only support Version=2 if (tmp.toUInt(&ok) != 2) warning() << ".pls playlist: Unsupported version." << endl; Q_ASSERT(ok); continue; } warning() << ".pls playlist: Unrecognized line: \"" << *i << "\"" << endl; } return true; } bool PlaylistFile::loadXSPF( QTextStream &stream ) { XSPFPlaylist* doc = new XSPFPlaylist( stream ); XSPFtrackList trackList = doc->trackList(); foreachType( XSPFtrackList, trackList ) { KURL location = (*it).location; QString artist = (*it).creator; QString title = (*it).title; QString album = (*it).album; if( location.isEmpty() || ( location.isLocalFile() && !QFile::exists( location.url() ) ) ) { QueryBuilder qb; qb.addMatch( QueryBuilder::tabArtist, QueryBuilder::valName, artist ); qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valTitle, title ); if( !album.isEmpty() ) qb.addMatch( QueryBuilder::valName, album ); qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL ); QStringList values = qb.run(); if( values.isEmpty() ) continue; MetaBundle b( values[0] ); m_bundles += b; } else { debug() << location << ' ' << artist << ' ' << title << ' ' << album << endl; MetaBundle b; b.setUrl( location ); b.setArtist( artist ); b.setTitle( title ); b.setAlbum( album ); b.setComment( (*it).annotation ); b.setLength( (*it).duration / 1000 ); m_bundles += b; } } m_title = doc->title(); return true; } unsigned int PlaylistFile::loadPls_extractIndex( const QString &str ) const { /* Extract the index number out of a .pls line. * Example: * loadPls_extractIndex("File2=foobar") == 2 */ bool ok = false; unsigned int ret; QString tmp(str.section('=', 0, 0)); tmp.remove(QRegExp("^\\D*")); ret = tmp.stripWhiteSpace().toUInt(&ok); Q_ASSERT(ok); return ret; } bool PlaylistFile::loadRealAudioRam( QTextStream &stream ) { MetaBundle b; QString url; //while loop adapted from Kaffeine 0.5 while (!stream.atEnd()) { url = stream.readLine(); if (url[0] == '#') continue; /* ignore comments */ if (url == "--stop--") break; /* stop line */ if ((url.left(7) == "rtsp://") || (url.left(6) == "pnm://") || (url.left(7) == "http://")) { b.setUrl(KURL(url)); m_bundles += b; b = MetaBundle(); } } return true; } bool PlaylistFile::loadASX( QTextStream &stream ) { //adapted from Kaffeine 0.7 MetaBundle b; QDomDocument doc; QString errorMsg; int errorLine, errorColumn; stream.setEncoding( QTextStream::UnicodeUTF8 ); QString content = stream.read(); //ASX looks a lot like xml, but doesn't require tags to be case sensitive, //meaning we have to accept things like: ... //We use a dirty way to achieve this: we make all tags lower case QRegExp ex("(<[/]?[^>]*[A-Z]+[^>]*>)"); ex.setCaseSensitive(true); while ( (ex.search(content)) != -1 ) content.replace(ex.cap( 1 ), ex.cap( 1 ).lower()); if (!doc.setContent(content, &errorMsg, &errorLine, &errorColumn)) { debug() << "Error loading xml file: " "(" << errorMsg << ")" << " at line " << errorLine << ", column " << errorColumn << endl; return false; } QDomElement root = doc.documentElement(); QString url; QString title; QString author; QTime length; QString duration; if (root.nodeName().lower() != "asx") return false; QDomNode node = root.firstChild(); QDomNode subNode; QDomElement element; while (!node.isNull()) { url = QString::null; title = QString::null; author = QString::null; length = QTime(); if (node.nodeName().lower() == "entry") { subNode = node.firstChild(); while (!subNode.isNull()) { if ((subNode.nodeName().lower() == "ref") && (subNode.isElement()) && (url.isNull())) { element = subNode.toElement(); if (element.hasAttribute("href")) url = element.attribute("href"); if (element.hasAttribute("HREF")) url = element.attribute("HREF"); if (element.hasAttribute("Href")) url = element.attribute("Href"); if (element.hasAttribute("HRef")) url = element.attribute("HRef"); } if ((subNode.nodeName().lower() == "duration") && (subNode.isElement())) { duration = QString::null; element = subNode.toElement(); if (element.hasAttribute("value")) duration = element.attribute("value"); if (element.hasAttribute("Value")) duration = element.attribute("Value"); if (element.hasAttribute("VALUE")) duration = element.attribute("VALUE"); if (!duration.isNull()) length = PlaylistFile::stringToTime(duration); } if ((subNode.nodeName().lower() == "title") && (subNode.isElement())) { title = subNode.toElement().text(); } if ((subNode.nodeName().lower() == "author") && (subNode.isElement())) { author = subNode.toElement().text(); } subNode = subNode.nextSibling(); } if (!url.isNull()) { if (title.isNull()) title = url; b.setUrl(KURL(url)); m_bundles += b; b = MetaBundle(); } } node = node.nextSibling(); } return true; } bool PlaylistFile::loadSMIL( QTextStream &stream ) { // adapted from Kaffeine 0.7 QDomDocument doc; if( !doc.setContent( stream.read() ) ) { debug() << "Could now read smil playlist" << endl; return false; } QDomElement root = doc.documentElement(); stream.setEncoding ( QTextStream::UnicodeUTF8 ); if( root.nodeName().lower() != "smil" ) return false; KURL kurl; QString url; QDomNodeList nodeList; QDomNode node; QDomElement element; //audio sources... nodeList = doc.elementsByTagName( "audio" ); for( uint i = 0; i < nodeList.count(); i++ ) { MetaBundle b; node = nodeList.item(i); url = QString::null; if( (node.nodeName().lower() == "audio") && (node.isElement()) ) { element = node.toElement(); if( element.hasAttribute("src") ) url = element.attribute("src"); else if( element.hasAttribute("Src") ) url = element.attribute("Src"); else if( element.hasAttribute("SRC") ) url = element.attribute("SRC"); } if( !url.isNull() ) { b.setUrl( url ); m_bundles += b; } } return true; } /// @class RemotePlaylistFetcher #include #include #include RemotePlaylistFetcher::RemotePlaylistFetcher( const KURL &source, QListViewItem *after, int options ) : QObject( Playlist::instance()->qscrollview() ) , m_source( source ) , m_after( after ) , m_playFirstUrl( options & (Playlist::StartPlay | Playlist::DirectPlay) ) , m_options( options ) { //We keep the extension so the UrlLoader knows what file type it is const QString path = source.path(); m_temp = new KTempFile( QString::null /*use default prefix*/, path.mid( path.findRev( '.' ) ) ); m_temp->setAutoDelete( true ); m_destination.setPath( m_temp->name() ); KIO::Job *job = KIO::file_copy( m_source, m_destination, -1, /* permissions, this means "do what you think" */ true, /* overwrite */ false, /* resume download */ false ); /* don't show stupid UIServer dialog */ Amarok::StatusBar::instance()->newProgressOperation( job ) .setDescription( i18n("Retrieving Playlist") ); connect( job, SIGNAL(result( KIO::Job* )), SLOT(result( KIO::Job* )) ); Playlist::instance()->lock(); } RemotePlaylistFetcher::~RemotePlaylistFetcher() { Playlist::instance()->unlock(); delete m_temp; } void RemotePlaylistFetcher::result( KIO::Job *job ) { if( job->error() ) { error() << "Couldn't download remote playlist\n"; deleteLater(); } else { debug() << "Playlist was downloaded successfully\n"; UrlLoader *loader = new UrlLoader( m_destination, m_after, m_options ); ThreadManager::instance()->queueJob( loader ); // we mustn't get deleted until the loader is finished // or the playlist we downloaded will be deleted before // it can be parsed! loader->insertChild( this ); } } /// @class SqlLoader SqlLoader::SqlLoader( const QString &sql, QListViewItem *after, int options ) : UrlLoader( KURL::List(), after, options ) , m_sql( QDeepCopy( sql ) ) { // Ovy: just until we make sure every SQL query from dynamic playlists is handled // correctly debug() << "Sql loader: query is: " << sql << "\n"; } bool SqlLoader::doJob() { DEBUG_BLOCK const QStringList values = CollectionDB::instance()->query( m_sql ); setProgressTotalSteps( values.count() ); BundleList bundles; uint x = 0; for( for_iterators( QStringList, values ); it != end && !isAborted(); ++it ) { setProgress( x += QueryBuilder::dragFieldCount ); bundles += CollectionDB::instance()->bundleFromQuery( &it ); if( bundles.count() == OPTIMUM_BUNDLE_COUNT || it == last ) { QApplication::postEvent( this, new TagsEvent( bundles ) ); bundles.clear(); } } setProgress100Percent(); return true; } QTime PlaylistFile::stringToTime(const QString& timeString) { int sec = 0; bool ok = false; QStringList tokens = QStringList::split(':',timeString); sec += tokens[0].toInt(&ok)*3600; //hours sec += tokens[1].toInt(&ok)*60; //minutes sec += tokens[2].toInt(&ok); //secs if (ok) return QTime().addSecs(sec); else return QTime(); } bool MyXmlLoader::startElement( const QString &a, const QString &name, const QString &b, const QXmlAttributes &atts ) { if( name == "playlist" ) { QString product, version, dynamic; for( int i = 0, n = atts.count(); i < n; ++i ) { if( atts.localName( i ) == "product" ) product = atts.value( i ); else if( atts.localName( i ) == "version" ) version = atts.value( i ); else if( atts.localName( i ) == "dynamicMode" ) dynamic = atts.value( i ); } emit playlistInfo( product, version, dynamic ); return !m_aborted; } else return XmlLoader::startElement( a, name, b, atts ); } #include "playlistloader.moc" diff --git a/src/playlistloader.h b/src/playlistloader.h index 2354b619a5..ec47789b48 100644 --- a/src/playlistloader.h +++ b/src/playlistloader.h @@ -1,207 +1,207 @@ // Author: Max Howell (C) Copyright 2003-4 // Author: Mark Kretschmann (C) Copyright 2004 // Copyright: See COPYING file that comes with this distribution // #ifndef UrlLoader_H #define UrlLoader_H #include "amarok.h" #include "debug.h" //stack allocated #include #include //baseclass #include //KURL::List #include "metabundle.h" //stack allocated #include "threadmanager.h" //baseclass #include "xmlloader.h" //baseclass class QListViewItem; class QTextStream; class PlaylistItem; class PLItemList; class XMLData; namespace KIO { class Job; } /** * @class PlaylistFile * @author Max Howell * @short Allocate on the stack, the contents are immediately available from bundles() * * Note, it won't do anything with XML playlists * * TODO be able to load directories too, it's in the spec * TODO and playlists within playlists, remote and local */ class PlaylistFile { public: PlaylistFile( const QString &path ); enum Format { M3U, PLS, XML, RAM, SMIL, ASX, XSPF, Unknown, NotPlaylist = Unknown }; /// the bundles from this playlist, they only contain /// the information that can be extracted from the playlists BundleList &bundles() { return m_bundles; } /// the name of the playlist. often stored in the document (eg xspf) or derived from the filename QString &title() { return m_title; } ///@return true if couldn't load the playlist's contents bool isError() const { return !m_error.isEmpty(); } /// if start returns false this has a translated error description QString error() const { return m_error; } static inline bool isPlaylistFile( const KURL &url ) { return isPlaylistFile( url.fileName() ); } static inline bool isPlaylistFile( const QString &fileName ) { return format( fileName ) != Unknown; } static inline Format format( const QString &fileName ); static QTime stringToTime(const QString&); protected: /// make these virtual if you need to bool loadM3u( QTextStream& ); bool loadPls( QTextStream& ); unsigned int loadPls_extractIndex( const QString &str ) const; bool loadRealAudioRam( QTextStream& ); bool loadASX( QTextStream& ); bool loadSMIL( QTextStream& ); bool loadXSPF( QTextStream& ); QString m_path; QString m_error; BundleList m_bundles; QString m_title; }; inline PlaylistFile::Format PlaylistFile::format( const QString &fileName ) { const QString ext = Amarok::extension( fileName ); if( ext == "m3u" ) return M3U; if( ext == "pls" ) return PLS; if( ext == "ram" ) return RAM; if( ext == "smil") return SMIL; if( ext == "asx" || ext == "wax" ) return ASX; if( ext == "xml" ) return XML; if( ext == "xspf" ) return XSPF; return Unknown; } /** * @author Max Howell * @author Mark Kretschmann * @short Populates the Playlist-view with URLs * * + Load playlists, remote and local * + List directories, remote and local * + Read tags, from file:/// and from DB */ class UrlLoader : public ThreadManager::DependentJob { Q_OBJECT public: UrlLoader( const KURL::List&, QListViewItem*, int options = 0 ); ~UrlLoader(); - static const uint OPTIMUM_BUNDLE_COUNT = 50; + static const uint OPTIMUM_BUNDLE_COUNT = 200; signals: void queueChanged( const PLItemList &, const PLItemList & ); protected: /// reimplemented from ThreadManager::Job virtual bool doJob(); virtual void completeJob(); virtual void customEvent( QCustomEvent* ); void loadXml( const KURL& ); private slots: void slotNewBundle( const MetaBundle &bundle, const XmlAttributeList &attributes ); void slotPlaylistInfo( const QString &product, const QString &version, const QString &dynamicMode ); private: KURL::List recurse( const KURL& ); private: KURL::List m_badURLs; KURL::List m_URLs; PlaylistItem *m_markerListViewItem; bool m_playFirstUrl; bool m_coloring; int m_options; Debug::Block m_block; QPtrList m_oldQueue; QXmlInputSource *m_xmlSource; QValueList m_xml; KURL m_currentURL; QString m_dynamicMode; protected: UrlLoader( const UrlLoader& ); //undefined UrlLoader &operator=( const UrlLoader& ); //undefined }; /** * @author Max Howell * @short Populates the Playlist-view using the result of a single SQL query * * The format of the query must be in a set order, see doJob() */ class SqlLoader : public UrlLoader { const QString m_sql; public: SqlLoader( const QString &sql, QListViewItem *after, int options = 0 ); virtual bool doJob(); }; /** * @author Max Howell * @short Fetches a playlist-file from any location, and then loads it into the Playlist-view */ class RemotePlaylistFetcher : public QObject { Q_OBJECT const KURL m_source; KURL m_destination; QListViewItem *m_after; bool m_playFirstUrl; int m_options; class KTempFile *m_temp; public: RemotePlaylistFetcher( const KURL &source, QListViewItem *after, int options = 0 ); ~RemotePlaylistFetcher(); private slots: void result( KIO::Job* ); void abort() { delete this; } }; // PRIVATE -- should be in the .cpp, but fucking moc. class MyXmlLoader: public MetaBundle::XmlLoader { Q_OBJECT public: MyXmlLoader() { } virtual bool startElement( const QString&, const QString&, const QString &, const QXmlAttributes& ); signals: void playlistInfo( const QString &product, const QString &version, const QString &dynamicMode ); }; #endif diff --git a/src/playlistwindow.h b/src/playlistwindow.h index 9204f26b7f..243d618e7f 100644 --- a/src/playlistwindow.h +++ b/src/playlistwindow.h @@ -1,157 +1,157 @@ /*************************************************************************** begin : Fre Nov 15 2002 copyright : (C) Mark Kretschmann : (C) Max Howell ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifndef AMAROK_PLAYLISTWINDOW_H #define AMAROK_PLAYLISTWINDOW_H #include "browserbar.h" #include //baseclass for DynamicBox #include //baseclass #include //baseclass (for XMLGUI) class ClickLineEdit; class CollectionBrowser; class ContextBrowser; class MediaBrowser; class QMenuBar; class KPopupMenu; class KToolBar; class QLabel; class QTimer; /** * @class PlaylistWindow * @short The PlaylistWindow widget class. * * This is the main window widget (the Playlist not Player). */ class PlaylistWindow : public QWidget, public KXMLGUIClient { Q_OBJECT public: PlaylistWindow(); ~PlaylistWindow(); void init(); void applySettings(); void createGUI(); //should be private but App::slowConfigToolbars requires it void recreateGUI(); //allows us to switch browsers from within other browsers etc void showBrowser( const QString& name ) { m_browsers->showBrowser( name ); } void addBrowser( const QString &name, QWidget *widget, const QString &text, const QString &icon ); //takes into account minimized, multiple desktops, etc. bool isReallyShown() const; virtual bool eventFilter( QObject*, QEvent* ); //instance is declared in KXMLGUI static PlaylistWindow *self() { return s_instance; } void activate(); public slots: void showHide(); void mbAvailabilityChanged( bool isAvailable ); private slots: void savePlaylist() const; void slotBurnPlaylist() const; void slotPlayMedia(); void slotAddLocation( bool directPlay = false ); void slotAddStream(); void playLastfmPersonal(); void addLastfmPersonal(); void playLastfmNeighbor(); void addLastfmNeighbor(); void playLastfmCustom(); void addLastfmCustom(); void playLastfmGlobaltag( int ); void addLastfmGlobaltag( int ); void playAudioCD(); void showQueueManager(); void showScriptSelector(); void showStatistics(); void slotMenuActivated( int ); void actionsMenuAboutToShow(); void toolsMenuAboutToShow(); void slotToggleMenu(); void slotToggleFocus(); void slotEditFilter(); void slotSetFilter( const QString &filter ); protected: virtual void closeEvent( QCloseEvent* ); virtual void showEvent( QShowEvent* ); virtual QSize sizeHint() const; private: enum MenuId { ID_SHOW_TOOLBAR = 2000, ID_SHOW_PLAYERWINDOW }; QMenuBar *m_menubar; KPopupMenu *m_toolsMenu; KPopupMenu *m_settingsMenu; BrowserBar *m_browsers; KPopupMenu *m_searchMenu; ClickLineEdit *m_lineEdit; KToolBar *m_toolbar; QTimer *m_timer; //search filter timer QStringList m_lastfmTags; MediaBrowser *m_currMediaBrowser; int m_lastBrowser; int m_searchField; static PlaylistWindow *s_instance; }; class DynamicTitle : public QWidget { Q_OBJECT public: - DynamicTitle(QWidget* parent); - void setTitle(const QString& newTitle); + DynamicTitle( QWidget* parent ); + void setTitle( const QString& newTitle ); protected: - virtual void paintEvent(QPaintEvent* e); + virtual void paintEvent( QPaintEvent* e ); private: static const int s_curveWidth = 5; static const int s_imageSize = 16; QString m_title; QFont m_font; }; class DynamicBar : public QHBox { Q_OBJECT public: - DynamicBar(QWidget* parent); + DynamicBar( QWidget* parent ); void init(); public slots: - void slotNewDynamicMode(const DynamicMode* mode); - void changeTitle(const QString& title); + void slotNewDynamicMode( const DynamicMode* mode ); + void changeTitle( const QString& title ); private: DynamicTitle* m_titleWidget; }; #endif //AMAROK_PLAYLISTWINDOW_H