aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
committerScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
commita1a18f77a50804e0127dfa4b0f5240c49c541184 (patch)
tree19a38880afe505beddb5590379a8134d7730a277 /subsonic-main/src/main/java
parentb61d787706979e7e20f4c3c4f93c1f129d92273f (diff)
downloaddsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.gz
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.bz2
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.zip
Initial Commit
Diffstat (limited to 'subsonic-main/src/main/java')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java231
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java163
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java43
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.java145
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.java53
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java105
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java51
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NetworkStatus.java55
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java98
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java172
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java174
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java455
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.java90
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java187
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ScanInfo.java43
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java64
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.java128
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java49
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.java58
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java146
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java88
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java42
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java184
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java187
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java92
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java65
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java215
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java250
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.java66
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java135
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java278
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java60
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java91
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java38
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java82
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java141
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java72
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java294
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java66
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java74
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java453
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java194
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java179
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java114
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java80
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java340
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java93
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java116
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java270
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java46
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java128
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java297
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java89
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java244
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java130
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java89
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java79
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java58
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java164
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java77
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java150
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java82
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java152
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java102
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java85
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java67
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java68
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java1983
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java101
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java66
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java106
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java58
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java69
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java123
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java161
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java96
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java149
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java141
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java419
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java84
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java139
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java260
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java145
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java159
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java110
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java247
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java127
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java243
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java161
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java94
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java117
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.java89
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java374
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.java91
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java194
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java142
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java165
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java99
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java131
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.java123
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java352
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.java66
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java81
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.java110
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java54
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.java110
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java55
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java56
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.java93
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java47
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java53
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java151
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java48
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.java77
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java54
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.java46
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java65
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java76
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java87
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java234
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java166
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java95
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java75
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java65
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java48
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java168
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java449
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java99
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java62
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java148
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java167
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java61
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java417
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java338
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java49
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.java141
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java96
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java172
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java29
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java70
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java43
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java63
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java59
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.java69
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java100
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java42
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java104
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java221
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java303
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java245
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java328
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.java141
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java50
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java56
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java107
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java147
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java54
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java57
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java104
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java63
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java154
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java150
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java205
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java124
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java131
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java50
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java246
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java71
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java331
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java206
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java614
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java354
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java250
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java336
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java317
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java426
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java599
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.java101
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java567
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java303
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.java46
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java1254
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java133
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.java134
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java530
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java267
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java207
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java75
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java74
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java170
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java296
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java135
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java162
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java51
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.java77
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java76
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.java67
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java207
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.java72
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java117
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.java49
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java51
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java47
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.java60
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java29
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java71
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java186
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java54
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java537
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java127
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java328
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java51
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java45
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.java91
-rw-r--r--subsonic-main/src/main/java/org/json/CDL.java279
-rw-r--r--subsonic-main/src/main/java/org/json/Cookie.java169
-rw-r--r--subsonic-main/src/main/java/org/json/CookieList.java90
-rw-r--r--subsonic-main/src/main/java/org/json/HTTP.java163
-rw-r--r--subsonic-main/src/main/java/org/json/HTTPTokener.java77
-rw-r--r--subsonic-main/src/main/java/org/json/JSONArray.java920
-rw-r--r--subsonic-main/src/main/java/org/json/JSONException.java28
-rw-r--r--subsonic-main/src/main/java/org/json/JSONML.java465
-rw-r--r--subsonic-main/src/main/java/org/json/JSONObject.java1630
-rw-r--r--subsonic-main/src/main/java/org/json/JSONString.java18
-rw-r--r--subsonic-main/src/main/java/org/json/JSONStringer.java78
-rw-r--r--subsonic-main/src/main/java/org/json/JSONTokener.java446
-rw-r--r--subsonic-main/src/main/java/org/json/JSONWriter.java327
-rw-r--r--subsonic-main/src/main/java/org/json/XML.java508
-rw-r--r--subsonic-main/src/main/java/org/json/XMLTokener.java365
234 files changed, 40162 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java
new file mode 100644
index 00000000..0e595458
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java
@@ -0,0 +1,231 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic;
+
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.util.*;
+import org.apache.commons.lang.exception.*;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+
+/**
+ * Logger implementation which logs to SUBSONIC_HOME/subsonic.log.
+ * <br/>
+ * Note: Third party logging libraries (such as log4j and Commons logging) are intentionally not
+ * used. These libraries causes a lot of headache when deploying to some application servers
+ * (for instance Jetty and JBoss).
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.1 $ $Date: 2005/05/09 19:58:26 $
+ */
+public class Logger {
+
+ private String category;
+
+ private static List<Entry> entries = Collections.synchronizedList(new BoundedList<Entry>(50));
+ private static PrintWriter writer;
+
+ /**
+ * Creates a logger for the given class.
+ * @param clazz The class.
+ * @return A logger for the class.
+ */
+ public static Logger getLogger(Class clazz) {
+ return new Logger(clazz.getName());
+ }
+
+ /**
+ * Creates a logger for the given namee.
+ * @param name The name.
+ * @return A logger for the name.
+ */
+ public static Logger getLogger(String name) {
+ return new Logger(name);
+ }
+
+ /**
+ * Returns the last few log entries.
+ * @return The last few log entries.
+ */
+ public static Entry[] getLatestLogEntries() {
+ return entries.toArray(new Entry[0]);
+ }
+
+ private Logger(String name) {
+ int lastDot = name.lastIndexOf('.');
+ if (lastDot == -1) {
+ category = name;
+ } else {
+ category = name.substring(lastDot + 1);
+ }
+ }
+
+ /**
+ * Logs a debug message.
+ * @param message The log message.
+ */
+ public void debug(Object message) {
+ debug(message, null);
+ }
+
+ /**
+ * Logs a debug message.
+ * @param message The message.
+ * @param error The optional exception.
+ */
+ public void debug(Object message, Throwable error) {
+ add(Level.DEBUG, message, error);
+ }
+
+ /**
+ * Logs an info message.
+ * @param message The message.
+ */
+ public void info(Object message) {
+ info(message, null);
+ }
+
+ /**
+ * Logs an info message.
+ * @param message The message.
+ * @param error The optional exception.
+ */
+ public void info(Object message, Throwable error) {
+ add(Level.INFO, message, error);
+ }
+
+ /**
+ * Logs a warning message.
+ * @param message The message.
+ */
+ public void warn(Object message) {
+ warn(message, null);
+ }
+
+ /**
+ * Logs a warning message.
+ * @param message The message.
+ * @param error The optional exception.
+ */
+ public void warn(Object message, Throwable error) {
+ add(Level.WARN, message, error);
+ }
+
+ /**
+ * Logs an error message.
+ * @param message The message.
+ */
+ public void error(Object message) {
+ error(message, null);
+ }
+
+ /**
+ * Logs an error message.
+ * @param message The message.
+ * @param error The optional exception.
+ */
+ public void error(Object message, Throwable error) {
+ add(Level.ERROR, message, error);
+ }
+
+ private void add(Level level, Object message, Throwable error) {
+ Entry entry = new Entry(category, level, message, error);
+ try {
+ getPrintWriter().println(entry);
+ } catch (IOException x) {
+ System.err.println("Failed to write to subsonic.log.");
+ x.printStackTrace();
+ }
+ entries.add(entry);
+ }
+
+ private static synchronized PrintWriter getPrintWriter() throws IOException {
+ if (writer == null) {
+ writer = new PrintWriter(new FileWriter(getLogFile(), false), true);
+ }
+ return writer;
+ }
+
+ public static File getLogFile() {
+ File subsonicHome = SettingsService.getSubsonicHome();
+ return new File(subsonicHome, "subsonic.log");
+ }
+
+ /**
+ * Log level.
+ */
+ public enum Level {
+ DEBUG, INFO, WARN, ERROR
+ }
+
+ /**
+ * Log entry.
+ */
+ public static class Entry {
+ private String category;
+ private Date date;
+ private Level level;
+ private Object message;
+ private Throwable error;
+ private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
+
+ public Entry(String category, Level level, Object message, Throwable error) {
+ this.date = new Date();
+ this.category = category;
+ this.level = level;
+ this.message = message;
+ this.error = error;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public Level getLevel() {
+ return level;
+ }
+
+ public Object getMessage() {
+ return message;
+ }
+
+ public Throwable getError() {
+ return error;
+ }
+
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append('[').append(DATE_FORMAT.format(date)).append("] ");
+ buf.append(level).append(' ');
+ buf.append(category).append(" - ");
+ buf.append(message);
+
+ if (error != null) {
+ buf.append('\n').append(ExceptionUtils.getFullStackTrace(error));
+ }
+ return buf.toString();
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java
new file mode 100644
index 00000000..8905c8a6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java
@@ -0,0 +1,163 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.util.BoundedList;
+import org.apache.commons.lang.StringUtils;
+import org.directwebremoting.WebContext;
+import org.directwebremoting.WebContextFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides AJAX-enabled services for the chatting.
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class ChatService {
+
+ private static final Logger LOG = Logger.getLogger(ChatService.class);
+ private static final String CACHE_KEY = "1";
+ private static final int MAX_MESSAGES = 10;
+ private static final long TTL_MILLIS = 3L * 24L * 60L * 60L * 1000L; // 3 days.
+
+ private final LinkedList<Message> messages = new BoundedList<Message>(MAX_MESSAGES);
+ private SecurityService securityService;
+
+ private long revision = System.identityHashCode(this);
+
+ /**
+ * Invoked by Spring.
+ */
+ public void init() {
+ // Delete old messages every hour.
+ ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+ Runnable runnable = new Runnable() {
+ public void run() {
+ removeOldMessages();
+ }
+ };
+ executor.scheduleWithFixedDelay(runnable, 0L, 3600L, TimeUnit.SECONDS);
+ }
+
+ private synchronized void removeOldMessages() {
+ long now = System.currentTimeMillis();
+ for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
+ Message message = iterator.next();
+ if (now - message.getDate().getTime() > TTL_MILLIS) {
+ iterator.remove();
+ revision++;
+ }
+ }
+ }
+
+ public synchronized void addMessage(String message) {
+ WebContext webContext = WebContextFactory.get();
+ doAddMessage(message, webContext.getHttpServletRequest());
+ }
+
+ public synchronized void doAddMessage(String message, HttpServletRequest request) {
+
+ String user = securityService.getCurrentUsername(request);
+ message = StringUtils.trimToNull(message);
+ if (message != null && user != null) {
+ messages.addFirst(new Message(message, user, new Date()));
+ revision++;
+ }
+ }
+
+ public synchronized void clearMessages() {
+ messages.clear();
+ revision++;
+ }
+
+ /**
+ * Returns all messages, but only if the given revision is different from the
+ * current revision.
+ */
+ public synchronized Messages getMessages(long revision) {
+ if (this.revision != revision) {
+ return new Messages(new ArrayList<Message>(messages), this.revision);
+ }
+ return null;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public static class Messages implements Serializable {
+
+ private static final long serialVersionUID = -752602719879818165L;
+ private final List<Message> messages;
+ private final long revision;
+
+ public Messages(List<Message> messages, long revision) {
+ this.messages = messages;
+ this.revision = revision;
+ }
+
+ public List<Message> getMessages() {
+ return messages;
+ }
+
+ public long getRevision() {
+ return revision;
+ }
+ }
+
+ public static class Message implements Serializable {
+
+ private static final long serialVersionUID = -1907101191518133712L;
+ private final String content;
+ private final String username;
+ private final Date date;
+
+ public Message(String content, String username, Date date) {
+ this.content = content;
+ this.username = username;
+ this.date = date;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java
new file mode 100644
index 00000000..c9160f26
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java
@@ -0,0 +1,43 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+/**
+ * Contains info about cover art images for an album.
+ *
+ * @author Sindre Mehus
+ */
+public class CoverArtInfo {
+
+ private final String imagePreviewUrl;
+ private final String imageDownloadUrl;
+
+ public CoverArtInfo(String imagePreviewUrl, String imageDownloadUrl) {
+ this.imagePreviewUrl = imagePreviewUrl;
+ this.imageDownloadUrl = imageDownloadUrl;
+ }
+
+ public String getImagePreviewUrl() {
+ return imagePreviewUrl;
+ }
+
+ public String getImageDownloadUrl() {
+ return imageDownloadUrl;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.java
new file mode 100644
index 00000000..1c3642b6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.java
@@ -0,0 +1,145 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Provides AJAX-enabled services for changing cover art images.
+ * <p/>
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class CoverArtService {
+
+ private static final Logger LOG = Logger.getLogger(CoverArtService.class);
+
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+
+ /**
+ * Downloads and saves the cover art at the given URL.
+ *
+ * @param albumId ID of the album in question.
+ * @param url The image URL.
+ * @return The error string if something goes wrong, <code>null</code> otherwise.
+ */
+ public String setCoverArtImage(int albumId, String url) {
+ try {
+ MediaFile mediaFile = mediaFileService.getMediaFile(albumId);
+ saveCoverArt(mediaFile.getPath(), url);
+ return null;
+ } catch (Exception x) {
+ LOG.warn("Failed to save cover art for album " + albumId, x);
+ return x.toString();
+ }
+ }
+
+ private void saveCoverArt(String path, String url) throws Exception {
+ InputStream input = null;
+ HttpClient client = new DefaultHttpClient();
+
+ try {
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 20 * 1000); // 20 seconds
+ HttpConnectionParams.setSoTimeout(client.getParams(), 20 * 1000); // 20 seconds
+ HttpGet method = new HttpGet(url);
+
+ HttpResponse response = client.execute(method);
+ input = response.getEntity().getContent();
+
+ // Attempt to resolve proper suffix.
+ String suffix = "jpg";
+ if (url.toLowerCase().endsWith(".gif")) {
+ suffix = "gif";
+ } else if (url.toLowerCase().endsWith(".png")) {
+ suffix = "png";
+ }
+
+ // Check permissions.
+ File newCoverFile = new File(path, "cover." + suffix);
+ if (!securityService.isWriteAllowed(newCoverFile)) {
+ throw new Exception("Permission denied: " + StringUtil.toHtml(newCoverFile.getPath()));
+ }
+
+ // If file exists, create a backup.
+ backup(newCoverFile, new File(path, "cover.backup." + suffix));
+
+ // Write file.
+ IOUtils.copy(input, new FileOutputStream(newCoverFile));
+
+ MediaFile mediaFile = mediaFileService.getMediaFile(path);
+
+ // Rename existing cover file if new cover file is not the preferred.
+ try {
+ File coverFile = mediaFileService.getCoverArt(mediaFile);
+ if (coverFile != null) {
+ if (!newCoverFile.equals(coverFile)) {
+ coverFile.renameTo(new File(coverFile.getCanonicalPath() + ".old"));
+ LOG.info("Renamed old image file " + coverFile);
+ }
+ }
+ } catch (Exception x) {
+ LOG.warn("Failed to rename existing cover file.", x);
+ }
+
+ mediaFileService.refreshMediaFile(mediaFile);
+
+ } finally {
+ IOUtils.closeQuietly(input);
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ private void backup(File newCoverFile, File backup) {
+ if (newCoverFile.exists()) {
+ if (backup.exists()) {
+ backup.delete();
+ }
+ if (newCoverFile.renameTo(backup)) {
+ LOG.info("Backed up old image file to " + backup);
+ } else {
+ LOG.warn("Failed to create image file backup " + backup);
+ }
+ }
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.java
new file mode 100644
index 00000000..b84ffe1f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.java
@@ -0,0 +1,53 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+/**
+ * Contains lyrics info for a song.
+ *
+ * @author Sindre Mehus
+ */
+public class LyricsInfo {
+
+ private final String lyrics;
+ private final String artist;
+ private final String title;
+
+ public LyricsInfo() {
+ this(null, null, null);
+ }
+
+ public LyricsInfo(String lyrics, String artist, String title) {
+ this.lyrics = lyrics;
+ this.artist = artist;
+ this.title = title;
+ }
+
+ public String getLyrics() {
+ return lyrics;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java
new file mode 100644
index 00000000..45c039f7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java
@@ -0,0 +1,105 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.util.StringUtil;
+
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.params.HttpConnectionParams;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+import org.jdom.input.SAXBuilder;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Provides AJAX-enabled services for retrieving song lyrics from chartlyrics.com.
+ * <p/>
+ * See http://www.chartlyrics.com/api.aspx for details.
+ * <p/>
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class LyricsService {
+
+ private static final Logger LOG = Logger.getLogger(LyricsService.class);
+
+ /**
+ * Returns lyrics for the given song and artist.
+ *
+ * @param artist The artist.
+ * @param song The song.
+ * @return The lyrics, never <code>null</code> .
+ */
+ public LyricsInfo getLyrics(String artist, String song) {
+ LyricsInfo lyrics = new LyricsInfo();
+ try {
+
+ artist = StringUtil.urlEncode(artist);
+ song = StringUtil.urlEncode(song);
+
+ String url = "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect?artist=" + artist + "&song=" + song;
+ String xml = executeGetRequest(url);
+
+ lyrics = parseSearchResult(xml);
+
+ } catch (Exception x) {
+ LOG.warn("Failed to get lyrics for song '" + song + "'.", x);
+ }
+ return lyrics;
+ }
+
+
+ private LyricsInfo parseSearchResult(String xml) throws Exception {
+ SAXBuilder builder = new SAXBuilder();
+ Document document = builder.build(new StringReader(xml));
+
+ Element root = document.getRootElement();
+ Namespace ns = root.getNamespace();
+
+ String lyric = root.getChildText("Lyric", ns);
+ String song = root.getChildText("LyricSong", ns);
+ String artist = root.getChildText("LyricArtist", ns);
+
+ return new LyricsInfo(lyric, artist, song);
+ }
+
+ private String executeGetRequest(String url) throws IOException {
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 15000);
+ HttpGet method = new HttpGet(url);
+ try {
+
+ ResponseHandler<String> responseHandler = new BasicResponseHandler();
+ return client.execute(method, responseHandler);
+
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java
new file mode 100644
index 00000000..0c83e30f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.service.NetworkService;
+
+/**
+ * Provides miscellaneous AJAX-enabled services.
+ * <p/>
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class MultiService {
+
+ private static final Logger LOG = Logger.getLogger(MultiService.class);
+ private NetworkService networkService;
+
+ /**
+ * Returns status for port forwarding and URL redirection.
+ */
+ public NetworkStatus getNetworkStatus() {
+ NetworkService.Status portForwardingStatus = networkService.getPortForwardingStatus();
+ NetworkService.Status urlRedirectionStatus = networkService.getURLRedirecionStatus();
+ return new NetworkStatus(portForwardingStatus.getText(),
+ portForwardingStatus.getDate(),
+ urlRedirectionStatus.getText(),
+ urlRedirectionStatus.getDate());
+ }
+
+ public void setNetworkService(NetworkService networkService) {
+ this.networkService = networkService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NetworkStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NetworkStatus.java
new file mode 100644
index 00000000..8634af26
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NetworkStatus.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ */
+public class NetworkStatus {
+ private final String portForwardingStatusText;
+ private final Date portForwardingStatusDate;
+ private final String urlRedirectionStatusText;
+ private final Date urlRedirectionStatusDate;
+
+ public NetworkStatus(String portForwardingStatusText, Date portForwardingStatusDate,
+ String urlRedirectionStatusText, Date urlRedirectionStatusDate) {
+ this.portForwardingStatusText = portForwardingStatusText;
+ this.portForwardingStatusDate = portForwardingStatusDate;
+ this.urlRedirectionStatusText = urlRedirectionStatusText;
+ this.urlRedirectionStatusDate = urlRedirectionStatusDate;
+ }
+
+ public String getPortForwardingStatusText() {
+ return portForwardingStatusText;
+ }
+
+ public Date getPortForwardingStatusDate() {
+ return portForwardingStatusDate;
+ }
+
+ public String getUrlRedirectionStatusText() {
+ return urlRedirectionStatusText;
+ }
+
+ public Date getUrlRedirectionStatusDate() {
+ return urlRedirectionStatusDate;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java
new file mode 100644
index 00000000..520cfcab
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java
@@ -0,0 +1,98 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+/**
+ * Details about what a user is currently listening to.
+ *
+ * @author Sindre Mehus
+ */
+public class NowPlayingInfo {
+
+ private final String username;
+ private final String artist;
+ private final String title;
+ private final String tooltip;
+ private final String streamUrl;
+ private final String albumUrl;
+ private final String lyricsUrl;
+ private final String coverArtUrl;
+ private final String coverArtZoomUrl;
+ private final String avatarUrl;
+ private final int minutesAgo;
+
+ public NowPlayingInfo(String user, String artist, String title, String tooltip, String streamUrl, String albumUrl,
+ String lyricsUrl, String coverArtUrl, String coverArtZoomUrl, String avatarUrl, int minutesAgo) {
+ this.username = user;
+ this.artist = artist;
+ this.title = title;
+ this.tooltip = tooltip;
+ this.streamUrl = streamUrl;
+ this.albumUrl = albumUrl;
+ this.lyricsUrl = lyricsUrl;
+ this.coverArtUrl = coverArtUrl;
+ this.coverArtZoomUrl = coverArtZoomUrl;
+ this.avatarUrl = avatarUrl;
+ this.minutesAgo = minutesAgo;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getTooltip() {
+ return tooltip;
+ }
+
+ public String getStreamUrl() {
+ return streamUrl;
+ }
+
+ public String getAlbumUrl() {
+ return albumUrl;
+ }
+
+ public String getLyricsUrl() {
+ return lyricsUrl;
+ }
+
+ public String getCoverArtUrl() {
+ return coverArtUrl;
+ }
+
+ public String getCoverArtZoomUrl() {
+ return coverArtZoomUrl;
+ }
+
+ public String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ public int getMinutesAgo() {
+ return minutesAgo;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java
new file mode 100644
index 00000000..ef7922b4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java
@@ -0,0 +1,172 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.domain.AvatarScheme;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.MediaScannerService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.StatusService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.lang.StringUtils;
+import org.directwebremoting.WebContext;
+import org.directwebremoting.WebContextFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides AJAX-enabled services for retrieving the currently playing file and directory.
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class NowPlayingService {
+
+ private PlayerService playerService;
+ private StatusService statusService;
+ private SettingsService settingsService;
+ private MediaScannerService mediaScannerService;
+ private MediaFileService mediaFileService;
+
+ /**
+ * Returns details about what the current player is playing.
+ *
+ * @return Details about what the current player is playing, or <code>null</code> if not playing anything.
+ */
+ public NowPlayingInfo getNowPlayingForCurrentPlayer() throws Exception {
+ WebContext webContext = WebContextFactory.get();
+ Player player = playerService.getPlayer(webContext.getHttpServletRequest(), webContext.getHttpServletResponse());
+ List<TransferStatus> statuses = statusService.getStreamStatusesForPlayer(player);
+ List<NowPlayingInfo> result = convert(statuses);
+
+ return result.isEmpty() ? null : result.get(0);
+ }
+
+ /**
+ * Returns details about what all users are currently playing.
+ *
+ * @return Details about what all users are currently playing.
+ */
+ public List<NowPlayingInfo> getNowPlaying() throws Exception {
+ return convert(statusService.getAllStreamStatuses());
+ }
+
+ /**
+ * Returns media folder scanning status.
+ */
+ public ScanInfo getScanningStatus() {
+ return new ScanInfo(mediaScannerService.isScanning(), mediaScannerService.getScanCount());
+ }
+
+ private List<NowPlayingInfo> convert(List<TransferStatus> statuses) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ String url = request.getRequestURL().toString();
+ List<NowPlayingInfo> result = new ArrayList<NowPlayingInfo>();
+ for (TransferStatus status : statuses) {
+
+ Player player = status.getPlayer();
+ File file = status.getFile();
+
+ if (player != null && player.getUsername() != null && file != null) {
+
+ String username = player.getUsername();
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ if (!userSettings.isNowPlayingAllowed()) {
+ continue;
+ }
+
+ MediaFile mediaFile = mediaFileService.getMediaFile(file);
+ File coverArt = mediaFileService.getCoverArt(mediaFile);
+
+ String artist = mediaFile.getArtist();
+ String title = mediaFile.getTitle();
+ String streamUrl = url.replaceFirst("/dwr/.*", "/stream?player=" + player.getId() + "&id=" + mediaFile.getId());
+ String albumUrl = url.replaceFirst("/dwr/.*", "/main.view?id=" + mediaFile.getId());
+ String lyricsUrl = url.replaceFirst("/dwr/.*", "/lyrics.view?artistUtf8Hex=" + StringUtil.utf8HexEncode(artist) +
+ "&songUtf8Hex=" + StringUtil.utf8HexEncode(title));
+ String coverArtUrl = coverArt == null ? null : url.replaceFirst("/dwr/.*", "/coverArt.view?size=48&id=" + mediaFile.getId());
+ String coverArtZoomUrl = coverArt == null ? null : url.replaceFirst("/dwr/.*", "/coverArt.view?id=" + mediaFile.getId());
+
+ String avatarUrl = null;
+ if (userSettings.getAvatarScheme() == AvatarScheme.SYSTEM) {
+ avatarUrl = url.replaceFirst("/dwr/.*", "/avatar.view?id=" + userSettings.getSystemAvatarId());
+ } else if (userSettings.getAvatarScheme() == AvatarScheme.CUSTOM && settingsService.getCustomAvatar(username) != null) {
+ avatarUrl = url.replaceFirst("/dwr/.*", "/avatar.view?username=" + username);
+ }
+
+ // Rewrite URLs in case we're behind a proxy.
+ if (settingsService.isRewriteUrlEnabled()) {
+ String referer = request.getHeader("referer");
+ streamUrl = StringUtil.rewriteUrl(streamUrl, referer);
+ albumUrl = StringUtil.rewriteUrl(albumUrl, referer);
+ lyricsUrl = StringUtil.rewriteUrl(lyricsUrl, referer);
+ coverArtUrl = StringUtil.rewriteUrl(coverArtUrl, referer);
+ coverArtZoomUrl = StringUtil.rewriteUrl(coverArtZoomUrl, referer);
+ avatarUrl = StringUtil.rewriteUrl(avatarUrl, referer);
+ }
+
+ String tooltip = StringUtil.toHtml(artist) + " &ndash; " + StringUtil.toHtml(title);
+
+ if (StringUtils.isNotBlank(player.getName())) {
+ username += "@" + player.getName();
+ }
+ artist = StringUtil.toHtml(StringUtils.abbreviate(artist, 25));
+ title = StringUtil.toHtml(StringUtils.abbreviate(title, 25));
+ username = StringUtil.toHtml(StringUtils.abbreviate(username, 25));
+
+ long minutesAgo = status.getMillisSinceLastUpdate() / 1000L / 60L;
+ if (minutesAgo < 60) {
+ result.add(new NowPlayingInfo(username, artist, title, tooltip, streamUrl, albumUrl, lyricsUrl,
+ coverArtUrl, coverArtZoomUrl, avatarUrl, (int) minutesAgo));
+ }
+ }
+ }
+
+ return result;
+
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaScannerService(MediaScannerService mediaScannerService) {
+ this.mediaScannerService = mediaScannerService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java
new file mode 100644
index 00000000..e95ec1c8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java
@@ -0,0 +1,174 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import java.util.List;
+
+/**
+ * The playlist of a player.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayQueueInfo {
+
+ private final List<Entry> entries;
+ private final int index;
+ private final boolean stopEnabled;
+ private final boolean repeatEnabled;
+ private final boolean sendM3U;
+ private final float gain;
+
+ public PlayQueueInfo(List<Entry> entries, int index, boolean stopEnabled, boolean repeatEnabled, boolean sendM3U, float gain) {
+ this.entries = entries;
+ this.index = index;
+ this.stopEnabled = stopEnabled;
+ this.repeatEnabled = repeatEnabled;
+ this.sendM3U = sendM3U;
+ this.gain = gain;
+ }
+
+ public List<Entry> getEntries() {
+ return entries;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public boolean isStopEnabled() {
+ return stopEnabled;
+ }
+
+ public boolean isSendM3U() {
+ return sendM3U;
+ }
+
+ public boolean isRepeatEnabled() {
+ return repeatEnabled;
+ }
+
+ public float getGain() {
+ return gain;
+ }
+
+ public static class Entry {
+ private final int id;
+ private final Integer trackNumber;
+ private final String title;
+ private final String artist;
+ private final String album;
+ private final String genre;
+ private final Integer year;
+ private final String bitRate;
+ private final Integer duration;
+ private final String durationAsString;
+ private final String format;
+ private final String contentType;
+ private final String fileSize;
+ private final boolean starred;
+ private final String albumUrl;
+ private final String streamUrl;
+
+ public Entry(int id, Integer trackNumber, String title, String artist, String album, String genre, Integer year,
+ String bitRate, Integer duration, String durationAsString, String format, String contentType, String fileSize,
+ boolean starred, String albumUrl, String streamUrl) {
+ this.id = id;
+ this.trackNumber = trackNumber;
+ this.title = title;
+ this.artist = artist;
+ this.album = album;
+ this.genre = genre;
+ this.year = year;
+ this.bitRate = bitRate;
+ this.duration = duration;
+ this.durationAsString = durationAsString;
+ this.format = format;
+ this.contentType = contentType;
+ this.fileSize = fileSize;
+ this.starred = starred;
+ this.albumUrl = albumUrl;
+ this.streamUrl = streamUrl;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public Integer getTrackNumber() {
+ return trackNumber;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public String getBitRate() {
+ return bitRate;
+ }
+
+ public String getDurationAsString() {
+ return durationAsString;
+ }
+
+ public Integer getDuration() {
+ return duration;
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public String getFileSize() {
+ return fileSize;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ public String getAlbumUrl() {
+ return albumUrl;
+ }
+
+ public String getStreamUrl() {
+ return streamUrl;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java
new file mode 100644
index 00000000..94f78aba
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java
@@ -0,0 +1,455 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.service.PlaylistService;
+import org.directwebremoting.WebContextFactory;
+import org.springframework.web.servlet.support.RequestContextUtils;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Provides AJAX-enabled services for manipulating the play queue of a player.
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class PlayQueueService {
+
+ private PlayerService playerService;
+ private JukeboxService jukeboxService;
+ private TranscodingService transcodingService;
+ private SettingsService settingsService;
+ private MediaFileService mediaFileService;
+ private SecurityService securityService;
+ private MediaFileDao mediaFileDao;
+ private net.sourceforge.subsonic.service.PlaylistService playlistService;
+
+ /**
+ * Returns the play queue for the player of the current user.
+ *
+ * @return The play queue.
+ */
+ public PlayQueueInfo getPlayQueue() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo start() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doStart(request, response);
+ }
+
+ public PlayQueueInfo doStart(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().setStatus(PlayQueue.Status.PLAYING);
+ return convert(request, player, true);
+ }
+
+ public PlayQueueInfo stop() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doStop(request, response);
+ }
+
+ public PlayQueueInfo doStop(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().setStatus(PlayQueue.Status.STOPPED);
+ return convert(request, player, true);
+ }
+
+ public PlayQueueInfo skip(int index) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doSkip(request, response, index, 0);
+ }
+
+ public PlayQueueInfo doSkip(HttpServletRequest request, HttpServletResponse response, int index, int offset) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().setIndex(index);
+ boolean serverSidePlaylist = !player.isExternalWithPlaylist();
+ return convert(request, player, serverSidePlaylist, offset);
+ }
+
+ public PlayQueueInfo play(int id) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+
+ Player player = getCurrentPlayer(request, response);
+ MediaFile file = mediaFileService.getMediaFile(id);
+ List<MediaFile> files = mediaFileService.getDescendantsOf(file, true);
+ return doPlay(request, player, files);
+ }
+
+ public PlayQueueInfo playPlaylist(int id) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+
+ List<MediaFile> files = playlistService.getFilesInPlaylist(id);
+ Player player = getCurrentPlayer(request, response);
+ return doPlay(request, player, files);
+ }
+
+ private PlayQueueInfo doPlay(HttpServletRequest request, Player player, List<MediaFile> files) throws Exception {
+ if (player.isWeb()) {
+ removeVideoFiles(files);
+ }
+ player.getPlayQueue().addFiles(false, files);
+ player.getPlayQueue().setRandomSearchCriteria(null);
+ return convert(request, player, true);
+ }
+
+ public PlayQueueInfo playRandom(int id, int count) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+
+ MediaFile file = mediaFileService.getMediaFile(id);
+ List<MediaFile> randomFiles = getRandomChildren(file, count);
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().addFiles(false, randomFiles);
+ player.getPlayQueue().setRandomSearchCriteria(null);
+ return convert(request, player, true);
+ }
+
+ public PlayQueueInfo add(int id) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doAdd(request, response, new int[]{id});
+ }
+
+ public PlayQueueInfo doAdd(HttpServletRequest request, HttpServletResponse response, int[] ids) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ List<MediaFile> files = new ArrayList<MediaFile>(ids.length);
+ for (int id : ids) {
+ MediaFile ancestor = mediaFileService.getMediaFile(id);
+ files.addAll(mediaFileService.getDescendantsOf(ancestor, true));
+ }
+ if (player.isWeb()) {
+ removeVideoFiles(files);
+ }
+ player.getPlayQueue().addFiles(true, files);
+ player.getPlayQueue().setRandomSearchCriteria(null);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo doSet(HttpServletRequest request, HttpServletResponse response, int[] ids) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ PlayQueue playQueue = player.getPlayQueue();
+ MediaFile currentFile = playQueue.getCurrentFile();
+ PlayQueue.Status status = playQueue.getStatus();
+
+ playQueue.clear();
+ PlayQueueInfo result = doAdd(request, response, ids);
+
+ int index = currentFile == null ? -1 : playQueue.getFiles().indexOf(currentFile);
+ playQueue.setIndex(index);
+ playQueue.setStatus(status);
+ return result;
+ }
+
+ public PlayQueueInfo clear() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doClear(request, response);
+ }
+
+ public PlayQueueInfo doClear(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().clear();
+ boolean serverSidePlaylist = !player.isExternalWithPlaylist();
+ return convert(request, player, serverSidePlaylist);
+ }
+
+ public PlayQueueInfo shuffle() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doShuffle(request, response);
+ }
+
+ public PlayQueueInfo doShuffle(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().shuffle();
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo remove(int index) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ return doRemove(request, response, index);
+ }
+
+ public PlayQueueInfo toggleStar(int index) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+
+ MediaFile file = player.getPlayQueue().getFile(index);
+ String username = securityService.getCurrentUsername(request);
+ boolean starred = mediaFileDao.getMediaFileStarredDate(file.getId(), username) != null;
+ if (starred) {
+ mediaFileDao.unstarMediaFile(file.getId(), username);
+ } else {
+ mediaFileDao.starMediaFile(file.getId(), username);
+ }
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo doRemove(HttpServletRequest request, HttpServletResponse response, int index) throws Exception {
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().removeFileAt(index);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo removeMany(int[] indexes) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ for (int i = indexes.length - 1; i >= 0; i--) {
+ player.getPlayQueue().removeFileAt(indexes[i]);
+ }
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo up(int index) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().moveUp(index);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo down(int index) throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().moveDown(index);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo toggleRepeat() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().setRepeatEnabled(!player.getPlayQueue().isRepeatEnabled());
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo undo() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().undo();
+ boolean serverSidePlaylist = !player.isExternalWithPlaylist();
+ return convert(request, player, serverSidePlaylist);
+ }
+
+ public PlayQueueInfo sortByTrack() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().sort(PlayQueue.SortOrder.TRACK);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo sortByArtist() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().sort(PlayQueue.SortOrder.ARTIST);
+ return convert(request, player, false);
+ }
+
+ public PlayQueueInfo sortByAlbum() throws Exception {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ player.getPlayQueue().sort(PlayQueue.SortOrder.ALBUM);
+ return convert(request, player, false);
+ }
+
+ public void setGain(float gain) {
+ jukeboxService.setGain(gain);
+ }
+
+
+ public String savePlaylist() {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ HttpServletResponse response = WebContextFactory.get().getHttpServletResponse();
+ Player player = getCurrentPlayer(request, response);
+ Locale locale = settingsService.getLocale();
+ DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, locale);
+
+ Date now = new Date();
+ Playlist playlist = new Playlist();
+ playlist.setUsername(securityService.getCurrentUsername(request));
+ playlist.setCreated(now);
+ playlist.setChanged(now);
+ playlist.setPublic(false);
+ playlist.setName(dateFormat.format(now));
+
+ playlistService.createPlaylist(playlist);
+ playlistService.setFilesInPlaylist(playlist.getId(), player.getPlayQueue().getFiles());
+ return playlist.getName();
+ }
+
+ private List<MediaFile> getRandomChildren(MediaFile file, int count) throws IOException {
+ List<MediaFile> children = mediaFileService.getDescendantsOf(file, false);
+ removeVideoFiles(children);
+
+ if (children.isEmpty()) {
+ return children;
+ }
+ Collections.shuffle(children);
+ return children.subList(0, Math.min(count, children.size()));
+ }
+
+ private void removeVideoFiles(List<MediaFile> files) {
+ Iterator<MediaFile> iterator = files.iterator();
+ while (iterator.hasNext()) {
+ MediaFile file = iterator.next();
+ if (file.isVideo()) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean sendM3U) throws Exception {
+ return convert(request, player, sendM3U, 0);
+ }
+
+ private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean sendM3U, int offset) throws Exception {
+ String url = request.getRequestURL().toString();
+
+ if (sendM3U && player.isJukebox()) {
+ jukeboxService.updateJukebox(player, offset);
+ }
+ boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr());
+
+ boolean m3uSupported = player.isExternal() || player.isExternalWithPlaylist();
+ sendM3U = player.isAutoControlEnabled() && m3uSupported && isCurrentPlayer && sendM3U;
+ Locale locale = RequestContextUtils.getLocale(request);
+
+ List<PlayQueueInfo.Entry> entries = new ArrayList<PlayQueueInfo.Entry>();
+ PlayQueue playQueue = player.getPlayQueue();
+ for (MediaFile file : playQueue.getFiles()) {
+ String albumUrl = url.replaceFirst("/dwr/.*", "/main.view?id=" + file.getId());
+ String streamUrl = url.replaceFirst("/dwr/.*", "/stream?player=" + player.getId() + "&id=" + file.getId());
+
+ // Rewrite URLs in case we're behind a proxy.
+ if (settingsService.isRewriteUrlEnabled()) {
+ String referer = request.getHeader("referer");
+ albumUrl = StringUtil.rewriteUrl(albumUrl, referer);
+ streamUrl = StringUtil.rewriteUrl(streamUrl, referer);
+ }
+
+ String format = formatFormat(player, file);
+ String username = securityService.getCurrentUsername(request);
+ boolean starred = mediaFileService.getMediaFileStarredDate(file.getId(), username) != null;
+ entries.add(new PlayQueueInfo.Entry(file.getId(), file.getTrackNumber(), file.getTitle(), file.getArtist(),
+ file.getAlbumName(), file.getGenre(), file.getYear(), formatBitRate(file),
+ file.getDurationSeconds(), file.getDurationString(), format, formatContentType(format),
+ formatFileSize(file.getFileSize(), locale), starred, albumUrl, streamUrl));
+ }
+ boolean isStopEnabled = playQueue.getStatus() == PlayQueue.Status.PLAYING && !player.isExternalWithPlaylist();
+ float gain = jukeboxService.getGain();
+ return new PlayQueueInfo(entries, playQueue.getIndex(), isStopEnabled, playQueue.isRepeatEnabled(), sendM3U, gain);
+ }
+
+ private String formatFileSize(Long fileSize, Locale locale) {
+ if (fileSize == null) {
+ return null;
+ }
+ return StringUtil.formatBytes(fileSize, locale);
+ }
+
+ private String formatFormat(Player player, MediaFile file) {
+ return transcodingService.getSuffix(player, file, null);
+ }
+
+ private String formatContentType(String format) {
+ return StringUtil.getMimeType(format);
+ }
+
+ private String formatBitRate(MediaFile mediaFile) {
+ if (mediaFile.getBitRate() == null) {
+ return null;
+ }
+ if (mediaFile.isVariableBitRate()) {
+ return mediaFile.getBitRate() + " Kbps vbr";
+ }
+ return mediaFile.getBitRate() + " Kbps";
+ }
+
+ private Player getCurrentPlayer(HttpServletRequest request, HttpServletResponse response) {
+ return playerService.getPlayer(request, response);
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setJukeboxService(JukeboxService jukeboxService) {
+ this.jukeboxService = jukeboxService;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.java
new file mode 100644
index 00000000..3fcbfb14
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.java
@@ -0,0 +1,90 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import java.util.List;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Playlist;
+
+/**
+ * The playlist of a player.
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistInfo {
+
+ private final Playlist playlist;
+ private final List<Entry> entries;
+
+ public PlaylistInfo(Playlist playlist, List<Entry> entries) {
+ this.playlist = playlist;
+ this.entries = entries;
+ }
+
+ public Playlist getPlaylist() {
+ return playlist;
+ }
+
+ public List<Entry> getEntries() {
+ return entries;
+ }
+
+ public static class Entry {
+ private final int id;
+ private final String title;
+ private final String artist;
+ private final String album;
+ private final String durationAsString;
+ private final boolean starred;
+
+ public Entry(int id, String title, String artist, String album, String durationAsString, boolean starred) {
+ this.id = id;
+ this.title = title;
+ this.artist = artist;
+ this.album = album;
+ this.durationAsString = durationAsString;
+ this.starred = starred;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public String getDurationAsString() {
+ return durationAsString;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java
new file mode 100644
index 00000000..d3bf854f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java
@@ -0,0 +1,187 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.directwebremoting.WebContextFactory;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SecurityService;
+
+/**
+ * Provides AJAX-enabled services for manipulating playlists.
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistService {
+
+ private MediaFileService mediaFileService;
+ private SecurityService securityService;
+ private net.sourceforge.subsonic.service.PlaylistService playlistService;
+ private MediaFileDao mediaFileDao;
+ private SettingsService settingsService;
+
+ public List<Playlist> getReadablePlaylists() {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ String username = securityService.getCurrentUsername(request);
+ return playlistService.getReadablePlaylistsForUser(username);
+ }
+
+ public List<Playlist> getWritablePlaylists() {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ String username = securityService.getCurrentUsername(request);
+ return playlistService.getWritablePlaylistsForUser(username);
+ }
+
+ public PlaylistInfo getPlaylist(int id) {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+
+ Playlist playlist = playlistService.getPlaylist(id);
+ List<MediaFile> files = playlistService.getFilesInPlaylist(id);
+
+ String username = securityService.getCurrentUsername(request);
+ mediaFileService.populateStarredDate(files, username);
+ return new PlaylistInfo(playlist, createEntries(files));
+ }
+
+ public List<Playlist> createEmptyPlaylist() {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ Locale locale = settingsService.getLocale();
+ DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, locale);
+
+ Date now = new Date();
+ Playlist playlist = new Playlist();
+ playlist.setUsername(securityService.getCurrentUsername(request));
+ playlist.setCreated(now);
+ playlist.setChanged(now);
+ playlist.setPublic(false);
+ playlist.setName(dateFormat.format(now));
+
+ playlistService.createPlaylist(playlist);
+ return getReadablePlaylists();
+ }
+
+ public void appendToPlaylist(int playlistId, List<Integer> mediaFileIds) {
+ List<MediaFile> files = playlistService.getFilesInPlaylist(playlistId);
+ for (Integer mediaFileId : mediaFileIds) {
+ MediaFile file = mediaFileService.getMediaFile(mediaFileId);
+ if (file != null) {
+ files.add(file);
+ }
+ }
+ playlistService.setFilesInPlaylist(playlistId, files);
+ }
+
+ private List<PlaylistInfo.Entry> createEntries(List<MediaFile> files) {
+ List<PlaylistInfo.Entry> result = new ArrayList<PlaylistInfo.Entry>();
+ for (MediaFile file : files) {
+ result.add(new PlaylistInfo.Entry(file.getId(), file.getTitle(), file.getArtist(), file.getAlbumName(),
+ file.getDurationString(), file.getStarredDate() != null));
+ }
+
+ return result;
+ }
+
+ public PlaylistInfo toggleStar(int id, int index) {
+ HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
+ String username = securityService.getCurrentUsername(request);
+ List<MediaFile> files = playlistService.getFilesInPlaylist(id);
+ MediaFile file = files.get(index);
+
+ boolean starred = mediaFileDao.getMediaFileStarredDate(file.getId(), username) != null;
+ if (starred) {
+ mediaFileDao.unstarMediaFile(file.getId(), username);
+ } else {
+ mediaFileDao.starMediaFile(file.getId(), username);
+ }
+ return getPlaylist(id);
+ }
+
+ public PlaylistInfo remove(int id, int index) {
+ List<MediaFile> files = playlistService.getFilesInPlaylist(id);
+ files.remove(index);
+ playlistService.setFilesInPlaylist(id, files);
+ return getPlaylist(id);
+ }
+
+ public PlaylistInfo up(int id, int index) {
+ List<MediaFile> files = playlistService.getFilesInPlaylist(id);
+ if (index > 0) {
+ MediaFile file = files.remove(index);
+ files.add(index - 1, file);
+ playlistService.setFilesInPlaylist(id, files);
+ }
+ return getPlaylist(id);
+ }
+
+ public PlaylistInfo down(int id, int index) {
+ List<MediaFile> files = playlistService.getFilesInPlaylist(id);
+ if (index < files.size() - 1) {
+ MediaFile file = files.remove(index);
+ files.add(index + 1, file);
+ playlistService.setFilesInPlaylist(id, files);
+ }
+ return getPlaylist(id);
+ }
+
+ public void deletePlaylist(int id) {
+ playlistService.deletePlaylist(id);
+ }
+
+ public PlaylistInfo updatePlaylist(int id, String name, String comment, boolean isPublic) {
+ Playlist playlist = playlistService.getPlaylist(id);
+ playlist.setName(name);
+ playlist.setComment(comment);
+ playlist.setPublic(isPublic);
+ playlistService.updatePlaylist(playlist);
+ return getPlaylist(id);
+ }
+
+ public void setPlaylistService(net.sourceforge.subsonic.service.PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ScanInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ScanInfo.java
new file mode 100644
index 00000000..d984069e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ScanInfo.java
@@ -0,0 +1,43 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+/**
+ * Media folder scanning status.
+ *
+ * @author Sindre Mehus
+ */
+public class ScanInfo {
+
+ private final boolean scanning;
+ private final int count;
+
+ public ScanInfo(boolean scanning, int count) {
+ this.scanning = scanning;
+ this.count = count;
+ }
+
+ public boolean isScanning() {
+ return scanning;
+ }
+
+ public int getCount() {
+ return count;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java
new file mode 100644
index 00000000..15ba359b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java
@@ -0,0 +1,64 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.SecurityService;
+import org.directwebremoting.WebContext;
+import org.directwebremoting.WebContextFactory;
+
+/**
+ * Provides AJAX-enabled services for starring.
+ * <p/>
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class StarService {
+
+ private static final Logger LOG = Logger.getLogger(StarService.class);
+
+ private SecurityService securityService;
+ private MediaFileDao mediaFileDao;
+
+ public void star(int id) {
+ mediaFileDao.starMediaFile(id, getUser());
+ }
+
+ public void unstar(int id) {
+ mediaFileDao.unstarMediaFile(id, getUser());
+ }
+
+ private String getUser() {
+ WebContext webContext = WebContextFactory.get();
+ User user = securityService.getCurrentUser(webContext.getHttpServletRequest());
+ return user.getUsername();
+ }
+
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.java
new file mode 100644
index 00000000..f7373b4e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.java
@@ -0,0 +1,128 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.metadata.MetaData;
+import net.sourceforge.subsonic.service.metadata.MetaDataParser;
+import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Provides AJAX-enabled services for editing tags in music files.
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class TagService {
+
+ private static final Logger LOG = Logger.getLogger(TagService.class);
+
+ private MetaDataParserFactory metaDataParserFactory;
+ private MediaFileService mediaFileService;
+
+ /**
+ * Updated tags for a given music file.
+ *
+ * @param id The ID of the music file.
+ * @param track The track number.
+ * @param artist The artist name.
+ * @param album The album name.
+ * @param title The song title.
+ * @param year The release year.
+ * @param genre The musical genre.
+ * @return "UPDATED" if the new tags were updated, "SKIPPED" if no update was necessary.
+ * Otherwise the error message is returned.
+ */
+ public String setTags(int id, String track, String artist, String album, String title, String year, String genre) {
+
+ track = StringUtils.trimToNull(track);
+ artist = StringUtils.trimToNull(artist);
+ album = StringUtils.trimToNull(album);
+ title = StringUtils.trimToNull(title);
+ year = StringUtils.trimToNull(year);
+ genre = StringUtils.trimToNull(genre);
+
+ Integer trackNumber = null;
+ if (track != null) {
+ try {
+ trackNumber = new Integer(track);
+ } catch (NumberFormatException x) {
+ LOG.warn("Illegal track number: " + track, x);
+ }
+ }
+
+ Integer yearNumber = null;
+ if (year != null) {
+ try {
+ yearNumber = new Integer(year);
+ } catch (NumberFormatException x) {
+ LOG.warn("Illegal year: " + year, x);
+ }
+ }
+
+ try {
+
+ MediaFile file = mediaFileService.getMediaFile(id);
+ MetaDataParser parser = metaDataParserFactory.getParser(file.getFile());
+
+ if (!parser.isEditingSupported()) {
+ return "Tag editing of " + FilenameUtils.getExtension(file.getPath()) + " files is not supported.";
+ }
+
+ MetaData existingMetaData = parser.getRawMetaData(file.getFile());
+
+ if (StringUtils.equals(artist, existingMetaData.getArtist()) &&
+ StringUtils.equals(album, existingMetaData.getAlbumName()) &&
+ StringUtils.equals(title, existingMetaData.getTitle()) &&
+ ObjectUtils.equals(yearNumber, existingMetaData.getYear()) &&
+ StringUtils.equals(genre, existingMetaData.getGenre()) &&
+ ObjectUtils.equals(trackNumber, existingMetaData.getTrackNumber())) {
+ return "SKIPPED";
+ }
+
+ MetaData newMetaData = new MetaData();
+ newMetaData.setArtist(artist);
+ newMetaData.setAlbumName(album);
+ newMetaData.setTitle(title);
+ newMetaData.setYear(yearNumber);
+ newMetaData.setGenre(genre);
+ newMetaData.setTrackNumber(trackNumber);
+ parser.setMetaData(file, newMetaData);
+ mediaFileService.refreshMediaFile(file);
+ return "UPDATED";
+
+ } catch (Exception x) {
+ LOG.warn("Failed to update tags for " + id, x);
+ return x.getMessage();
+ }
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) {
+ this.metaDataParserFactory = metaDataParserFactory;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java
new file mode 100644
index 00000000..19309348
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java
@@ -0,0 +1,49 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.controller.*;
+import org.directwebremoting.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Provides AJAX-enabled services for retrieving the status of ongoing transfers.
+ * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
+ *
+ * @author Sindre Mehus
+ */
+public class TransferService {
+
+ /**
+ * Returns info about any ongoing upload within the current session.
+ * @return Info about ongoing upload.
+ */
+ public UploadInfo getUploadInfo() {
+
+ HttpSession session = WebContextFactory.get().getSession();
+ TransferStatus status = (TransferStatus) session.getAttribute(UploadController.UPLOAD_STATUS);
+
+ if (status != null) {
+ return new UploadInfo(status.getBytesTransfered(), status.getBytesTotal());
+ }
+ return new UploadInfo(0L, 0L);
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.java
new file mode 100644
index 00000000..47f9de99
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.java
@@ -0,0 +1,52 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ajax;
+
+/**
+ * Contains status for a file upload.
+ *
+ * @author Sindre Mehus
+ */
+public class UploadInfo {
+
+ private long bytesUploaded;
+ private long bytesTotal;
+
+ public UploadInfo(long bytesUploaded, long bytesTotal) {
+ this.bytesUploaded = bytesUploaded;
+ this.bytesTotal = bytesTotal;
+ }
+
+ /**
+ * Returns the number of bytes uploaded.
+ * @return The number of bytes uploaded.
+ */
+ public long getBytesUploaded() {
+ return bytesUploaded;
+ }
+
+ /**
+ * Returns the total number of bytes.
+ * @return The total number of bytes.
+ */
+ public long getBytesTotal() {
+ return bytesTotal;
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.java
new file mode 100644
index 00000000..00f656b1
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.java
@@ -0,0 +1,58 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.cache;
+
+
+import java.io.File;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Ehcache;
+import net.sf.ehcache.config.Configuration;
+import net.sf.ehcache.config.ConfigurationFactory;
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.service.SettingsService;
+
+/**
+ * Initializes Ehcache and creates caches.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheFactory implements InitializingBean {
+
+ private static final Logger LOG = Logger.getLogger(CacheFactory.class);
+ private CacheManager cacheManager;
+
+ public void afterPropertiesSet() throws Exception {
+ Configuration configuration = ConfigurationFactory.parseConfiguration();
+
+ // Override configuration to make sure cache is stored in Subsonic home dir.
+ File cacheDir = new File(SettingsService.getSubsonicHome(), "cache");
+ configuration.getDiskStoreConfiguration().setPath(cacheDir.getPath());
+
+ cacheManager = CacheManager.create(configuration);
+ }
+
+ public Ehcache getCache(String name) {
+ return cacheManager.getCache(name);
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java
new file mode 100644
index 00000000..6c87df51
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java
@@ -0,0 +1,146 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.controller.AdvancedSettingsController;
+
+/**
+ * Command used in {@link AdvancedSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class AdvancedSettingsCommand {
+ private String downsampleCommand;
+ private String coverArtLimit;
+ private String downloadLimit;
+ private String uploadLimit;
+ private String streamPort;
+ private boolean ldapEnabled;
+ private String ldapUrl;
+ private String ldapSearchFilter;
+ private String ldapManagerDn;
+ private String ldapManagerPassword;
+ private boolean ldapAutoShadowing;
+ private String brand;
+ private boolean isReloadNeeded;
+
+ public String getDownsampleCommand() {
+ return downsampleCommand;
+ }
+
+ public void setDownsampleCommand(String downsampleCommand) {
+ this.downsampleCommand = downsampleCommand;
+ }
+
+ public String getCoverArtLimit() {
+ return coverArtLimit;
+ }
+
+ public void setCoverArtLimit(String coverArtLimit) {
+ this.coverArtLimit = coverArtLimit;
+ }
+
+ public String getDownloadLimit() {
+ return downloadLimit;
+ }
+
+ public void setDownloadLimit(String downloadLimit) {
+ this.downloadLimit = downloadLimit;
+ }
+
+ public String getUploadLimit() {
+ return uploadLimit;
+ }
+
+ public String getStreamPort() {
+ return streamPort;
+ }
+
+ public void setStreamPort(String streamPort) {
+ this.streamPort = streamPort;
+ }
+
+ public void setUploadLimit(String uploadLimit) {
+ this.uploadLimit = uploadLimit;
+ }
+
+ public boolean isLdapEnabled() {
+ return ldapEnabled;
+ }
+
+ public void setLdapEnabled(boolean ldapEnabled) {
+ this.ldapEnabled = ldapEnabled;
+ }
+
+ public String getLdapUrl() {
+ return ldapUrl;
+ }
+
+ public void setLdapUrl(String ldapUrl) {
+ this.ldapUrl = ldapUrl;
+ }
+
+ public String getLdapSearchFilter() {
+ return ldapSearchFilter;
+ }
+
+ public void setLdapSearchFilter(String ldapSearchFilter) {
+ this.ldapSearchFilter = ldapSearchFilter;
+ }
+
+ public String getLdapManagerDn() {
+ return ldapManagerDn;
+ }
+
+ public void setLdapManagerDn(String ldapManagerDn) {
+ this.ldapManagerDn = ldapManagerDn;
+ }
+
+ public String getLdapManagerPassword() {
+ return ldapManagerPassword;
+ }
+
+ public void setLdapManagerPassword(String ldapManagerPassword) {
+ this.ldapManagerPassword = ldapManagerPassword;
+ }
+
+ public boolean isLdapAutoShadowing() {
+ return ldapAutoShadowing;
+ }
+
+ public void setLdapAutoShadowing(boolean ldapAutoShadowing) {
+ this.ldapAutoShadowing = ldapAutoShadowing;
+ }
+
+ public void setBrand(String brand) {
+ this.brand = brand;
+ }
+
+ public String getBrand() {
+ return brand;
+ }
+
+ public void setReloadNeeded(boolean reloadNeeded) {
+ isReloadNeeded = reloadNeeded;
+ }
+
+ public boolean isReloadNeeded() {
+ return isReloadNeeded;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java
new file mode 100644
index 00000000..04af0ff6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java
@@ -0,0 +1,88 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.controller.DonateController;
+
+import java.util.Date;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Command used in {@link DonateController}.
+ *
+ * @author Sindre Mehus
+ */
+public class DonateCommand {
+
+ private String path;
+ private String emailAddress;
+ private String license;
+ private Date licenseDate;
+ private boolean licenseValid;
+ private String brand;
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getEmailAddress() {
+ return emailAddress;
+ }
+
+ public void setEmailAddress(String emailAddress) {
+ this.emailAddress = StringUtils.trim(emailAddress);
+ }
+
+ public String getLicense() {
+ return license;
+ }
+
+ public void setLicense(String license) {
+ this.license = StringUtils.trim(license);
+ }
+
+ public Date getLicenseDate() {
+ return licenseDate;
+ }
+
+ public void setLicenseDate(Date licenseDate) {
+ this.licenseDate = licenseDate;
+ }
+
+ public boolean isLicenseValid() {
+ return licenseValid;
+ }
+
+ public void setLicenseValid(boolean licenseValid) {
+ this.licenseValid = licenseValid;
+ }
+
+ public String getBrand() {
+ return brand;
+ }
+
+ public void setBrand(String brand) {
+ this.brand = brand;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java
new file mode 100644
index 00000000..bb1fc5ff
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java
@@ -0,0 +1,42 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+/**
+ * Holds the name and description of an enum value.
+ *
+ * @author Sindre Mehus
+ */
+public class EnumHolder {
+ private String name;
+ private String description;
+
+ public EnumHolder(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java
new file mode 100644
index 00000000..2322a3bd
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java
@@ -0,0 +1,184 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.controller.GeneralSettingsController;
+import net.sourceforge.subsonic.domain.Theme;
+
+/**
+ * Command used in {@link GeneralSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class GeneralSettingsCommand {
+
+ private String musicFileTypes;
+ private String videoFileTypes;
+ private String coverArtFileTypes;
+ private String index;
+ private String ignoredArticles;
+ private String shortcuts;
+ private boolean sortAlbumsByYear;
+ private boolean gettingStartedEnabled;
+ private String welcomeTitle;
+ private String welcomeSubtitle;
+ private String welcomeMessage;
+ private String loginMessage;
+ private String localeIndex;
+ private String[] locales;
+ private String themeIndex;
+ private Theme[] themes;
+ private boolean isReloadNeeded;
+
+ public String getMusicFileTypes() {
+ return musicFileTypes;
+ }
+
+ public void setMusicFileTypes(String musicFileTypes) {
+ this.musicFileTypes = musicFileTypes;
+ }
+
+ public String getVideoFileTypes() {
+ return videoFileTypes;
+ }
+
+ public void setVideoFileTypes(String videoFileTypes) {
+ this.videoFileTypes = videoFileTypes;
+ }
+
+ public String getCoverArtFileTypes() {
+ return coverArtFileTypes;
+ }
+
+ public void setCoverArtFileTypes(String coverArtFileTypes) {
+ this.coverArtFileTypes = coverArtFileTypes;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String index) {
+ this.index = index;
+ }
+
+ public String getIgnoredArticles() {
+ return ignoredArticles;
+ }
+
+ public void setIgnoredArticles(String ignoredArticles) {
+ this.ignoredArticles = ignoredArticles;
+ }
+
+ public String getShortcuts() {
+ return shortcuts;
+ }
+
+ public void setShortcuts(String shortcuts) {
+ this.shortcuts = shortcuts;
+ }
+
+ public String getWelcomeTitle() {
+ return welcomeTitle;
+ }
+
+ public void setWelcomeTitle(String welcomeTitle) {
+ this.welcomeTitle = welcomeTitle;
+ }
+
+ public String getWelcomeSubtitle() {
+ return welcomeSubtitle;
+ }
+
+ public void setWelcomeSubtitle(String welcomeSubtitle) {
+ this.welcomeSubtitle = welcomeSubtitle;
+ }
+
+ public String getWelcomeMessage() {
+ return welcomeMessage;
+ }
+
+ public void setWelcomeMessage(String welcomeMessage) {
+ this.welcomeMessage = welcomeMessage;
+ }
+
+ public String getLoginMessage() {
+ return loginMessage;
+ }
+
+ public void setLoginMessage(String loginMessage) {
+ this.loginMessage = loginMessage;
+ }
+
+ public String getLocaleIndex() {
+ return localeIndex;
+ }
+
+ public void setLocaleIndex(String localeIndex) {
+ this.localeIndex = localeIndex;
+ }
+
+ public String[] getLocales() {
+ return locales;
+ }
+
+ public void setLocales(String[] locales) {
+ this.locales = locales;
+ }
+
+ public String getThemeIndex() {
+ return themeIndex;
+ }
+
+ public void setThemeIndex(String themeIndex) {
+ this.themeIndex = themeIndex;
+ }
+
+ public Theme[] getThemes() {
+ return themes;
+ }
+
+ public void setThemes(Theme[] themes) {
+ this.themes = themes;
+ }
+
+ public boolean isReloadNeeded() {
+ return isReloadNeeded;
+ }
+
+ public void setReloadNeeded(boolean reloadNeeded) {
+ isReloadNeeded = reloadNeeded;
+ }
+
+ public boolean isSortAlbumsByYear() {
+ return sortAlbumsByYear;
+ }
+
+ public void setSortAlbumsByYear(boolean sortAlbumsByYear) {
+ this.sortAlbumsByYear = sortAlbumsByYear;
+ }
+
+ public boolean isGettingStartedEnabled() {
+ return gettingStartedEnabled;
+ }
+
+ public void setGettingStartedEnabled(boolean gettingStartedEnabled) {
+ this.gettingStartedEnabled = gettingStartedEnabled;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java
new file mode 100644
index 00000000..8fcfa72c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java
@@ -0,0 +1,187 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import java.io.File;
+import java.util.Date;
+import java.util.List;
+
+import net.sourceforge.subsonic.controller.MusicFolderSettingsController;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Command used in {@link MusicFolderSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class MusicFolderSettingsCommand {
+
+ private String interval;
+ private String hour;
+ private boolean scanning;
+ private boolean fastCache;
+ private boolean organizeByFolderStructure;
+ private List<MusicFolderInfo> musicFolders;
+ private MusicFolderInfo newMusicFolder;
+ private boolean reload;
+
+ public String getInterval() {
+ return interval;
+ }
+
+ public void setInterval(String interval) {
+ this.interval = interval;
+ }
+
+ public String getHour() {
+ return hour;
+ }
+
+ public void setHour(String hour) {
+ this.hour = hour;
+ }
+
+ public boolean isScanning() {
+ return scanning;
+ }
+
+ public void setScanning(boolean scanning) {
+ this.scanning = scanning;
+ }
+
+ public boolean isFastCache() {
+ return fastCache;
+ }
+
+ public List<MusicFolderInfo> getMusicFolders() {
+ return musicFolders;
+ }
+
+ public void setMusicFolders(List<MusicFolderInfo> musicFolders) {
+ this.musicFolders = musicFolders;
+ }
+
+ public void setFastCache(boolean fastCache) {
+ this.fastCache = fastCache;
+ }
+
+ public MusicFolderInfo getNewMusicFolder() {
+ return newMusicFolder;
+ }
+
+ public void setNewMusicFolder(MusicFolderInfo newMusicFolder) {
+ this.newMusicFolder = newMusicFolder;
+ }
+
+ public void setReload(boolean reload) {
+ this.reload = reload;
+ }
+
+ public boolean isReload() {
+ return reload;
+ }
+
+ public boolean isOrganizeByFolderStructure() {
+ return organizeByFolderStructure;
+ }
+
+ public void setOrganizeByFolderStructure(boolean organizeByFolderStructure) {
+ this.organizeByFolderStructure = organizeByFolderStructure;
+ }
+
+ public static class MusicFolderInfo {
+
+ private Integer id;
+ private String path;
+ private String name;
+ private boolean enabled;
+ private boolean delete;
+ private boolean existing;
+
+ public MusicFolderInfo(MusicFolder musicFolder) {
+ id = musicFolder.getId();
+ path = musicFolder.getPath().getPath();
+ name = musicFolder.getName();
+ enabled = musicFolder.isEnabled();
+ existing = musicFolder.getPath().exists() && musicFolder.getPath().isDirectory();
+ }
+
+ public MusicFolderInfo() {
+ enabled = true;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isDelete() {
+ return delete;
+ }
+
+ public void setDelete(boolean delete) {
+ this.delete = delete;
+ }
+
+ public MusicFolder toMusicFolder() {
+ String path = StringUtils.trimToNull(this.path);
+ if (path == null) {
+ return null;
+ }
+ File file = new File(path);
+ String name = StringUtils.trimToNull(this.name);
+ if (name == null) {
+ name = file.getName();
+ }
+ return new MusicFolder(id, new File(path), name, enabled, new Date());
+ }
+
+ public boolean isExisting() {
+ return existing;
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java
new file mode 100644
index 00000000..d0ae2b07
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java
@@ -0,0 +1,92 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class NetworkSettingsCommand {
+
+ private boolean portForwardingEnabled;
+ private boolean urlRedirectionEnabled;
+ private String urlRedirectFrom;
+ private int port;
+ private boolean trial;
+ private Date trialExpires;
+ private boolean trialExpired;
+
+ public void setPortForwardingEnabled(boolean portForwardingEnabled) {
+ this.portForwardingEnabled = portForwardingEnabled;
+ }
+
+ public boolean isPortForwardingEnabled() {
+ return portForwardingEnabled;
+ }
+
+ public boolean isUrlRedirectionEnabled() {
+ return urlRedirectionEnabled;
+ }
+
+ public void setUrlRedirectionEnabled(boolean urlRedirectionEnabled) {
+ this.urlRedirectionEnabled = urlRedirectionEnabled;
+ }
+
+ public String getUrlRedirectFrom() {
+ return urlRedirectFrom;
+ }
+
+ public void setUrlRedirectFrom(String urlRedirectFrom) {
+ this.urlRedirectFrom = urlRedirectFrom;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public void setTrial(boolean trial) {
+ this.trial = trial;
+ }
+
+ public boolean isTrial() {
+ return trial;
+ }
+
+ public void setTrialExpires(Date trialExpires) {
+ this.trialExpires = trialExpires;
+ }
+
+ public Date getTrialExpires() {
+ return trialExpires;
+ }
+
+ public void setTrialExpired(boolean trialExpired) {
+ this.trialExpired = trialExpired;
+ }
+
+ public boolean isTrialExpired() {
+ return trialExpired;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java
new file mode 100644
index 00000000..b9e5383c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java
@@ -0,0 +1,65 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.controller.*;
+
+/**
+ * Command used in {@link PasswordSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class PasswordSettingsCommand {
+ private String username;
+ private String password;
+ private String confirmPassword;
+ private boolean ldapAuthenticated;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getConfirmPassword() {
+ return confirmPassword;
+ }
+
+ public void setConfirmPassword(String confirmPassword) {
+ this.confirmPassword = confirmPassword;
+ }
+
+ public boolean isLdapAuthenticated() {
+ return ldapAuthenticated;
+ }
+
+ public void setLdapAuthenticated(boolean ldapAuthenticated) {
+ this.ldapAuthenticated = ldapAuthenticated;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java
new file mode 100644
index 00000000..680d06e9
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java
@@ -0,0 +1,215 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.controller.PersonalSettingsController;
+import net.sourceforge.subsonic.domain.Avatar;
+import net.sourceforge.subsonic.domain.Theme;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+
+import java.util.List;
+
+/**
+ * Command used in {@link PersonalSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class PersonalSettingsCommand {
+ private User user;
+ private String localeIndex;
+ private String[] locales;
+ private String themeIndex;
+ private Theme[] themes;
+ private int avatarId;
+ private List<Avatar> avatars;
+ private Avatar customAvatar;
+ private UserSettings.Visibility mainVisibility;
+ private UserSettings.Visibility playlistVisibility;
+ private boolean partyModeEnabled;
+ private boolean showNowPlayingEnabled;
+ private boolean showChatEnabled;
+ private boolean nowPlayingAllowed;
+ private boolean finalVersionNotificationEnabled;
+ private boolean betaVersionNotificationEnabled;
+ private boolean lastFmEnabled;
+ private String lastFmUsername;
+ private String lastFmPassword;
+ private boolean isReloadNeeded;
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ public String getLocaleIndex() {
+ return localeIndex;
+ }
+
+ public void setLocaleIndex(String localeIndex) {
+ this.localeIndex = localeIndex;
+ }
+
+ public String[] getLocales() {
+ return locales;
+ }
+
+ public void setLocales(String[] locales) {
+ this.locales = locales;
+ }
+
+ public String getThemeIndex() {
+ return themeIndex;
+ }
+
+ public void setThemeIndex(String themeIndex) {
+ this.themeIndex = themeIndex;
+ }
+
+ public Theme[] getThemes() {
+ return themes;
+ }
+
+ public void setThemes(Theme[] themes) {
+ this.themes = themes;
+ }
+
+ public int getAvatarId() {
+ return avatarId;
+ }
+
+ public void setAvatarId(int avatarId) {
+ this.avatarId = avatarId;
+ }
+
+ public List<Avatar> getAvatars() {
+ return avatars;
+ }
+
+ public void setAvatars(List<Avatar> avatars) {
+ this.avatars = avatars;
+ }
+
+ public Avatar getCustomAvatar() {
+ return customAvatar;
+ }
+
+ public void setCustomAvatar(Avatar customAvatar) {
+ this.customAvatar = customAvatar;
+ }
+
+ public UserSettings.Visibility getMainVisibility() {
+ return mainVisibility;
+ }
+
+ public void setMainVisibility(UserSettings.Visibility mainVisibility) {
+ this.mainVisibility = mainVisibility;
+ }
+
+ public UserSettings.Visibility getPlaylistVisibility() {
+ return playlistVisibility;
+ }
+
+ public void setPlaylistVisibility(UserSettings.Visibility playlistVisibility) {
+ this.playlistVisibility = playlistVisibility;
+ }
+
+ public boolean isPartyModeEnabled() {
+ return partyModeEnabled;
+ }
+
+ public void setPartyModeEnabled(boolean partyModeEnabled) {
+ this.partyModeEnabled = partyModeEnabled;
+ }
+
+ public boolean isShowNowPlayingEnabled() {
+ return showNowPlayingEnabled;
+ }
+
+ public void setShowNowPlayingEnabled(boolean showNowPlayingEnabled) {
+ this.showNowPlayingEnabled = showNowPlayingEnabled;
+ }
+
+ public boolean isShowChatEnabled() {
+ return showChatEnabled;
+ }
+
+ public void setShowChatEnabled(boolean showChatEnabled) {
+ this.showChatEnabled = showChatEnabled;
+ }
+
+ public boolean isNowPlayingAllowed() {
+ return nowPlayingAllowed;
+ }
+
+ public void setNowPlayingAllowed(boolean nowPlayingAllowed) {
+ this.nowPlayingAllowed = nowPlayingAllowed;
+ }
+
+ public boolean isFinalVersionNotificationEnabled() {
+ return finalVersionNotificationEnabled;
+ }
+
+ public void setFinalVersionNotificationEnabled(boolean finalVersionNotificationEnabled) {
+ this.finalVersionNotificationEnabled = finalVersionNotificationEnabled;
+ }
+
+ public boolean isBetaVersionNotificationEnabled() {
+ return betaVersionNotificationEnabled;
+ }
+
+ public void setBetaVersionNotificationEnabled(boolean betaVersionNotificationEnabled) {
+ this.betaVersionNotificationEnabled = betaVersionNotificationEnabled;
+ }
+
+ public boolean isLastFmEnabled() {
+ return lastFmEnabled;
+ }
+
+ public void setLastFmEnabled(boolean lastFmEnabled) {
+ this.lastFmEnabled = lastFmEnabled;
+ }
+
+ public String getLastFmUsername() {
+ return lastFmUsername;
+ }
+
+ public void setLastFmUsername(String lastFmUsername) {
+ this.lastFmUsername = lastFmUsername;
+ }
+
+ public String getLastFmPassword() {
+ return lastFmPassword;
+ }
+
+ public void setLastFmPassword(String lastFmPassword) {
+ this.lastFmPassword = lastFmPassword;
+ }
+
+ public boolean isReloadNeeded() {
+ return isReloadNeeded;
+ }
+
+ public void setReloadNeeded(boolean reloadNeeded) {
+ isReloadNeeded = reloadNeeded;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java
new file mode 100644
index 00000000..8331260d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java
@@ -0,0 +1,250 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import java.util.Date;
+import java.util.List;
+
+import net.sourceforge.subsonic.controller.PlayerSettingsController;
+import net.sourceforge.subsonic.domain.CoverArtScheme;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayerTechnology;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import net.sourceforge.subsonic.domain.Transcoding;
+
+/**
+ * Command used in {@link PlayerSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayerSettingsCommand {
+ private String playerId;
+ private String name;
+ private String description;
+ private String type;
+ private Date lastSeen;
+ private boolean isDynamicIp;
+ private boolean isAutoControlEnabled;
+ private String coverArtSchemeName;
+ private String technologyName;
+ private String transcodeSchemeName;
+ private boolean transcodingSupported;
+ private String transcodeDirectory;
+ private List<Transcoding> allTranscodings;
+ private int[] activeTranscodingIds;
+ private EnumHolder[] technologyHolders;
+ private EnumHolder[] transcodeSchemeHolders;
+ private EnumHolder[] coverArtSchemeHolders;
+ private Player[] players;
+ private boolean isAdmin;
+ private boolean isReloadNeeded;
+
+ public String getPlayerId() {
+ return playerId;
+ }
+
+ public void setPlayerId(String playerId) {
+ this.playerId = playerId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public Date getLastSeen() {
+ return lastSeen;
+ }
+
+ public void setLastSeen(Date lastSeen) {
+ this.lastSeen = lastSeen;
+ }
+
+ public boolean isDynamicIp() {
+ return isDynamicIp;
+ }
+
+ public void setDynamicIp(boolean dynamicIp) {
+ isDynamicIp = dynamicIp;
+ }
+
+ public boolean isAutoControlEnabled() {
+ return isAutoControlEnabled;
+ }
+
+ public void setAutoControlEnabled(boolean autoControlEnabled) {
+ isAutoControlEnabled = autoControlEnabled;
+ }
+
+ public String getCoverArtSchemeName() {
+ return coverArtSchemeName;
+ }
+
+ public void setCoverArtSchemeName(String coverArtSchemeName) {
+ this.coverArtSchemeName = coverArtSchemeName;
+ }
+
+ public String getTranscodeSchemeName() {
+ return transcodeSchemeName;
+ }
+
+ public void setTranscodeSchemeName(String transcodeSchemeName) {
+ this.transcodeSchemeName = transcodeSchemeName;
+ }
+
+ public boolean isTranscodingSupported() {
+ return transcodingSupported;
+ }
+
+ public void setTranscodingSupported(boolean transcodingSupported) {
+ this.transcodingSupported = transcodingSupported;
+ }
+
+ public String getTranscodeDirectory() {
+ return transcodeDirectory;
+ }
+
+ public void setTranscodeDirectory(String transcodeDirectory) {
+ this.transcodeDirectory = transcodeDirectory;
+ }
+
+ public List<Transcoding> getAllTranscodings() {
+ return allTranscodings;
+ }
+
+ public void setAllTranscodings(List<Transcoding> allTranscodings) {
+ this.allTranscodings = allTranscodings;
+ }
+
+ public int[] getActiveTranscodingIds() {
+ return activeTranscodingIds;
+ }
+
+ public void setActiveTranscodingIds(int[] activeTranscodingIds) {
+ this.activeTranscodingIds = activeTranscodingIds;
+ }
+
+ public EnumHolder[] getTechnologyHolders() {
+ return technologyHolders;
+ }
+
+ public void setTechnologies(PlayerTechnology[] technologies) {
+ technologyHolders = new EnumHolder[technologies.length];
+ for (int i = 0; i < technologies.length; i++) {
+ PlayerTechnology technology = technologies[i];
+ technologyHolders[i] = new EnumHolder(technology.name(), technology.toString());
+ }
+ }
+
+ public EnumHolder[] getTranscodeSchemeHolders() {
+ return transcodeSchemeHolders;
+ }
+
+ public void setTranscodeSchemes(TranscodeScheme[] transcodeSchemes) {
+ transcodeSchemeHolders = new EnumHolder[transcodeSchemes.length];
+ for (int i = 0; i < transcodeSchemes.length; i++) {
+ TranscodeScheme scheme = transcodeSchemes[i];
+ transcodeSchemeHolders[i] = new EnumHolder(scheme.name(), scheme.toString());
+ }
+ }
+
+ public EnumHolder[] getCoverArtSchemeHolders() {
+ return coverArtSchemeHolders;
+ }
+
+ public void setCoverArtSchemes(CoverArtScheme[] coverArtSchemes) {
+ coverArtSchemeHolders = new EnumHolder[coverArtSchemes.length];
+ for (int i = 0; i < coverArtSchemes.length; i++) {
+ CoverArtScheme scheme = coverArtSchemes[i];
+ coverArtSchemeHolders[i] = new EnumHolder(scheme.name(), scheme.toString());
+ }
+ }
+
+ public String getTechnologyName() {
+ return technologyName;
+ }
+
+ public void setTechnologyName(String technologyName) {
+ this.technologyName = technologyName;
+ }
+
+ public Player[] getPlayers() {
+ return players;
+ }
+
+ public void setPlayers(Player[] players) {
+ this.players = players;
+ }
+
+ public boolean isAdmin() {
+ return isAdmin;
+ }
+
+ public void setAdmin(boolean admin) {
+ isAdmin = admin;
+ }
+
+ public boolean isReloadNeeded() {
+ return isReloadNeeded;
+ }
+
+ public void setReloadNeeded(boolean reloadNeeded) {
+ isReloadNeeded = reloadNeeded;
+ }
+
+ /**
+ * Holds the transcoding and whether it is active for the given player.
+ */
+ public static class TranscodingHolder {
+ private Transcoding transcoding;
+ private boolean isActive;
+
+ public TranscodingHolder(Transcoding transcoding, boolean isActive) {
+ this.transcoding = transcoding;
+ this.isActive = isActive;
+ }
+
+ public Transcoding getTranscoding() {
+ return transcoding;
+ }
+
+ public boolean isActive() {
+ return isActive;
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.java
new file mode 100644
index 00000000..e6917ff4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.java
@@ -0,0 +1,66 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.controller.PodcastSettingsController;
+
+/**
+ * Command used in {@link PodcastSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastSettingsCommand {
+
+ private String interval;
+ private String folder;
+ private String episodeRetentionCount;
+ private String episodeDownloadCount;
+
+ public String getInterval() {
+ return interval;
+ }
+
+ public void setInterval(String interval) {
+ this.interval = interval;
+ }
+
+ public String getFolder() {
+ return folder;
+ }
+
+ public void setFolder(String folder) {
+ this.folder = folder;
+ }
+
+ public String getEpisodeRetentionCount() {
+ return episodeRetentionCount;
+ }
+
+ public void setEpisodeRetentionCount(String episodeRetentionCount) {
+ this.episodeRetentionCount = episodeRetentionCount;
+ }
+
+ public String getEpisodeDownloadCount() {
+ return episodeDownloadCount;
+ }
+
+ public void setEpisodeDownloadCount(String episodeDownloadCount) {
+ this.episodeDownloadCount = episodeDownloadCount;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java
new file mode 100644
index 00000000..0dacfbd4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java
@@ -0,0 +1,135 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.controller.*;
+
+import java.util.*;
+
+/**
+ * Command used in {@link SearchController}.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchCommand {
+
+ private String query;
+ private List<MediaFile> artists;
+ private List<MediaFile> albums;
+ private List<MediaFile> songs;
+ private boolean isIndexBeingCreated;
+ private User user;
+ private boolean partyModeEnabled;
+ private Player player;
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public boolean isIndexBeingCreated() {
+ return isIndexBeingCreated;
+ }
+
+ public void setIndexBeingCreated(boolean indexBeingCreated) {
+ isIndexBeingCreated = indexBeingCreated;
+ }
+
+ public List<MediaFile> getArtists() {
+ return artists;
+ }
+
+ public void setArtists(List<MediaFile> artists) {
+ this.artists = artists;
+ }
+
+ public List<MediaFile> getAlbums() {
+ return albums;
+ }
+
+ public void setAlbums(List<MediaFile> albums) {
+ this.albums = albums;
+ }
+
+ public List<MediaFile> getSongs() {
+ return songs;
+ }
+
+ public void setSongs(List<MediaFile> songs) {
+ this.songs = songs;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ public boolean isPartyModeEnabled() {
+ return partyModeEnabled;
+ }
+
+ public void setPartyModeEnabled(boolean partyModeEnabled) {
+ this.partyModeEnabled = partyModeEnabled;
+ }
+
+ public Player getPlayer() {
+ return player;
+ }
+
+ public void setPlayer(Player player) {
+ this.player = player;
+ }
+
+ public static class Match {
+ private MediaFile mediaFile;
+ private String title;
+ private String album;
+ private String artist;
+
+ public Match(MediaFile mediaFile, String title, String album, String artist) {
+ this.mediaFile = mediaFile;
+ this.title = title;
+ this.album = album;
+ this.artist = artist;
+ }
+
+ public MediaFile getMediaFile() {
+ return mediaFile;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java
new file mode 100644
index 00000000..ce185f7b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java
@@ -0,0 +1,278 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.command;
+
+import java.util.List;
+
+import net.sourceforge.subsonic.controller.*;
+import net.sourceforge.subsonic.domain.*;
+
+/**
+ * Command used in {@link UserSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class UserSettingsCommand {
+ private String username;
+ private boolean isAdminRole;
+ private boolean isDownloadRole;
+ private boolean isUploadRole;
+ private boolean isCoverArtRole;
+ private boolean isCommentRole;
+ private boolean isPodcastRole;
+ private boolean isStreamRole;
+ private boolean isJukeboxRole;
+ private boolean isSettingsRole;
+ private boolean isShareRole;
+
+ private List<User> users;
+ private boolean isAdmin;
+ private boolean isPasswordChange;
+ private boolean isNew;
+ private boolean isDelete;
+ private String password;
+ private String confirmPassword;
+ private String email;
+ private boolean isLdapAuthenticated;
+ private boolean isLdapEnabled;
+
+ private String transcodeSchemeName;
+ private EnumHolder[] transcodeSchemeHolders;
+ private boolean transcodingSupported;
+ private String transcodeDirectory;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public boolean isAdminRole() {
+ return isAdminRole;
+ }
+
+ public void setAdminRole(boolean adminRole) {
+ isAdminRole = adminRole;
+ }
+
+ public boolean isDownloadRole() {
+ return isDownloadRole;
+ }
+
+ public void setDownloadRole(boolean downloadRole) {
+ isDownloadRole = downloadRole;
+ }
+
+ public boolean isUploadRole() {
+ return isUploadRole;
+ }
+
+ public void setUploadRole(boolean uploadRole) {
+ isUploadRole = uploadRole;
+ }
+
+ public boolean isCoverArtRole() {
+ return isCoverArtRole;
+ }
+
+ public void setCoverArtRole(boolean coverArtRole) {
+ isCoverArtRole = coverArtRole;
+ }
+
+ public boolean isCommentRole() {
+ return isCommentRole;
+ }
+
+ public void setCommentRole(boolean commentRole) {
+ isCommentRole = commentRole;
+ }
+
+ public boolean isPodcastRole() {
+ return isPodcastRole;
+ }
+
+ public void setPodcastRole(boolean podcastRole) {
+ isPodcastRole = podcastRole;
+ }
+
+ public boolean isStreamRole() {
+ return isStreamRole;
+ }
+
+ public void setStreamRole(boolean streamRole) {
+ isStreamRole = streamRole;
+ }
+
+ public boolean isJukeboxRole() {
+ return isJukeboxRole;
+ }
+
+ public void setJukeboxRole(boolean jukeboxRole) {
+ isJukeboxRole = jukeboxRole;
+ }
+
+ public boolean isSettingsRole() {
+ return isSettingsRole;
+ }
+
+ public void setSettingsRole(boolean settingsRole) {
+ isSettingsRole = settingsRole;
+ }
+
+ public boolean isShareRole() {
+ return isShareRole;
+ }
+
+ public void setShareRole(boolean shareRole) {
+ isShareRole = shareRole;
+ }
+
+ public List<User> getUsers() {
+ return users;
+ }
+
+ public void setUsers(List<User> users) {
+ this.users = users;
+ }
+
+ public boolean isAdmin() {
+ return isAdmin;
+ }
+
+ public void setAdmin(boolean admin) {
+ isAdmin = admin;
+ }
+
+ public boolean isPasswordChange() {
+ return isPasswordChange;
+ }
+
+ public void setPasswordChange(boolean passwordChange) {
+ isPasswordChange = passwordChange;
+ }
+
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public void setNew(boolean isNew) {
+ this.isNew = isNew;
+ }
+
+ public boolean isDelete() {
+ return isDelete;
+ }
+
+ public void setDelete(boolean delete) {
+ isDelete = delete;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getConfirmPassword() {
+ return confirmPassword;
+ }
+
+ public void setConfirmPassword(String confirmPassword) {
+ this.confirmPassword = confirmPassword;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public boolean isLdapAuthenticated() {
+ return isLdapAuthenticated;
+ }
+
+ public void setLdapAuthenticated(boolean ldapAuthenticated) {
+ isLdapAuthenticated = ldapAuthenticated;
+ }
+
+ public boolean isLdapEnabled() {
+ return isLdapEnabled;
+ }
+
+ public void setLdapEnabled(boolean ldapEnabled) {
+ isLdapEnabled = ldapEnabled;
+ }
+
+ public String getTranscodeSchemeName() {
+ return transcodeSchemeName;
+ }
+
+ public void setTranscodeSchemeName(String transcodeSchemeName) {
+ this.transcodeSchemeName = transcodeSchemeName;
+ }
+
+ public EnumHolder[] getTranscodeSchemeHolders() {
+ return transcodeSchemeHolders;
+ }
+
+ public void setTranscodeSchemes(TranscodeScheme[] transcodeSchemes) {
+ transcodeSchemeHolders = new EnumHolder[transcodeSchemes.length];
+ for (int i = 0; i < transcodeSchemes.length; i++) {
+ TranscodeScheme scheme = transcodeSchemes[i];
+ transcodeSchemeHolders[i] = new EnumHolder(scheme.name(), scheme.toString());
+ }
+ }
+
+ public boolean isTranscodingSupported() {
+ return transcodingSupported;
+ }
+
+ public void setTranscodingSupported(boolean transcodingSupported) {
+ this.transcodingSupported = transcodingSupported;
+ }
+
+ public String getTranscodeDirectory() {
+ return transcodeDirectory;
+ }
+
+ public void setTranscodeDirectory(String transcodeDirectory) {
+ this.transcodeDirectory = transcodeDirectory;
+ }
+
+ public void setUser(User user) {
+ username = user == null ? null : user.getUsername();
+ isAdminRole = user != null && user.isAdminRole();
+ isDownloadRole = user != null && user.isDownloadRole();
+ isUploadRole = user != null && user.isUploadRole();
+ isCoverArtRole = user != null && user.isCoverArtRole();
+ isCommentRole = user != null && user.isCommentRole();
+ isPodcastRole = user != null && user.isPodcastRole();
+ isStreamRole = user != null && user.isStreamRole();
+ isJukeboxRole = user != null && user.isJukeboxRole();
+ isSettingsRole = user != null && user.isSettingsRole();
+ isShareRole = user != null && user.isShareRole();
+ isLdapAuthenticated = user != null && user.isLdapAuthenticated();
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java
new file mode 100644
index 00000000..f163f82d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java
@@ -0,0 +1,60 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import org.springframework.web.servlet.support.*;
+import org.springframework.web.servlet.mvc.*;
+import org.springframework.ui.context.*;
+
+import javax.servlet.http.*;
+import java.awt.*;
+import java.util.*;
+
+/**
+ * Abstract super class for controllers which generate charts.
+ *
+ * @author Sindre Mehus
+ */
+public abstract class AbstractChartController implements Controller {
+
+ /**
+ * Returns the chart background color for the current theme.
+ * @param request The servlet request.
+ * @return The chart background color.
+ */
+ protected Color getBackground(HttpServletRequest request) {
+ return getColor("backgroundColor", request);
+ }
+
+ /**
+ * Returns the chart foreground color for the current theme.
+ * @param request The servlet request.
+ * @return The chart foreground color.
+ */
+ protected Color getForeground(HttpServletRequest request) {
+ return getColor("textColor", request);
+ }
+
+ private Color getColor(String code, HttpServletRequest request) {
+ Theme theme = RequestContextUtils.getTheme(request);
+ Locale locale = RequestContextUtils.getLocale(request);
+ String colorHex = theme.getMessageSource().getMessage(code, new Object[0], locale);
+ return new Color(Integer.parseInt(colorHex, 16));
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java
new file mode 100644
index 00000000..0b43f4eb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java
@@ -0,0 +1,91 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.command.AdvancedSettingsCommand;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+import org.apache.commons.lang.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Controller for the page used to administrate advanced settings.
+ *
+ * @author Sindre Mehus
+ */
+public class AdvancedSettingsController extends SimpleFormController {
+
+ private SettingsService settingsService;
+
+ @Override
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ AdvancedSettingsCommand command = new AdvancedSettingsCommand();
+ command.setCoverArtLimit(String.valueOf(settingsService.getCoverArtLimit()));
+ command.setDownsampleCommand(settingsService.getDownsamplingCommand());
+ command.setDownloadLimit(String.valueOf(settingsService.getDownloadBitrateLimit()));
+ command.setUploadLimit(String.valueOf(settingsService.getUploadBitrateLimit()));
+ command.setStreamPort(String.valueOf(settingsService.getStreamPort()));
+ command.setLdapEnabled(settingsService.isLdapEnabled());
+ command.setLdapUrl(settingsService.getLdapUrl());
+ command.setLdapSearchFilter(settingsService.getLdapSearchFilter());
+ command.setLdapManagerDn(settingsService.getLdapManagerDn());
+ command.setLdapAutoShadowing(settingsService.isLdapAutoShadowing());
+ command.setBrand(settingsService.getBrand());
+
+ return command;
+ }
+
+ @Override
+ protected void doSubmitAction(Object comm) throws Exception {
+ AdvancedSettingsCommand command = (AdvancedSettingsCommand) comm;
+
+ command.setReloadNeeded(false);
+ settingsService.setDownsamplingCommand(command.getDownsampleCommand());
+
+ try {
+ settingsService.setCoverArtLimit(Integer.parseInt(command.getCoverArtLimit()));
+ } catch (NumberFormatException x) { /* Intentionally ignored. */ }
+ try {
+ settingsService.setDownloadBitrateLimit(Long.parseLong(command.getDownloadLimit()));
+ } catch (NumberFormatException x) { /* Intentionally ignored. */ }
+ try {
+ settingsService.setUploadBitrateLimit(Long.parseLong(command.getUploadLimit()));
+ } catch (NumberFormatException x) { /* Intentionally ignored. */ }
+ try {
+ settingsService.setStreamPort(Integer.parseInt(command.getStreamPort()));
+ } catch (NumberFormatException x) { /* Intentionally ignored. */ }
+
+ settingsService.setLdapEnabled(command.isLdapEnabled());
+ settingsService.setLdapUrl(command.getLdapUrl());
+ settingsService.setLdapSearchFilter(command.getLdapSearchFilter());
+ settingsService.setLdapManagerDn(command.getLdapManagerDn());
+ settingsService.setLdapAutoShadowing(command.isLdapAutoShadowing());
+
+ if (StringUtils.isNotEmpty(command.getLdapManagerPassword())) {
+ settingsService.setLdapManagerPassword(command.getLdapManagerPassword());
+ }
+
+ settingsService.save();
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java
new file mode 100644
index 00000000..8b34f383
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java
@@ -0,0 +1,38 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.mvc.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Controller for the page which forwards to allmusic.com.
+ *
+ * @author Sindre Mehus
+ */
+public class AllmusicController extends ParameterizableViewController {
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("album", request.getParameter("album"));
+ return result;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java
new file mode 100644
index 00000000..100fcedb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java
@@ -0,0 +1,82 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.Avatar;
+import net.sourceforge.subsonic.domain.AvatarScheme;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+import org.springframework.web.servlet.mvc.LastModified;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Controller which produces avatar images.
+ *
+ * @author Sindre Mehus
+ */
+public class AvatarController implements Controller, LastModified {
+
+ private SettingsService settingsService;
+
+ public long getLastModified(HttpServletRequest request) {
+ Avatar avatar = getAvatar(request);
+ return avatar == null ? -1L : avatar.getCreatedDate().getTime();
+ }
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Avatar avatar = getAvatar(request);
+
+ if (avatar == null) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return null;
+ }
+
+ // TODO: specify caching filter.
+
+ response.setContentType(avatar.getMimeType());
+ response.getOutputStream().write(avatar.getData());
+ return null;
+ }
+
+ private Avatar getAvatar(HttpServletRequest request) {
+ String id = request.getParameter("id");
+ if (id != null) {
+ return settingsService.getSystemAvatar(Integer.parseInt(id));
+ }
+
+ String username = request.getParameter("username");
+ if (username == null) {
+ return null;
+ }
+
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ if (userSettings.getAvatarScheme() == AvatarScheme.SYSTEM) {
+ return settingsService.getSystemAvatar(userSettings.getSystemAvatarId());
+ }
+ return settingsService.getCustomAvatar(username);
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java
new file mode 100644
index 00000000..a22cd9a9
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java
@@ -0,0 +1,141 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Avatar;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileItemFactory;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller which receives uploaded avatar images.
+ *
+ * @author Sindre Mehus
+ */
+public class AvatarUploadController extends ParameterizableViewController {
+
+ private static final Logger LOG = Logger.getLogger(AvatarUploadController.class);
+ private static final int MAX_AVATAR_SIZE = 64;
+
+ private SettingsService settingsService;
+ private SecurityService securityService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String username = securityService.getCurrentUsername(request);
+
+ // Check that we have a file upload request.
+ if (!ServletFileUpload.isMultipartContent(request)) {
+ throw new Exception("Illegal request.");
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ FileItemFactory factory = new DiskFileItemFactory();
+ ServletFileUpload upload = new ServletFileUpload(factory);
+ List<?> items = upload.parseRequest(request);
+
+ // Look for file items.
+ for (Object o : items) {
+ FileItem item = (FileItem) o;
+
+ if (!item.isFormField()) {
+ String fileName = item.getName();
+ byte[] data = item.get();
+
+ if (StringUtils.isNotBlank(fileName) && data.length > 0) {
+ createAvatar(fileName, data, username, map);
+ } else {
+ map.put("error", new Exception("Missing file."));
+ LOG.warn("Failed to upload personal image. No file specified.");
+ }
+ break;
+ }
+ }
+
+ map.put("username", username);
+ map.put("avatar", settingsService.getCustomAvatar(username));
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ private void createAvatar(String fileName, byte[] data, String username, Map<String, Object> map) throws IOException {
+
+ BufferedImage image;
+ try {
+ image = ImageIO.read(new ByteArrayInputStream(data));
+ if (image == null) {
+ throw new Exception("Failed to decode incoming image: " + fileName + " (" + data.length + " bytes).");
+ }
+ int width = image.getWidth();
+ int height = image.getHeight();
+ String mimeType = StringUtil.getMimeType(FilenameUtils.getExtension(fileName));
+
+ // Scale down image if necessary.
+ if (width > MAX_AVATAR_SIZE || height > MAX_AVATAR_SIZE) {
+ double scaleFactor = (double) MAX_AVATAR_SIZE / (double) Math.max(width, height);
+ height = (int) (height * scaleFactor);
+ width = (int) (width * scaleFactor);
+ image = CoverArtController.scale(image, width, height);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ImageIO.write(image, "jpeg", out);
+ data = out.toByteArray();
+ mimeType = StringUtil.getMimeType("jpeg");
+ map.put("resized", true);
+ }
+ Avatar avatar = new Avatar(0, fileName, new Date(), mimeType, width, height, data);
+ settingsService.setCustomAvatar(avatar, username);
+ LOG.info("Created avatar '" + fileName + "' (" + data.length + " bytes) for user " + username);
+
+ } catch (Exception x) {
+ LOG.warn("Failed to upload personal image: " + x, x);
+ map.put("error", x);
+ }
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java
new file mode 100644
index 00000000..94c88656
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java
@@ -0,0 +1,72 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.MediaFileService;
+
+/**
+ * Controller for changing cover art.
+ *
+ * @author Sindre Mehus
+ */
+public class ChangeCoverArtController extends ParameterizableViewController {
+
+ private MediaFileService mediaFileService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ String artist = request.getParameter("artist");
+ String album = request.getParameter("album");
+ MediaFile dir = mediaFileService.getMediaFile(id);
+
+ if (artist == null) {
+ artist = dir.getArtist();
+ }
+ if (album == null) {
+ album = dir.getAlbumName();
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("id", id);
+ map.put("artist", artist);
+ map.put("album", album);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+
+ return result;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java
new file mode 100644
index 00000000..a5093024
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java
@@ -0,0 +1,294 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.AlbumDao;
+import net.sourceforge.subsonic.dao.ArtistDao;
+import net.sourceforge.subsonic.domain.Album;
+import net.sourceforge.subsonic.domain.Artist;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser;
+import net.sourceforge.subsonic.util.FileUtil;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+import org.springframework.web.servlet.mvc.LastModified;
+
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Controller which produces cover art images.
+ *
+ * @author Sindre Mehus
+ */
+public class CoverArtController implements Controller, LastModified {
+
+ public static final String ALBUM_COVERART_PREFIX = "al-";
+ public static final String ARTIST_COVERART_PREFIX = "ar-";
+
+ private static final Logger LOG = Logger.getLogger(CoverArtController.class);
+
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+ private ArtistDao artistDao;
+ private AlbumDao albumDao;
+
+ public long getLastModified(HttpServletRequest request) {
+ try {
+ File file = getImageFile(request);
+ if (file == null) {
+ return 0; // Request for the default image.
+ }
+ if (!FileUtil.exists(file)) {
+ return -1;
+ }
+
+ return FileUtil.lastModified(file);
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ File file = getImageFile(request);
+
+ if (file != null && !FileUtil.exists(file)) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return null;
+ }
+
+ // Check access.
+ if (file != null && !securityService.isReadAllowed(file)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return null;
+ }
+
+ // Send default image if no path is given. (No need to cache it, since it will be cached in browser.)
+ Integer size = ServletRequestUtils.getIntParameter(request, "size");
+ if (file == null) {
+ sendDefault(size, response);
+ return null;
+ }
+
+ // Optimize if no scaling is required.
+ if (size == null) {
+ sendUnscaled(file, response);
+ return null;
+ }
+
+ // Send cached image, creating it if necessary.
+ try {
+ File cachedImage = getCachedImage(file, size);
+ sendImage(cachedImage, response);
+ } catch (IOException e) {
+ sendDefault(size, response);
+ }
+
+ return null;
+ }
+
+ private File getImageFile(HttpServletRequest request) {
+ String id = request.getParameter("id");
+ if (id != null) {
+ if (id.startsWith(ALBUM_COVERART_PREFIX)) {
+ return getAlbumImage(Integer.valueOf(id.replace(ALBUM_COVERART_PREFIX, "")));
+ }
+ if (id.startsWith(ARTIST_COVERART_PREFIX)) {
+ return getArtistImage(Integer.valueOf(id.replace(ARTIST_COVERART_PREFIX, "")));
+ }
+ return getMediaFileImage(Integer.valueOf(id));
+ }
+
+ String path = StringUtils.trimToNull(request.getParameter("path"));
+ return path != null ? new File(path) : null;
+ }
+
+ private File getArtistImage(int id) {
+ Artist artist = artistDao.getArtist(id);
+ return artist == null || artist.getCoverArtPath() == null ? null : new File(artist.getCoverArtPath());
+ }
+
+ private File getAlbumImage(int id) {
+ Album album = albumDao.getAlbum(id);
+ return album == null || album.getCoverArtPath() == null ? null : new File(album.getCoverArtPath());
+ }
+
+ private File getMediaFileImage(int id) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(id);
+ return mediaFile == null ? null : mediaFileService.getCoverArt(mediaFile);
+ }
+
+ private void sendImage(File file, HttpServletResponse response) throws IOException {
+ InputStream in = new FileInputStream(file);
+ try {
+ IOUtils.copy(in, response.getOutputStream());
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ private void sendDefault(Integer size, HttpServletResponse response) throws IOException {
+ InputStream in = null;
+ try {
+ in = getClass().getResourceAsStream("default_cover.jpg");
+ BufferedImage image = ImageIO.read(in);
+ if (size != null) {
+ image = scale(image, size, size);
+ }
+ ImageIO.write(image, "jpeg", response.getOutputStream());
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ private void sendUnscaled(File file, HttpServletResponse response) throws IOException {
+ InputStream in = null;
+ try {
+ in = getImageInputStream(file);
+ IOUtils.copy(in, response.getOutputStream());
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ private File getCachedImage(File file, int size) throws IOException {
+ String md5 = DigestUtils.md5Hex(file.getPath());
+ File cachedImage = new File(getImageCacheDirectory(size), md5 + ".jpeg");
+
+ // Is cache missing or obsolete?
+ if (!cachedImage.exists() || FileUtil.lastModified(file) > cachedImage.lastModified()) {
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ in = getImageInputStream(file);
+ out = new FileOutputStream(cachedImage);
+ BufferedImage image = ImageIO.read(in);
+ if (image == null) {
+ throw new Exception("Unable to decode image.");
+ }
+
+ image = scale(image, size, size);
+ ImageIO.write(image, "jpeg", out);
+
+ } catch (Throwable x) {
+ // Delete corrupt (probably empty) thumbnail cache.
+ LOG.warn("Failed to create thumbnail for " + file, x);
+ IOUtils.closeQuietly(out);
+ cachedImage.delete();
+ throw new IOException("Failed to create thumbnail for " + file + ". " + x.getMessage());
+
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(out);
+ }
+ }
+ return cachedImage;
+ }
+
+ /**
+ * Returns an input stream to the image in the given file. If the file is an audio file,
+ * the embedded album art is returned.
+ */
+ private InputStream getImageInputStream(File file) throws IOException {
+ JaudiotaggerParser parser = new JaudiotaggerParser();
+ if (parser.isApplicable(file)) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(file);
+ return new ByteArrayInputStream(parser.getImageData(mediaFile));
+ } else {
+ return new FileInputStream(file);
+ }
+ }
+
+ private synchronized File getImageCacheDirectory(int size) {
+ File dir = new File(SettingsService.getSubsonicHome(), "thumbs");
+ dir = new File(dir, String.valueOf(size));
+ if (!dir.exists()) {
+ if (dir.mkdirs()) {
+ LOG.info("Created thumbnail cache " + dir);
+ } else {
+ LOG.error("Failed to create thumbnail cache " + dir);
+ }
+ }
+
+ return dir;
+ }
+
+ public static BufferedImage scale(BufferedImage image, int width, int height) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+ BufferedImage thumb = image;
+
+ // For optimal results, use step by step bilinear resampling - halfing the size at each step.
+ do {
+ w /= 2;
+ h /= 2;
+ if (w < width) {
+ w = width;
+ }
+ if (h < height) {
+ h = height;
+ }
+
+ BufferedImage temp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
+ Graphics2D g2 = temp.createGraphics();
+ g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null);
+ g2.dispose();
+
+ thumb = temp;
+ } while (w != width);
+
+ return thumb;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setArtistDao(ArtistDao artistDao) {
+ this.artistDao = artistDao;
+ }
+
+ public void setAlbumDao(AlbumDao albumDao) {
+ this.albumDao = albumDao;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java
new file mode 100644
index 00000000..17d06497
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java
@@ -0,0 +1,66 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.dao.DaoHelper;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jdbc.core.ColumnMapRowMapper;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for the DB admin page.
+ *
+ * @author Sindre Mehus
+ */
+public class DBController extends ParameterizableViewController {
+
+ private DaoHelper daoHelper;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ String query = request.getParameter("query");
+ if (query != null) {
+ map.put("query", query);
+
+ try {
+ List<?> result = daoHelper.getJdbcTemplate().query(query, new ColumnMapRowMapper());
+ map.put("result", result);
+ } catch (DataAccessException x) {
+ map.put("error", ExceptionUtils.getRootCause(x).getMessage());
+ }
+ }
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setDaoHelper(DaoHelper daoHelper) {
+ this.daoHelper = daoHelper;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java
new file mode 100644
index 00000000..144d3327
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java
@@ -0,0 +1,74 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.command.DonateCommand;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.validation.BindException;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Date;
+
+/**
+ * Controller for the donation page.
+ *
+ * @author Sindre Mehus
+ */
+public class DonateController extends SimpleFormController {
+
+ private SettingsService settingsService;
+
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ DonateCommand command = new DonateCommand();
+ command.setPath(request.getParameter("path"));
+
+ command.setEmailAddress(settingsService.getLicenseEmail());
+ command.setLicenseDate(settingsService.getLicenseDate());
+ command.setLicenseValid(settingsService.isLicenseValid());
+ command.setLicense(settingsService.getLicenseCode());
+ command.setBrand(settingsService.getBrand());
+
+ return command;
+ }
+
+ protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors)
+ throws Exception {
+ DonateCommand command = (DonateCommand) com;
+ Date now = new Date();
+
+ settingsService.setLicenseCode(command.getLicense());
+ settingsService.setLicenseEmail(command.getEmailAddress());
+ settingsService.setLicenseDate(now);
+ settingsService.save();
+ settingsService.validateLicenseAsync();
+
+ // Reflect changes in view. The validator has already validated the license.
+ command.setLicenseValid(true);
+ command.setLicenseDate(now);
+
+ return new ModelAndView(getSuccessView(), errors.getModel());
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java
new file mode 100644
index 00000000..0125d3bb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java
@@ -0,0 +1,453 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.io.RangeOutputStream;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.StatusService;
+import net.sourceforge.subsonic.util.FileUtil;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.Util;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.math.LongRange;
+import org.apache.tools.zip.ZipEntry;
+import org.apache.tools.zip.ZipOutputStream;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+import org.springframework.web.servlet.mvc.LastModified;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+
+/**
+ * A controller used for downloading files to a remote client. If the requested path refers to a file, the
+ * given file is downloaded. If the requested path refers to a directory, the entire directory (including
+ * sub-directories) are downloaded as an uncompressed zip-file.
+ *
+ * @author Sindre Mehus
+ */
+public class DownloadController implements Controller, LastModified {
+
+ private static final Logger LOG = Logger.getLogger(DownloadController.class);
+
+ private PlayerService playerService;
+ private StatusService statusService;
+ private SecurityService securityService;
+ private PlaylistService playlistService;
+ private SettingsService settingsService;
+ private MediaFileService mediaFileService;
+
+ public long getLastModified(HttpServletRequest request) {
+ try {
+ MediaFile mediaFile = getSingleFile(request);
+ if (mediaFile == null || mediaFile.isDirectory() || mediaFile.getChanged() == null) {
+ return -1;
+ }
+ return mediaFile.getChanged().getTime();
+ } catch (ServletRequestBindingException e) {
+ return -1;
+ }
+ }
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ TransferStatus status = null;
+ try {
+
+ status = statusService.createDownloadStatus(playerService.getPlayer(request, response, false, false));
+
+ MediaFile mediaFile = getSingleFile(request);
+ String dir = request.getParameter("dir");
+ Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist");
+ String playerId = request.getParameter("player");
+ int[] indexes = ServletRequestUtils.getIntParameters(request, "i");
+
+ if (mediaFile != null) {
+ response.setIntHeader("ETag", mediaFile.getId());
+ response.setHeader("Accept-Ranges", "bytes");
+ }
+
+ LongRange range = StringUtil.parseRange(request.getHeader("Range"));
+ if (range != null) {
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ LOG.info("Got range: " + range);
+ }
+
+ if (mediaFile != null) {
+ File file = mediaFile.getFile();
+ if (!securityService.isReadAllowed(file)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return null;
+ }
+
+ if (file.isFile()) {
+ downloadFile(response, status, file, range);
+ } else {
+ downloadDirectory(response, status, file, range);
+ }
+ } else if (dir != null) {
+ File file = new File(dir);
+ if (!securityService.isReadAllowed(file)) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return null;
+ }
+ downloadFiles(response, status, file, indexes);
+
+ } else if (playlistId != null) {
+ List<MediaFile> songs = playlistService.getFilesInPlaylist(playlistId);
+ downloadFiles(response, status, songs, null, range);
+
+ } else if (playerId != null) {
+ Player player = playerService.getPlayerById(playerId);
+ PlayQueue playQueue = player.getPlayQueue();
+ playQueue.setName("Playlist");
+ downloadFiles(response, status, playQueue.getFiles(), indexes.length == 0 ? null : indexes, range);
+ }
+
+
+ } finally {
+ if (status != null) {
+ statusService.removeDownloadStatus(status);
+ User user = securityService.getCurrentUser(request);
+ securityService.updateUserByteCounts(user, 0L, status.getBytesTransfered(), 0L);
+ }
+ }
+
+ return null;
+ }
+
+ private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException {
+ String path = request.getParameter("path");
+ if (path != null) {
+ return mediaFileService.getMediaFile(path);
+ }
+ Integer id = ServletRequestUtils.getIntParameter(request, "id");
+ if (id != null) {
+ return mediaFileService.getMediaFile(id);
+ }
+ return null;
+ }
+
+ /**
+ * Downloads a single file.
+ *
+ * @param response The HTTP response.
+ * @param status The download status.
+ * @param file The file to download.
+ * @param range The byte range, may be <code>null</code>.
+ * @throws IOException If an I/O error occurs.
+ */
+ private void downloadFile(HttpServletResponse response, TransferStatus status, File file, LongRange range) throws IOException {
+ LOG.info("Starting to download '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer());
+ status.setFile(file);
+
+ response.setContentType("application/x-download");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + '\"');
+ if (range == null) {
+ Util.setContentLength(response, file.length());
+ }
+
+ copyFileToStream(file, RangeOutputStream.wrap(response.getOutputStream(), range), status, range);
+ LOG.info("Downloaded '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer());
+ }
+
+ /**
+ * Downloads a collection of files within a directory.
+ *
+ * @param response The HTTP response.
+ * @param status The download status.
+ * @param dir The directory.
+ * @param indexes Only download files with these indexes within the directory.
+ * @throws IOException If an I/O error occurs.
+ */
+ private void downloadFiles(HttpServletResponse response, TransferStatus status, File dir, int[] indexes) throws IOException {
+ String zipFileName = dir.getName() + ".zip";
+ LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer());
+ status.setFile(dir);
+
+ response.setContentType("application/x-download");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + "\"");
+
+ ZipOutputStream out = new ZipOutputStream(response.getOutputStream());
+ out.setMethod(ZipOutputStream.STORED); // No compression.
+
+ List<MediaFile> allChildren = mediaFileService.getChildrenOf(dir, true, true, true);
+ List<MediaFile> mediaFiles = new ArrayList<MediaFile>();
+ for (int index : indexes) {
+ mediaFiles.add(allChildren.get(index));
+ }
+
+ for (MediaFile mediaFile : mediaFiles) {
+ zip(out, mediaFile.getParentFile(), mediaFile.getFile(), status, null);
+ }
+
+ out.close();
+ LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer());
+ }
+
+ /**
+ * Downloads all files in a directory (including sub-directories). The files are packed together in an
+ * uncompressed zip-file.
+ *
+ * @param response The HTTP response.
+ * @param status The download status.
+ * @param file The file to download.
+ * @param range The byte range, may be <code>null</code>.
+ * @throws IOException If an I/O error occurs.
+ */
+ private void downloadDirectory(HttpServletResponse response, TransferStatus status, File file, LongRange range) throws IOException {
+ String zipFileName = file.getName() + ".zip";
+ LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer());
+ response.setContentType("application/x-download");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + '"');
+
+ ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range));
+ out.setMethod(ZipOutputStream.STORED); // No compression.
+
+ zip(out, file.getParentFile(), file, status, range);
+ out.close();
+ LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer());
+ }
+
+ /**
+ * Downloads the given files. The files are packed together in an
+ * uncompressed zip-file.
+ *
+ * @param response The HTTP response.
+ * @param status The download status.
+ * @param files The files to download.
+ * @param indexes Only download songs at these indexes. May be <code>null</code>.
+ * @param range The byte range, may be <code>null</code>.
+ * @throws IOException If an I/O error occurs.
+ */
+ private void downloadFiles(HttpServletResponse response, TransferStatus status, List<MediaFile> files, int[] indexes, LongRange range) throws IOException {
+ if (indexes != null && indexes.length == 1) {
+ downloadFile(response, status, files.get(indexes[0]).getFile(), range);
+ return;
+ }
+
+ String zipFileName = "download.zip";
+ LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer());
+ response.setContentType("application/x-download");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + '"');
+
+ ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range));
+ out.setMethod(ZipOutputStream.STORED); // No compression.
+
+ List<MediaFile> filesToDownload = new ArrayList<MediaFile>();
+ if (indexes == null) {
+ filesToDownload.addAll(files);
+ } else {
+ for (int index : indexes) {
+ try {
+ filesToDownload.add(files.get(index));
+ } catch (IndexOutOfBoundsException x) { /* Ignored */}
+ }
+ }
+
+ for (MediaFile mediaFile : filesToDownload) {
+ zip(out, mediaFile.getParentFile(), mediaFile.getFile(), status, range);
+ }
+
+ out.close();
+ LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer());
+ }
+
+ /**
+ * Utility method for writing the content of a given file to a given output stream.
+ *
+ * @param file The file to copy.
+ * @param out The output stream to write to.
+ * @param status The download status.
+ * @param range The byte range, may be <code>null</code>.
+ * @throws IOException If an I/O error occurs.
+ */
+ private void copyFileToStream(File file, OutputStream out, TransferStatus status, LongRange range) throws IOException {
+ LOG.info("Downloading '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer());
+
+ final int bufferSize = 16 * 1024; // 16 Kbit
+ InputStream in = new BufferedInputStream(new FileInputStream(file), bufferSize);
+
+ try {
+ byte[] buf = new byte[bufferSize];
+ long bitrateLimit = 0;
+ long lastLimitCheck = 0;
+
+ while (true) {
+ long before = System.currentTimeMillis();
+ int n = in.read(buf);
+ if (n == -1) {
+ break;
+ }
+ out.write(buf, 0, n);
+
+ // Don't sleep if outside range.
+ if (range != null && !range.containsLong(status.getBytesSkipped() + status.getBytesTransfered())) {
+ status.addBytesSkipped(n);
+ continue;
+ }
+
+ status.addBytesTransfered(n);
+ long after = System.currentTimeMillis();
+
+ // Calculate bitrate limit every 5 seconds.
+ if (after - lastLimitCheck > 5000) {
+ bitrateLimit = 1024L * settingsService.getDownloadBitrateLimit() /
+ Math.max(1, statusService.getAllDownloadStatuses().size());
+ lastLimitCheck = after;
+ }
+
+ // Sleep for a while to throttle bitrate.
+ if (bitrateLimit != 0) {
+ long sleepTime = 8L * 1000 * bufferSize / bitrateLimit - (after - before);
+ if (sleepTime > 0L) {
+ try {
+ Thread.sleep(sleepTime);
+ } catch (Exception x) {
+ LOG.warn("Failed to sleep.", x);
+ }
+ }
+ }
+ }
+ } finally {
+ out.flush();
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ /**
+ * Writes a file or a directory structure to a zip output stream. File entries in the zip file are relative
+ * to the given root.
+ *
+ * @param out The zip output stream.
+ * @param root The root of the directory structure. Used to create path information in the zip file.
+ * @param file The file or directory to zip.
+ * @param status The download status.
+ * @param range The byte range, may be <code>null</code>.
+ * @throws IOException If an I/O error occurs.
+ */
+ private void zip(ZipOutputStream out, File root, File file, TransferStatus status, LongRange range) throws IOException {
+
+ // Exclude all hidden files starting with a "."
+ if (file.getName().startsWith(".")) {
+ return;
+ }
+
+ String zipName = file.getCanonicalPath().substring(root.getCanonicalPath().length() + 1);
+
+ if (file.isFile()) {
+ status.setFile(file);
+
+ ZipEntry zipEntry = new ZipEntry(zipName);
+ zipEntry.setSize(file.length());
+ zipEntry.setCompressedSize(file.length());
+ zipEntry.setCrc(computeCrc(file));
+
+ out.putNextEntry(zipEntry);
+ copyFileToStream(file, out, status, range);
+ out.closeEntry();
+
+ } else {
+ ZipEntry zipEntry = new ZipEntry(zipName + '/');
+ zipEntry.setSize(0);
+ zipEntry.setCompressedSize(0);
+ zipEntry.setCrc(0);
+
+ out.putNextEntry(zipEntry);
+ out.closeEntry();
+
+ File[] children = FileUtil.listFiles(file);
+ for (File child : children) {
+ zip(out, root, child, status, range);
+ }
+ }
+ }
+
+ /**
+ * Computes the CRC checksum for the given file.
+ *
+ * @param file The file to compute checksum for.
+ * @return A CRC32 checksum.
+ * @throws IOException If an I/O error occurs.
+ */
+ private long computeCrc(File file) throws IOException {
+ CRC32 crc = new CRC32();
+ InputStream in = new FileInputStream(file);
+
+ try {
+
+ byte[] buf = new byte[8192];
+ int n = in.read(buf);
+ while (n != -1) {
+ crc.update(buf, 0, n);
+ n = in.read(buf);
+ }
+
+ } finally {
+ in.close();
+ }
+
+ return crc.getValue();
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java
new file mode 100644
index 00000000..91492222
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java
@@ -0,0 +1,194 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.service.metadata.MetaData;
+import net.sourceforge.subsonic.service.metadata.MetaDataParser;
+import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory;
+import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser;
+
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.mvc.*;
+
+import javax.servlet.http.*;
+import java.util.*;
+
+/**
+ * Controller for the page used to edit MP3 tags.
+ *
+ * @author Sindre Mehus
+ */
+public class EditTagsController extends ParameterizableViewController {
+
+ private MetaDataParserFactory metaDataParserFactory;
+ private MediaFileService mediaFileService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ MediaFile dir = mediaFileService.getMediaFile(id);
+ List<MediaFile> files = mediaFileService.getChildrenOf(dir, true, false, true);
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ if (!files.isEmpty()) {
+ map.put("defaultArtist", files.get(0).getArtist());
+ map.put("defaultAlbum", files.get(0).getAlbumName());
+ map.put("defaultYear", files.get(0).getYear());
+ map.put("defaultGenre", files.get(0).getGenre());
+ }
+ map.put("allGenres", JaudiotaggerParser.getID3V1Genres());
+
+ List<Song> songs = new ArrayList<Song>();
+ for (int i = 0; i < files.size(); i++) {
+ songs.add(createSong(files.get(i), i));
+ }
+ map.put("id", id);
+ map.put("songs", songs);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ private Song createSong(MediaFile file, int index) {
+ MetaDataParser parser = metaDataParserFactory.getParser(file.getFile());
+ MetaData metaData = parser.getRawMetaData(file.getFile());
+
+ Song song = new Song();
+ song.setId(file.getId());
+ song.setFileName(FilenameUtils.getBaseName(file.getPath()));
+ song.setTrack(metaData.getTrackNumber());
+ song.setSuggestedTrack(index + 1);
+ song.setTitle(metaData.getTitle());
+ song.setSuggestedTitle(parser.guessTitle(file.getFile()));
+ song.setArtist(metaData.getArtist());
+ song.setAlbum(metaData.getAlbumName());
+ song.setYear(metaData.getYear());
+ song.setGenre(metaData.getGenre());
+ return song;
+ }
+
+ public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) {
+ this.metaDataParserFactory = metaDataParserFactory;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ /**
+ * Contains information about a single song.
+ */
+ public static class Song {
+ private int id;
+ private String fileName;
+ private Integer suggestedTrack;
+ private Integer track;
+ private String suggestedTitle;
+ private String title;
+ private String artist;
+ private String album;
+ private Integer year;
+ private String genre;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public Integer getSuggestedTrack() {
+ return suggestedTrack;
+ }
+
+ public void setSuggestedTrack(Integer suggestedTrack) {
+ this.suggestedTrack = suggestedTrack;
+ }
+
+ public Integer getTrack() {
+ return track;
+ }
+
+ public void setTrack(Integer track) {
+ this.track = track;
+ }
+
+ public String getSuggestedTitle() {
+ return suggestedTitle;
+ }
+
+ public void setSuggestedTitle(String suggestedTitle) {
+ this.suggestedTitle = suggestedTitle;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getAlbum() {
+ return album;
+ }
+
+ public void setAlbum(String album) {
+ this.album = album;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java
new file mode 100644
index 00000000..d8d28f93
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java
@@ -0,0 +1,179 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.ShareDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.Share;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.RandomStringUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for the page used to play shared music (Twitter, Facebook etc).
+ *
+ * @author Sindre Mehus
+ */
+public class ExternalPlayerController extends ParameterizableViewController {
+
+ private static final Logger LOG = Logger.getLogger(ExternalPlayerController.class);
+ private static final String GUEST_USERNAME = "guest";
+
+ private SettingsService settingsService;
+ private SecurityService securityService;
+ private PlayerService playerService;
+ private ShareDao shareDao;
+ private MediaFileService mediaFileService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ String pathInfo = request.getPathInfo();
+
+ if (pathInfo == null || !pathInfo.startsWith("/")) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return null;
+ }
+
+ Share share = shareDao.getShareByName(pathInfo.substring(1));
+ if (share == null) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return null;
+ }
+
+ if (share.getExpires() != null && share.getExpires().before(new Date())) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return null;
+ }
+
+ share.setLastVisited(new Date());
+ share.setVisitCount(share.getVisitCount() + 1);
+ shareDao.updateShare(share);
+
+ List<MediaFile> songs = getSongs(share);
+ List<File> coverArts = getCoverArts(songs);
+
+ map.put("share", share);
+ map.put("songs", songs);
+ map.put("coverArts", coverArts);
+
+ if (!coverArts.isEmpty()) {
+ map.put("coverArt", coverArts.get(0));
+ }
+ map.put("redirectFrom", settingsService.getUrlRedirectFrom());
+ map.put("player", getPlayer(request).getId());
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ private List<MediaFile> getSongs(Share share) throws IOException {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+
+ for (String path : shareDao.getSharedFiles(share.getId())) {
+ try {
+ MediaFile file = mediaFileService.getMediaFile(path);
+ if (file.getFile().exists()) {
+ if (file.isDirectory()) {
+ result.addAll(mediaFileService.getChildrenOf(file, true, false, true));
+ } else {
+ result.add(file);
+ }
+ }
+ } catch (Exception x) {
+ LOG.warn("Couldn't read file " + path);
+ }
+ }
+ return result;
+ }
+
+ private List<File> getCoverArts(List<MediaFile> songs) throws IOException {
+ List<File> result = new ArrayList<File>();
+ for (MediaFile song : songs) {
+ result.add(mediaFileService.getCoverArt(song));
+ }
+ return result;
+ }
+
+
+ private Player getPlayer(HttpServletRequest request) {
+
+ // Create guest user if necessary.
+ User user = securityService.getUserByName(GUEST_USERNAME);
+ if (user == null) {
+ user = new User(GUEST_USERNAME, RandomStringUtils.randomAlphanumeric(30), null);
+ user.setStreamRole(true);
+ securityService.createUser(user);
+ }
+
+ // Look for existing player.
+ List<Player> players = playerService.getPlayersForUserAndClientId(GUEST_USERNAME, null);
+ if (!players.isEmpty()) {
+ return players.get(0);
+ }
+
+ // Create player if necessary.
+ Player player = new Player();
+ player.setIpAddress(request.getRemoteAddr());
+ player.setUsername(GUEST_USERNAME);
+ playerService.createPlayer(player);
+
+ return player;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setShareDao(ShareDao shareDao) {
+ this.shareDao = shareDao;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java
new file mode 100644
index 00000000..e7b19b04
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java
@@ -0,0 +1,114 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.command.GeneralSettingsCommand;
+import net.sourceforge.subsonic.domain.Theme;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Locale;
+
+/**
+ * Controller for the page used to administrate general settings.
+ *
+ * @author Sindre Mehus
+ */
+public class GeneralSettingsController extends SimpleFormController {
+
+ private SettingsService settingsService;
+
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ GeneralSettingsCommand command = new GeneralSettingsCommand();
+ command.setCoverArtFileTypes(settingsService.getCoverArtFileTypes());
+ command.setIgnoredArticles(settingsService.getIgnoredArticles());
+ command.setShortcuts(settingsService.getShortcuts());
+ command.setIndex(settingsService.getIndexString());
+ command.setMusicFileTypes(settingsService.getMusicFileTypes());
+ command.setVideoFileTypes(settingsService.getVideoFileTypes());
+ command.setSortAlbumsByYear(settingsService.isSortAlbumsByYear());
+ command.setGettingStartedEnabled(settingsService.isGettingStartedEnabled());
+ command.setWelcomeTitle(settingsService.getWelcomeTitle());
+ command.setWelcomeSubtitle(settingsService.getWelcomeSubtitle());
+ command.setWelcomeMessage(settingsService.getWelcomeMessage());
+ command.setLoginMessage(settingsService.getLoginMessage());
+
+ Theme[] themes = settingsService.getAvailableThemes();
+ command.setThemes(themes);
+ String currentThemeId = settingsService.getThemeId();
+ for (int i = 0; i < themes.length; i++) {
+ if (currentThemeId.equals(themes[i].getId())) {
+ command.setThemeIndex(String.valueOf(i));
+ break;
+ }
+ }
+
+ Locale currentLocale = settingsService.getLocale();
+ Locale[] locales = settingsService.getAvailableLocales();
+ String[] localeStrings = new String[locales.length];
+ for (int i = 0; i < locales.length; i++) {
+ localeStrings[i] = locales[i].getDisplayName(locales[i]);
+
+ if (currentLocale.equals(locales[i])) {
+ command.setLocaleIndex(String.valueOf(i));
+ }
+ }
+ command.setLocales(localeStrings);
+
+ return command;
+
+ }
+
+ protected void doSubmitAction(Object comm) throws Exception {
+ GeneralSettingsCommand command = (GeneralSettingsCommand) comm;
+
+ int themeIndex = Integer.parseInt(command.getThemeIndex());
+ Theme theme = settingsService.getAvailableThemes()[themeIndex];
+
+ int localeIndex = Integer.parseInt(command.getLocaleIndex());
+ Locale locale = settingsService.getAvailableLocales()[localeIndex];
+
+ command.setReloadNeeded(!settingsService.getIndexString().equals(command.getIndex()) ||
+ !settingsService.getIgnoredArticles().equals(command.getIgnoredArticles()) ||
+ !settingsService.getShortcuts().equals(command.getShortcuts()) ||
+ !settingsService.getThemeId().equals(theme.getId()) ||
+ !settingsService.getLocale().equals(locale));
+
+ settingsService.setIndexString(command.getIndex());
+ settingsService.setIgnoredArticles(command.getIgnoredArticles());
+ settingsService.setShortcuts(command.getShortcuts());
+ settingsService.setMusicFileTypes(command.getMusicFileTypes());
+ settingsService.setVideoFileTypes(command.getVideoFileTypes());
+ settingsService.setCoverArtFileTypes(command.getCoverArtFileTypes());
+ settingsService.setSortAlbumsByYear(command.isSortAlbumsByYear());
+ settingsService.setGettingStartedEnabled(command.isGettingStartedEnabled());
+ settingsService.setWelcomeTitle(command.getWelcomeTitle());
+ settingsService.setWelcomeSubtitle(command.getWelcomeSubtitle());
+ settingsService.setWelcomeMessage(command.getWelcomeMessage());
+ settingsService.setLoginMessage(command.getLoginMessage());
+ settingsService.setThemeId(theme.getId());
+ settingsService.setLocale(locale);
+ settingsService.save();
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java
new file mode 100644
index 00000000..4e0b0945
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java
@@ -0,0 +1,80 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.*;
+import net.sourceforge.subsonic.service.*;
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.mvc.*;
+
+import javax.servlet.http.*;
+import java.util.*;
+
+/**
+ * Controller for the help page.
+ *
+ * @author Sindre Mehus
+ */
+public class HelpController extends ParameterizableViewController {
+
+ private VersionService versionService;
+ private SettingsService settingsService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ if (versionService.isNewFinalVersionAvailable()) {
+ map.put("newVersionAvailable", true);
+ map.put("latestVersion", versionService.getLatestFinalVersion());
+ } else if (versionService.isNewBetaVersionAvailable()) {
+ map.put("newVersionAvailable", true);
+ map.put("latestVersion", versionService.getLatestBetaVersion());
+ }
+
+ long totalMemory = Runtime.getRuntime().totalMemory();
+ long freeMemory = Runtime.getRuntime().freeMemory();
+
+ String serverInfo = request.getSession().getServletContext().getServerInfo() +
+ ", java " + System.getProperty("java.version") +
+ ", " + System.getProperty("os.name");
+
+ map.put("brand", settingsService.getBrand());
+ map.put("localVersion", versionService.getLocalVersion());
+ map.put("buildDate", versionService.getLocalBuildDate());
+ map.put("buildNumber", versionService.getLocalBuildNumber());
+ map.put("serverInfo", serverInfo);
+ map.put("usedMemory", totalMemory - freeMemory);
+ map.put("totalMemory", totalMemory);
+ map.put("logEntries", Logger.getLatestLogEntries());
+ map.put("logFile", Logger.getLogFile());
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setVersionService(VersionService versionService) {
+ this.versionService = versionService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java
new file mode 100644
index 00000000..49c95926
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java
@@ -0,0 +1,340 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.MediaScannerService;
+import net.sourceforge.subsonic.service.RatingService;
+import net.sourceforge.subsonic.service.SearchService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for the home page.
+ *
+ * @author Sindre Mehus
+ */
+public class HomeController extends ParameterizableViewController {
+
+ private static final Logger LOG = Logger.getLogger(HomeController.class);
+
+ private static final int DEFAULT_LIST_SIZE = 10;
+ private static final int MAX_LIST_SIZE = 500;
+ private static final int DEFAULT_LIST_OFFSET = 0;
+ private static final int MAX_LIST_OFFSET = 5000;
+
+ private SettingsService settingsService;
+ private MediaScannerService mediaScannerService;
+ private RatingService ratingService;
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+ private SearchService searchService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ User user = securityService.getCurrentUser(request);
+ if (user.isAdminRole() && settingsService.isGettingStartedEnabled()) {
+ return new ModelAndView(new RedirectView("gettingStarted.view"));
+ }
+
+ int listSize = DEFAULT_LIST_SIZE;
+ int listOffset = DEFAULT_LIST_OFFSET;
+ if (request.getParameter("listSize") != null) {
+ listSize = Math.max(0, Math.min(Integer.parseInt(request.getParameter("listSize")), MAX_LIST_SIZE));
+ }
+ if (request.getParameter("listOffset") != null) {
+ listOffset = Math.max(0, Math.min(Integer.parseInt(request.getParameter("listOffset")), MAX_LIST_OFFSET));
+ }
+
+ String listType = request.getParameter("listType");
+ if (listType == null) {
+ listType = "random";
+ }
+
+ List<Album> albums;
+ if ("highest".equals(listType)) {
+ albums = getHighestRated(listOffset, listSize);
+ } else if ("frequent".equals(listType)) {
+ albums = getMostFrequent(listOffset, listSize);
+ } else if ("recent".equals(listType)) {
+ albums = getMostRecent(listOffset, listSize);
+ } else if ("newest".equals(listType)) {
+ albums = getNewest(listOffset, listSize);
+ } else if ("starred".equals(listType)) {
+ albums = getStarred(listOffset, listSize, user.getUsername());
+ } else if ("random".equals(listType)) {
+ albums = getRandom(listSize);
+ } else if ("alphabetical".equals(listType)) {
+ albums = getAlphabetical(listOffset, listSize, true);
+ } else {
+ albums = Collections.emptyList();
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("albums", albums);
+ map.put("welcomeTitle", settingsService.getWelcomeTitle());
+ map.put("welcomeSubtitle", settingsService.getWelcomeSubtitle());
+ map.put("welcomeMessage", settingsService.getWelcomeMessage());
+ map.put("isIndexBeingCreated", mediaScannerService.isScanning());
+ map.put("listType", listType);
+ map.put("listSize", listSize);
+ map.put("listOffset", listOffset);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ List<Album> getHighestRated(int offset, int count) {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile mediaFile : ratingService.getHighestRated(offset, count)) {
+ Album album = createAlbum(mediaFile);
+ if (album != null) {
+ album.setRating((int) Math.round(ratingService.getAverageRating(mediaFile) * 10.0D));
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ List<Album> getMostFrequent(int offset, int count) {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile mediaFile : mediaFileService.getMostFrequentlyPlayedAlbums(offset, count)) {
+ Album album = createAlbum(mediaFile);
+ if (album != null) {
+ album.setPlayCount(mediaFile.getPlayCount());
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ List<Album> getMostRecent(int offset, int count) {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile mediaFile : mediaFileService.getMostRecentlyPlayedAlbums(offset, count)) {
+ Album album = createAlbum(mediaFile);
+ if (album != null) {
+ album.setLastPlayed(mediaFile.getLastPlayed());
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ List<Album> getNewest(int offset, int count) throws IOException {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile file : mediaFileService.getNewestAlbums(offset, count)) {
+ Album album = createAlbum(file);
+ if (album != null) {
+ Date created = file.getCreated();
+ if (created == null) {
+ created = file.getChanged();
+ }
+ album.setCreated(created);
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ List<Album> getStarred(int offset, int count, String username) throws IOException {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile file : mediaFileService.getStarredAlbums(offset, count, username)) {
+ Album album = createAlbum(file);
+ if (album != null) {
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ List<Album> getRandom(int count) throws IOException {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile file : searchService.getRandomAlbums(count)) {
+ Album album = createAlbum(file);
+ if (album != null) {
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ List<Album> getAlphabetical(int offset, int count, boolean byArtist) throws IOException {
+ List<Album> result = new ArrayList<Album>();
+ for (MediaFile file : mediaFileService.getAlphabetialAlbums(offset, count, byArtist)) {
+ Album album = createAlbum(file);
+ if (album != null) {
+ result.add(album);
+ }
+ }
+ return result;
+ }
+
+ private Album createAlbum(MediaFile file) {
+ Album album = new Album();
+ album.setId(file.getId());
+ album.setPath(file.getPath());
+ try {
+ resolveArtistAndAlbumTitle(album, file);
+ resolveCoverArt(album, file);
+ } catch (Exception x) {
+ LOG.warn("Failed to create albumTitle list entry for " + file.getPath(), x);
+ return null;
+ }
+ return album;
+ }
+
+ private void resolveArtistAndAlbumTitle(Album album, MediaFile file) throws IOException {
+ album.setArtist(file.getArtist());
+ album.setAlbumTitle(file.getAlbumName());
+ }
+
+ private void resolveCoverArt(Album album, MediaFile file) {
+ album.setCoverArtPath(file.getCoverArtPath());
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaScannerService(MediaScannerService mediaScannerService) {
+ this.mediaScannerService = mediaScannerService;
+ }
+
+ public void setRatingService(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+
+ /**
+ * Contains info for a single album.
+ */
+ @Deprecated
+ public static class Album {
+ private String path;
+ private String coverArtPath;
+ private String artist;
+ private String albumTitle;
+ private Date created;
+ private Date lastPlayed;
+ private Integer playCount;
+ private Integer rating;
+ private int id;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getCoverArtPath() {
+ return coverArtPath;
+ }
+
+ public void setCoverArtPath(String coverArtPath) {
+ this.coverArtPath = coverArtPath;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getAlbumTitle() {
+ return albumTitle;
+ }
+
+ public void setAlbumTitle(String albumTitle) {
+ this.albumTitle = albumTitle;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getLastPlayed() {
+ return lastPlayed;
+ }
+
+ public void setLastPlayed(Date lastPlayed) {
+ this.lastPlayed = lastPlayed;
+ }
+
+ public Integer getPlayCount() {
+ return playCount;
+ }
+
+ public void setPlayCount(Integer playCount) {
+ this.playCount = playCount;
+ }
+
+ public Integer getRating() {
+ return rating;
+ }
+
+ public void setRating(Integer rating) {
+ this.rating = rating;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java
new file mode 100644
index 00000000..55e9b200
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java
@@ -0,0 +1,93 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileItemFactory;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+
+/**
+ * @author Sindre Mehus
+ */
+public class ImportPlaylistController extends ParameterizableViewController {
+
+ private static final long MAX_PLAYLIST_SIZE_MB = 5L;
+
+ private SecurityService securityService;
+ private PlaylistService playlistService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ try {
+ if (ServletFileUpload.isMultipartContent(request)) {
+
+ FileItemFactory factory = new DiskFileItemFactory();
+ ServletFileUpload upload = new ServletFileUpload(factory);
+ List<?> items = upload.parseRequest(request);
+ for (Object o : items) {
+ FileItem item = (FileItem) o;
+
+ if ("file".equals(item.getFieldName()) && !StringUtils.isBlank(item.getName())) {
+ if (item.getSize() > MAX_PLAYLIST_SIZE_MB * 1024L * 1024L) {
+ throw new Exception("The playlist file is too large. Max file size is " + MAX_PLAYLIST_SIZE_MB + " MB.");
+ }
+ String playlistName = FilenameUtils.getBaseName(item.getName());
+ String fileName = FilenameUtils.getName(item.getName());
+ String format = StringUtils.lowerCase(FilenameUtils.getExtension(item.getName()));
+ String username = securityService.getCurrentUsername(request);
+ Playlist playlist = playlistService.importPlaylist(username, playlistName, fileName, format, item.getInputStream());
+ map.put("playlist", playlist);
+ }
+ }
+ }
+ } catch (Exception e) {
+ map.put("error", e.getMessage());
+ }
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java
new file mode 100644
index 00000000..5ee7b799
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java
@@ -0,0 +1,116 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.InternetRadio;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Date;
+
+/**
+ * Controller for the page used to administrate the set of internet radio/tv stations.
+ *
+ * @author Sindre Mehus
+ */
+public class InternetRadioSettingsController extends ParameterizableViewController {
+
+ private SettingsService settingsService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ if (isFormSubmission(request)) {
+ String error = handleParameters(request);
+ map.put("error", error);
+ if (error == null) {
+ map.put("reload", true);
+ }
+ }
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ map.put("internetRadios", settingsService.getAllInternetRadios(true));
+
+ result.addObject("model", map);
+ return result;
+ }
+
+ /**
+ * Determine if the given request represents a form submission.
+ *
+ * @param request current HTTP request
+ * @return if the request represents a form submission
+ */
+ private boolean isFormSubmission(HttpServletRequest request) {
+ return "POST".equals(request.getMethod());
+ }
+
+ private String handleParameters(HttpServletRequest request) {
+ List<InternetRadio> radios = settingsService.getAllInternetRadios(true);
+ for (InternetRadio radio : radios) {
+ Integer id = radio.getId();
+ String streamUrl = getParameter(request, "streamUrl", id);
+ String homepageUrl = getParameter(request, "homepageUrl", id);
+ String name = getParameter(request, "name", id);
+ boolean enabled = getParameter(request, "enabled", id) != null;
+ boolean delete = getParameter(request, "delete", id) != null;
+
+ if (delete) {
+ settingsService.deleteInternetRadio(id);
+ } else {
+ if (name == null) {
+ return "internetradiosettings.noname";
+ }
+ if (streamUrl == null) {
+ return "internetradiosettings.nourl";
+ }
+ settingsService.updateInternetRadio(new InternetRadio(id, name, streamUrl, homepageUrl, enabled, new Date()));
+ }
+ }
+
+ String name = StringUtils.trimToNull(request.getParameter("name"));
+ String streamUrl = StringUtils.trimToNull(request.getParameter("streamUrl"));
+ String homepageUrl = StringUtils.trimToNull(request.getParameter("homepageUrl"));
+ boolean enabled = StringUtils.trimToNull(request.getParameter("enabled")) != null;
+
+ if (name != null && streamUrl != null) {
+ settingsService.createInternetRadio(new InternetRadio(name, streamUrl, homepageUrl, enabled, new Date()));
+ }
+
+ return null;
+ }
+
+ private String getParameter(HttpServletRequest request, String name, Integer id) {
+ return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]"));
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java
new file mode 100644
index 00000000..d273f0b9
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java
@@ -0,0 +1,270 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.subsonic.service.PlaylistService;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.LastModified;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+import org.springframework.web.servlet.support.RequestContextUtils;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.InternetRadio;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MediaLibraryStatistics;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.MusicIndex;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.MediaScannerService;
+import net.sourceforge.subsonic.service.MusicIndexService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.util.FileUtil;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Controller for the left index frame.
+ *
+ * @author Sindre Mehus
+ */
+public class LeftController extends ParameterizableViewController implements LastModified {
+
+ private static final Logger LOG = Logger.getLogger(LeftController.class);
+
+ // Update this time if you want to force a refresh in clients.
+ private static final Calendar LAST_COMPATIBILITY_TIME = Calendar.getInstance();
+ static {
+ LAST_COMPATIBILITY_TIME.set(2012, Calendar.MARCH, 6, 0, 0, 0);
+ LAST_COMPATIBILITY_TIME.set(Calendar.MILLISECOND, 0);
+ }
+
+ private MediaScannerService mediaScannerService;
+ private SettingsService settingsService;
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+ private MusicIndexService musicIndexService;
+ private PlayerService playerService;
+ private PlaylistService playlistService;
+
+ public long getLastModified(HttpServletRequest request) {
+ saveSelectedMusicFolder(request);
+
+ if (mediaScannerService.isScanning()) {
+ return -1L;
+ }
+
+ long lastModified = LAST_COMPATIBILITY_TIME.getTimeInMillis();
+ String username = securityService.getCurrentUsername(request);
+
+ // When was settings last changed?
+ lastModified = Math.max(lastModified, settingsService.getSettingsChanged());
+
+ // When was music folder(s) on disk last changed?
+ List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders();
+ MusicFolder selectedMusicFolder = getSelectedMusicFolder(request);
+ if (selectedMusicFolder != null) {
+ File file = selectedMusicFolder.getPath();
+ lastModified = Math.max(lastModified, FileUtil.lastModified(file));
+ } else {
+ for (MusicFolder musicFolder : allMusicFolders) {
+ File file = musicFolder.getPath();
+ lastModified = Math.max(lastModified, FileUtil.lastModified(file));
+ }
+ }
+
+ // When was music folder table last changed?
+ for (MusicFolder musicFolder : allMusicFolders) {
+ lastModified = Math.max(lastModified, musicFolder.getChanged().getTime());
+ }
+
+ // When was internet radio table last changed?
+ for (InternetRadio internetRadio : settingsService.getAllInternetRadios()) {
+ lastModified = Math.max(lastModified, internetRadio.getChanged().getTime());
+ }
+
+ // When was user settings last changed?
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ lastModified = Math.max(lastModified, userSettings.getChanged().getTime());
+
+ return lastModified;
+ }
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ saveSelectedMusicFolder(request);
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ MediaLibraryStatistics statistics = mediaScannerService.getStatistics();
+ Locale locale = RequestContextUtils.getLocale(request);
+
+ String username = securityService.getCurrentUsername(request);
+ List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders();
+ MusicFolder selectedMusicFolder = getSelectedMusicFolder(request);
+ List<MusicFolder> musicFoldersToUse = selectedMusicFolder == null ? allMusicFolders : Arrays.asList(selectedMusicFolder);
+ String[] shortcuts = settingsService.getShortcutsAsArray();
+ UserSettings userSettings = settingsService.getUserSettings(username);
+
+ MusicFolderContent musicFolderContent = getMusicFolderContent(musicFoldersToUse);
+
+ map.put("player", playerService.getPlayer(request, response));
+ map.put("scanning", mediaScannerService.isScanning());
+ map.put("musicFolders", allMusicFolders);
+ map.put("selectedMusicFolder", selectedMusicFolder);
+ map.put("radios", settingsService.getAllInternetRadios());
+ map.put("shortcuts", getShortcuts(musicFoldersToUse, shortcuts));
+ map.put("captionCutoff", userSettings.getMainVisibility().getCaptionCutoff());
+ map.put("partyMode", userSettings.isPartyModeEnabled());
+ map.put("organizeByFolderStructure", settingsService.isOrganizeByFolderStructure());
+
+ if (statistics != null) {
+ map.put("statistics", statistics);
+ long bytes = statistics.getTotalLengthInBytes();
+ long hours = statistics.getTotalDurationInSeconds() / 3600L;
+ map.put("hours", hours);
+ map.put("bytes", StringUtil.formatBytes(bytes, locale));
+ }
+
+ map.put("indexedArtists", musicFolderContent.getIndexedArtists());
+ map.put("singleSongs", musicFolderContent.getSingleSongs());
+ map.put("indexes", musicFolderContent.getIndexedArtists().keySet());
+ map.put("user", securityService.getCurrentUser(request));
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ private void saveSelectedMusicFolder(HttpServletRequest request) {
+ if (request.getParameter("musicFolderId") == null) {
+ return;
+ }
+ int musicFolderId = Integer.parseInt(request.getParameter("musicFolderId"));
+
+ // Note: UserSettings.setChanged() is intentionally not called. This would break browser caching
+ // of the left frame.
+ UserSettings settings = settingsService.getUserSettings(securityService.getCurrentUsername(request));
+ settings.setSelectedMusicFolderId(musicFolderId);
+ settingsService.updateUserSettings(settings);
+ }
+
+ /**
+ * Returns the selected music folder, or <code>null</code> if all music folders should be displayed.
+ */
+ private MusicFolder getSelectedMusicFolder(HttpServletRequest request) {
+ UserSettings settings = settingsService.getUserSettings(securityService.getCurrentUsername(request));
+ int musicFolderId = settings.getSelectedMusicFolderId();
+
+ return settingsService.getMusicFolderById(musicFolderId);
+ }
+
+ protected List<MediaFile> getSingleSongs(List<MusicFolder> folders) throws IOException {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+ for (MusicFolder folder : folders) {
+ MediaFile parent = mediaFileService.getMediaFile(folder.getPath(), true);
+ result.addAll(mediaFileService.getChildrenOf(parent, true, false, true, true));
+ }
+ return result;
+ }
+
+ public List<MediaFile> getShortcuts(List<MusicFolder> musicFoldersToUse, String[] shortcuts) {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+
+ for (String shortcut : shortcuts) {
+ for (MusicFolder musicFolder : musicFoldersToUse) {
+ File file = new File(musicFolder.getPath(), shortcut);
+ if (FileUtil.exists(file)) {
+ result.add(mediaFileService.getMediaFile(file, true));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public MusicFolderContent getMusicFolderContent(List<MusicFolder> musicFoldersToUse) throws Exception {
+ SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists = musicIndexService.getIndexedArtists(musicFoldersToUse);
+ List<MediaFile> singleSongs = getSingleSongs(musicFoldersToUse);
+ return new MusicFolderContent(indexedArtists, singleSongs);
+ }
+
+ public void setMediaScannerService(MediaScannerService mediaScannerService) {
+ this.mediaScannerService = mediaScannerService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setMusicIndexService(MusicIndexService musicIndexService) {
+ this.musicIndexService = musicIndexService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public static class MusicFolderContent {
+
+ private final SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists;
+ private final List<MediaFile> singleSongs;
+
+ public MusicFolderContent(SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists, List<MediaFile> singleSongs) {
+ this.indexedArtists = indexedArtists;
+ this.singleSongs = singleSongs;
+ }
+
+ public SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> getIndexedArtists() {
+ return indexedArtists;
+ }
+
+ public List<MediaFile> getSingleSongs() {
+ return singleSongs;
+ }
+
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java
new file mode 100644
index 00000000..d47ad233
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Controller for the lyrics popup.
+ *
+ * @author Sindre Mehus
+ */
+public class LyricsController extends ParameterizableViewController {
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ map.put("artist", request.getParameter("artist"));
+ map.put("song", request.getParameter("song"));
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java
new file mode 100644
index 00000000..bbd7a478
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java
@@ -0,0 +1,128 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.TranscodingService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Controller which produces the M3U playlist.
+ *
+ * @author Sindre Mehus
+ */
+public class M3UController implements Controller {
+
+ private PlayerService playerService;
+ private SettingsService settingsService;
+ private TranscodingService transcodingService;
+
+ private static final Logger LOG = Logger.getLogger(M3UController.class);
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ response.setContentType("audio/x-mpegurl");
+ response.setCharacterEncoding(StringUtil.ENCODING_UTF8);
+
+ Player player = playerService.getPlayer(request, response);
+
+ String url = request.getRequestURL().toString();
+ url = url.replaceFirst("play.m3u.*", "stream?");
+
+ // Rewrite URLs in case we're behind a proxy.
+ if (settingsService.isRewriteUrlEnabled()) {
+ String referer = request.getHeader("referer");
+ url = StringUtil.rewriteUrl(url, referer);
+ }
+
+ // Change protocol and port, if specified. (To make it work with players that don't support SSL.)
+ int streamPort = settingsService.getStreamPort();
+ if (streamPort != 0) {
+ url = StringUtil.toHttpUrl(url, streamPort);
+ LOG.info("Using non-SSL port " + streamPort + " in m3u playlist.");
+ }
+
+ if (player.isExternalWithPlaylist()) {
+ createClientSidePlaylist(response.getWriter(), player, url);
+ } else {
+ createServerSidePlaylist(response.getWriter(), player, url);
+ }
+ return null;
+ }
+
+ private void createClientSidePlaylist(PrintWriter out, Player player, String url) throws Exception {
+ out.println("#EXTM3U");
+ List<MediaFile> result;
+ synchronized (player.getPlayQueue()) {
+ result = player.getPlayQueue().getFiles();
+ }
+ for (MediaFile mediaFile : result) {
+ Integer duration = mediaFile.getDurationSeconds();
+ if (duration == null) {
+ duration = -1;
+ }
+ out.println("#EXTINF:" + duration + "," + mediaFile.getArtist() + " - " + mediaFile.getTitle());
+ out.println(url + "player=" + player.getId() + "&id=" +mediaFile.getId() + "&suffix=." + transcodingService.getSuffix(player, mediaFile, null));
+ }
+ }
+
+ private void createServerSidePlaylist(PrintWriter out, Player player, String url) throws IOException {
+
+ url += "player=" + player.getId();
+
+ // Get suffix of current file, e.g., ".mp3".
+ String suffix = getSuffix(player);
+ if (suffix != null) {
+ url += "&suffix=." + suffix;
+ }
+
+ out.println("#EXTM3U");
+ out.println("#EXTINF:-1,Subsonic");
+ out.println(url);
+ }
+
+ private String getSuffix(Player player) {
+ PlayQueue playQueue = player.getPlayQueue();
+ return playQueue.isEmpty() ? null : transcodingService.getSuffix(player, playQueue.getFile(0), null);
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java
new file mode 100644
index 00000000..1d9e0a61
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java
@@ -0,0 +1,297 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.CoverArtScheme;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.AdService;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.RatingService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controller for the main page.
+ *
+ * @author Sindre Mehus
+ */
+public class MainController extends ParameterizableViewController {
+
+ private SecurityService securityService;
+ private PlayerService playerService;
+ private SettingsService settingsService;
+ private RatingService ratingService;
+ private MediaFileService mediaFileService;
+ private AdService adService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ Player player = playerService.getPlayer(request, response);
+ List<MediaFile> mediaFiles = getMediaFiles(request);
+
+ if (mediaFiles.isEmpty()) {
+ return new ModelAndView(new RedirectView("notFound.view"));
+ }
+
+ MediaFile dir = mediaFiles.get(0);
+ if (dir.isFile()) {
+ dir = mediaFileService.getParentOf(dir);
+ }
+
+ // Redirect if root directory.
+ if (mediaFileService.isRoot(dir)) {
+ return new ModelAndView(new RedirectView("home.view?"));
+ }
+
+ List<MediaFile> children = mediaFiles.size() == 1 ? mediaFileService.getChildrenOf(dir, true, true, true) : getMultiFolderChildren(mediaFiles);
+ String username = securityService.getCurrentUsername(request);
+ UserSettings userSettings = settingsService.getUserSettings(username);
+
+ mediaFileService.populateStarredDate(dir, username);
+ mediaFileService.populateStarredDate(children, username);
+
+ map.put("dir", dir);
+ map.put("ancestors", getAncestors(dir));
+ map.put("children", children);
+ map.put("artist", guessArtist(children));
+ map.put("album", guessAlbum(children));
+ map.put("player", player);
+ map.put("user", securityService.getCurrentUser(request));
+ map.put("multipleArtists", isMultipleArtists(children));
+ map.put("visibility", userSettings.getMainVisibility());
+ map.put("showAlbumYear", settingsService.isSortAlbumsByYear());
+ map.put("updateNowPlaying", request.getParameter("updateNowPlaying") != null);
+ map.put("partyMode", userSettings.isPartyModeEnabled());
+ map.put("brand", settingsService.getBrand());
+ if (!settingsService.isLicenseValid()) {
+ map.put("ad", adService.getAd());
+ }
+
+ try {
+ MediaFile parent = mediaFileService.getParentOf(dir);
+ map.put("parent", parent);
+ map.put("navigateUpAllowed", !mediaFileService.isRoot(parent));
+ } catch (SecurityException x) {
+ // Happens if Podcast directory is outside music folder.
+ }
+
+ Integer userRating = ratingService.getRatingForUser(username, dir);
+ Double averageRating = ratingService.getAverageRating(dir);
+
+ if (userRating == null) {
+ userRating = 0;
+ }
+
+ if (averageRating == null) {
+ averageRating = 0.0D;
+ }
+
+ map.put("userRating", 10 * userRating);
+ map.put("averageRating", Math.round(10.0D * averageRating));
+ map.put("starred", mediaFileService.getMediaFileStarredDate(dir.getId(), username) != null);
+
+ CoverArtScheme scheme = player.getCoverArtScheme();
+ if (scheme != CoverArtScheme.OFF) {
+ List<MediaFile> coverArts = getCoverArts(dir, children);
+ int size = coverArts.size() > 1 ? scheme.getSize() : scheme.getSize() * 2;
+ map.put("coverArts", coverArts);
+ map.put("coverArtSize", size);
+ if (coverArts.isEmpty() && dir.isAlbum()) {
+ map.put("showGenericCoverArt", true);
+ }
+ }
+
+ setPreviousAndNextAlbums(dir, map);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ private List<MediaFile> getMediaFiles(HttpServletRequest request) {
+ List<MediaFile> mediaFiles = new ArrayList<MediaFile>();
+ for (String path : ServletRequestUtils.getStringParameters(request, "path")) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(path);
+ if (mediaFile != null) {
+ mediaFiles.add(mediaFile);
+ }
+ }
+ for (int id : ServletRequestUtils.getIntParameters(request, "id")) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(id);
+ if (mediaFile != null) {
+ mediaFiles.add(mediaFile);
+ }
+ }
+ return mediaFiles;
+ }
+
+ private String guessArtist(List<MediaFile> children) {
+ for (MediaFile child : children) {
+ if (child.isFile() && child.getArtist() != null) {
+ return child.getArtist();
+ }
+ }
+ return null;
+ }
+
+ private String guessAlbum(List<MediaFile> children) {
+ for (MediaFile child : children) {
+ if (child.isFile() && child.getArtist() != null) {
+ return child.getAlbumName();
+ }
+ }
+ return null;
+ }
+
+ private List<MediaFile> getCoverArts(MediaFile dir, List<MediaFile> children) throws IOException {
+ int limit = settingsService.getCoverArtLimit();
+ if (limit == 0) {
+ limit = Integer.MAX_VALUE;
+ }
+
+ List<MediaFile> coverArts = new ArrayList<MediaFile>();
+ if (dir.isAlbum() && dir.getCoverArtPath() != null) {
+ coverArts.add(dir);
+ } else {
+ for (MediaFile child : children) {
+ if (child.isAlbum()) {
+ if (child.getCoverArtPath() != null) {
+ coverArts.add(child);
+ }
+ if (coverArts.size() > limit) {
+ break;
+ }
+ }
+ }
+ }
+ return coverArts;
+ }
+
+ private List<MediaFile> getMultiFolderChildren(List<MediaFile> mediaFiles) throws IOException {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+ for (MediaFile mediaFile : mediaFiles) {
+ if (mediaFile.isFile()) {
+ mediaFile = mediaFileService.getParentOf(mediaFile);
+ }
+ result.addAll(mediaFileService.getChildrenOf(mediaFile, true, true, true));
+ }
+ return result;
+ }
+
+ private List<MediaFile> getAncestors(MediaFile dir) throws IOException {
+ LinkedList<MediaFile> result = new LinkedList<MediaFile>();
+
+ try {
+ MediaFile parent = mediaFileService.getParentOf(dir);
+ while (parent != null && !mediaFileService.isRoot(parent)) {
+ result.addFirst(parent);
+ parent = mediaFileService.getParentOf(parent);
+ }
+ } catch (SecurityException x) {
+ // Happens if Podcast directory is outside music folder.
+ }
+ return result;
+ }
+
+ private void setPreviousAndNextAlbums(MediaFile dir, Map<String, Object> map) throws IOException {
+ MediaFile parent = mediaFileService.getParentOf(dir);
+
+ if (dir.isAlbum() && !mediaFileService.isRoot(parent)) {
+ List<MediaFile> sieblings = mediaFileService.getChildrenOf(parent, false, true, true);
+
+ int index = sieblings.indexOf(dir);
+ if (index > 0) {
+ map.put("previousAlbum", sieblings.get(index - 1));
+ }
+ if (index < sieblings.size() - 1) {
+ map.put("nextAlbum", sieblings.get(index + 1));
+ }
+ }
+ }
+
+ private boolean isMultipleArtists(List<MediaFile> children) {
+ // Collect unique artist names.
+ Set<String> artists = new HashSet<String>();
+ for (MediaFile child : children) {
+ if (child.getArtist() != null) {
+ artists.add(child.getArtist().toLowerCase());
+ }
+ }
+
+ // If zero or one artist, it is definitely not multiple artists.
+ if (artists.size() < 2) {
+ return false;
+ }
+
+ // Fuzzily compare artist names, allowing for some differences in spelling, whitespace etc.
+ List<String> artistList = new ArrayList<String>(artists);
+ for (String artist : artistList) {
+ if (StringUtils.getLevenshteinDistance(artist, artistList.get(0)) > 3) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setRatingService(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ public void setAdService(AdService adService) {
+ this.adService = adService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java
new file mode 100644
index 00000000..f29cb346
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java
@@ -0,0 +1,89 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Calendar;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+
+/**
+ * Controller for the "more" page.
+ *
+ * @author Sindre Mehus
+ */
+public class MoreController extends ParameterizableViewController {
+
+ private SettingsService settingsService;
+ private SecurityService securityService;
+ private PlayerService playerService;
+ private MediaFileService mediaFileService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ String uploadDirectory = null;
+ List<MusicFolder> musicFolders = settingsService.getAllMusicFolders();
+ if (musicFolders.size() > 0) {
+ uploadDirectory = new File(musicFolders.get(0).getPath(), "Incoming").getPath();
+ }
+
+ Player player = playerService.getPlayer(request, response);
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ map.put("user", securityService.getCurrentUser(request));
+ map.put("uploadDirectory", uploadDirectory);
+ map.put("genres", mediaFileService.getGenres());
+ map.put("currentYear", Calendar.getInstance().get(Calendar.YEAR));
+ map.put("musicFolders", settingsService.getAllMusicFolders());
+ map.put("clientSidePlaylist", player.isExternalWithPlaylist() || player.isWeb());
+ map.put("brand", settingsService.getBrand());
+ return result;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java
new file mode 100644
index 00000000..1d781565
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java
@@ -0,0 +1,244 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Multi-controller used for simple pages.
+ *
+ * @author Sindre Mehus
+ */
+public class MultiController extends MultiActionController {
+
+ private static final Logger LOG = Logger.getLogger(MultiController.class);
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private PlaylistService playlistService;
+
+ public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ // Auto-login if "user" and "password" parameters are given.
+ String username = request.getParameter("user");
+ String password = request.getParameter("password");
+ if (username != null && password != null) {
+ username = StringUtil.urlEncode(username);
+ password = StringUtil.urlEncode(password);
+ return new ModelAndView(new RedirectView("j_acegi_security_check?j_username=" + username +
+ "&j_password=" + password + "&_acegi_security_remember_me=checked"));
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("logout", request.getParameter("logout") != null);
+ map.put("error", request.getParameter("error") != null);
+ map.put("brand", settingsService.getBrand());
+ map.put("loginMessage", settingsService.getLoginMessage());
+
+ User admin = securityService.getUserByName(User.USERNAME_ADMIN);
+ if (User.USERNAME_ADMIN.equals(admin.getPassword())) {
+ map.put("insecure", true);
+ }
+
+ return new ModelAndView("login", "model", map);
+ }
+
+ public ModelAndView recover(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ String usernameOrEmail = StringUtils.trimToNull(request.getParameter("usernameOrEmail"));
+
+ if (usernameOrEmail != null) {
+ User user = getUserByUsernameOrEmail(usernameOrEmail);
+ if (user == null) {
+ map.put("error", "recover.error.usernotfound");
+ } else if (user.getEmail() == null) {
+ map.put("error", "recover.error.noemail");
+ } else {
+ String password = RandomStringUtils.randomAlphanumeric(8);
+ if (emailPassword(password, user.getUsername(), user.getEmail())) {
+ map.put("sentTo", user.getEmail());
+ user.setLdapAuthenticated(false);
+ user.setPassword(password);
+ securityService.updateUser(user);
+ } else {
+ map.put("error", "recover.error.sendfailed");
+ }
+ }
+ }
+
+ return new ModelAndView("recover", "model", map);
+ }
+
+ private boolean emailPassword(String password, String username, String email) {
+ HttpClient client = new DefaultHttpClient();
+ try {
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 10000);
+ HttpPost method = new HttpPost("http://subsonic.org/backend/sendMail.view");
+
+ List<NameValuePair> params = new ArrayList<NameValuePair>();
+ params.add(new BasicNameValuePair("from", "noreply@subsonic.org"));
+ params.add(new BasicNameValuePair("to", email));
+ params.add(new BasicNameValuePair("subject", "Subsonic Password"));
+ params.add(new BasicNameValuePair("text",
+ "Hi there!\n\n" +
+ "You have requested to reset your Subsonic password. Please find your new login details below.\n\n" +
+ "Username: " + username + "\n" +
+ "Password: " + password + "\n\n" +
+ "--\n" +
+ "The Subsonic Team\n" +
+ "subsonic.org"));
+ method.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8));
+ client.execute(method);
+ return true;
+ } catch (Exception x) {
+ LOG.warn("Failed to send email.", x);
+ return false;
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ private User getUserByUsernameOrEmail(String usernameOrEmail) {
+ if (usernameOrEmail != null) {
+ User user = securityService.getUserByName(usernameOrEmail);
+ if (user != null) {
+ return user;
+ }
+ return securityService.getUserByEmail(usernameOrEmail);
+ }
+ return null;
+ }
+
+ public ModelAndView accessDenied(HttpServletRequest request, HttpServletResponse response) {
+ return new ModelAndView("accessDenied");
+ }
+
+ public ModelAndView notFound(HttpServletRequest request, HttpServletResponse response) {
+ return new ModelAndView("notFound");
+ }
+
+ public ModelAndView gettingStarted(HttpServletRequest request, HttpServletResponse response) {
+ updatePortAndContextPath(request);
+
+ if (request.getParameter("hide") != null) {
+ settingsService.setGettingStartedEnabled(false);
+ settingsService.save();
+ return new ModelAndView(new RedirectView("home.view"));
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("runningAsRoot", "root".equals(System.getProperty("user.name")));
+ return new ModelAndView("gettingStarted", "model", map);
+ }
+
+ public ModelAndView index(HttpServletRequest request, HttpServletResponse response) {
+ updatePortAndContextPath(request);
+ UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request));
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("showRight", userSettings.isShowNowPlayingEnabled() || userSettings.isShowChatEnabled());
+ map.put("brand", settingsService.getBrand());
+ return new ModelAndView("index", "model", map);
+ }
+
+ public ModelAndView exportPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ Playlist playlist = playlistService.getPlaylist(id);
+ if (!playlistService.isReadAllowed(playlist, securityService.getCurrentUsername(request))) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return null;
+
+ }
+ response.setContentType("application/x-download");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + StringUtil.fileSystemSafe(playlist.getName()) + ".m3u8\"");
+
+ playlistService.exportPlaylist(id, response.getOutputStream());
+ return null;
+ }
+
+ private void updatePortAndContextPath(HttpServletRequest request) {
+
+ int port = Integer.parseInt(System.getProperty("subsonic.port", String.valueOf(request.getLocalPort())));
+ int httpsPort = Integer.parseInt(System.getProperty("subsonic.httpsPort", "0"));
+
+ String contextPath = request.getContextPath().replace("/", "");
+
+ if (settingsService.getPort() != port) {
+ settingsService.setPort(port);
+ settingsService.save();
+ }
+ if (settingsService.getHttpsPort() != httpsPort) {
+ settingsService.setHttpsPort(httpsPort);
+ settingsService.save();
+ }
+ if (!ObjectUtils.equals(settingsService.getUrlRedirectContextPath(), contextPath)) {
+ settingsService.setUrlRedirectContextPath(contextPath);
+ settingsService.save();
+ }
+ }
+
+ public ModelAndView test(HttpServletRequest request, HttpServletResponse response) {
+ return new ModelAndView("test");
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java
new file mode 100644
index 00000000..8c002342
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java
@@ -0,0 +1,130 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.command.MusicFolderSettingsCommand;
+import net.sourceforge.subsonic.dao.AlbumDao;
+import net.sourceforge.subsonic.dao.ArtistDao;
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.service.MediaScannerService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Controller for the page used to administrate the set of music folders.
+ *
+ * @author Sindre Mehus
+ */
+public class MusicFolderSettingsController extends SimpleFormController {
+
+ private SettingsService settingsService;
+ private MediaScannerService mediaScannerService;
+ private ArtistDao artistDao;
+ private AlbumDao albumDao;
+ private MediaFileDao mediaFolderDao;
+
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ MusicFolderSettingsCommand command = new MusicFolderSettingsCommand();
+
+ if (request.getParameter("scanNow") != null) {
+ mediaScannerService.scanLibrary();
+ }
+ if (request.getParameter("expunge") != null) {
+ expunge();
+ }
+
+ command.setInterval(String.valueOf(settingsService.getIndexCreationInterval()));
+ command.setHour(String.valueOf(settingsService.getIndexCreationHour()));
+ command.setFastCache(settingsService.isFastCacheEnabled());
+ command.setOrganizeByFolderStructure(settingsService.isOrganizeByFolderStructure());
+ command.setScanning(mediaScannerService.isScanning());
+ command.setMusicFolders(wrap(settingsService.getAllMusicFolders(true, true)));
+ command.setNewMusicFolder(new MusicFolderSettingsCommand.MusicFolderInfo());
+ command.setReload(request.getParameter("reload") != null || request.getParameter("scanNow") != null);
+ return command;
+ }
+
+ private void expunge() {
+ artistDao.expunge();
+ albumDao.expunge();
+ mediaFolderDao.expunge();
+ }
+
+ private List<MusicFolderSettingsCommand.MusicFolderInfo> wrap(List<MusicFolder> musicFolders) {
+ ArrayList<MusicFolderSettingsCommand.MusicFolderInfo> result = new ArrayList<MusicFolderSettingsCommand.MusicFolderInfo>();
+ for (MusicFolder musicFolder : musicFolders) {
+ result.add(new MusicFolderSettingsCommand.MusicFolderInfo(musicFolder));
+ }
+ return result;
+ }
+
+ @Override
+ protected ModelAndView onSubmit(Object comm) throws Exception {
+ MusicFolderSettingsCommand command = (MusicFolderSettingsCommand) comm;
+
+ for (MusicFolderSettingsCommand.MusicFolderInfo musicFolderInfo : command.getMusicFolders()) {
+ if (musicFolderInfo.isDelete()) {
+ settingsService.deleteMusicFolder(musicFolderInfo.getId());
+ } else {
+ settingsService.updateMusicFolder(musicFolderInfo.toMusicFolder());
+ }
+ }
+
+ MusicFolder newMusicFolder = command.getNewMusicFolder().toMusicFolder();
+ if (newMusicFolder != null) {
+ settingsService.createMusicFolder(newMusicFolder);
+ }
+
+ settingsService.setIndexCreationInterval(Integer.parseInt(command.getInterval()));
+ settingsService.setIndexCreationHour(Integer.parseInt(command.getHour()));
+ settingsService.setFastCacheEnabled(command.isFastCache());
+ settingsService.setOrganizeByFolderStructure(command.isOrganizeByFolderStructure());
+ settingsService.save();
+
+ mediaScannerService.schedule();
+ return new ModelAndView(new RedirectView(getSuccessView() + ".view?reload"));
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaScannerService(MediaScannerService mediaScannerService) {
+ this.mediaScannerService = mediaScannerService;
+ }
+
+ public void setArtistDao(ArtistDao artistDao) {
+ this.artistDao = artistDao;
+ }
+
+ public void setAlbumDao(AlbumDao albumDao) {
+ this.albumDao = albumDao;
+ }
+
+ public void setMediaFolderDao(MediaFileDao mediaFolderDao) {
+ this.mediaFolderDao = mediaFolderDao;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java
new file mode 100644
index 00000000..3807eb71
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java
@@ -0,0 +1,89 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.Date;
+import java.util.Random;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+
+import net.sourceforge.subsonic.command.NetworkSettingsCommand;
+import net.sourceforge.subsonic.service.NetworkService;
+import net.sourceforge.subsonic.service.SettingsService;
+
+/**
+ * Controller for the page used to change the network settings.
+ *
+ * @author Sindre Mehus
+ */
+public class NetworkSettingsController extends SimpleFormController {
+
+ private static final long TRIAL_DAYS = 30L;
+
+ private SettingsService settingsService;
+ private NetworkService networkService;
+
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ NetworkSettingsCommand command = new NetworkSettingsCommand();
+ command.setPortForwardingEnabled(settingsService.isPortForwardingEnabled());
+ command.setUrlRedirectionEnabled(settingsService.isUrlRedirectionEnabled());
+ command.setUrlRedirectFrom(settingsService.getUrlRedirectFrom());
+ command.setPort(settingsService.getPort());
+
+ Date trialExpires = settingsService.getUrlRedirectTrialExpires();
+ command.setTrialExpires(trialExpires);
+ command.setTrialExpired(trialExpires != null && trialExpires.before(new Date()));
+ command.setTrial(trialExpires != null && !settingsService.isLicenseValid());
+
+ return command;
+ }
+
+ protected void doSubmitAction(Object cmd) throws Exception {
+ NetworkSettingsCommand command = (NetworkSettingsCommand) cmd;
+
+ settingsService.setPortForwardingEnabled(command.isPortForwardingEnabled());
+ settingsService.setUrlRedirectionEnabled(command.isUrlRedirectionEnabled());
+ settingsService.setUrlRedirectFrom(StringUtils.lowerCase(command.getUrlRedirectFrom()));
+
+ if (!settingsService.isLicenseValid() && settingsService.getUrlRedirectTrialExpires() == null) {
+ Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L);
+ settingsService.setUrlRedirectTrialExpires(expiryDate);
+ }
+
+ if (settingsService.getServerId() == null) {
+ Random rand = new Random(System.currentTimeMillis());
+ settingsService.setServerId(String.valueOf(Math.abs(rand.nextLong())));
+ }
+
+ settingsService.save();
+ networkService.initPortForwarding();
+ networkService.initUrlRedirection(true);
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setNetworkService(NetworkService networkService) {
+ this.networkService = networkService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java
new file mode 100644
index 00000000..79fe7c77
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java
@@ -0,0 +1,79 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.filter.ParameterDecodingFilter;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.StatusService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.AbstractController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * Controller for showing what's currently playing.
+ *
+ * @author Sindre Mehus
+ */
+public class NowPlayingController extends AbstractController {
+
+ private PlayerService playerService;
+ private StatusService statusService;
+ private MediaFileService mediaFileService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Player player = playerService.getPlayer(request, response);
+ List<TransferStatus> statuses = statusService.getStreamStatusesForPlayer(player);
+
+ MediaFile current = statuses.isEmpty() ? null : mediaFileService.getMediaFile(statuses.get(0).getFile());
+ MediaFile dir = current == null ? null : mediaFileService.getParentOf(current);
+
+ String url;
+ if (dir != null && !mediaFileService.isRoot(dir)) {
+ url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" +
+ StringUtil.utf8HexEncode(dir.getPath()) + "&updateNowPlaying=true";
+ } else {
+ url = "home.view";
+ }
+
+ return new ModelAndView(new RedirectView(url));
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java
new file mode 100644
index 00000000..8dd8d875
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java
@@ -0,0 +1,58 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import org.springframework.web.servlet.mvc.*;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.command.*;
+import net.sourceforge.subsonic.domain.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Controller for the page used to change password.
+ *
+ * @author Sindre Mehus
+ */
+public class PasswordSettingsController extends SimpleFormController {
+
+ private SecurityService securityService;
+
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ PasswordSettingsCommand command = new PasswordSettingsCommand();
+ User user = securityService.getCurrentUser(request);
+ command.setUsername(user.getUsername());
+ command.setLdapAuthenticated(user.isLdapAuthenticated());
+ return command;
+ }
+
+ protected void doSubmitAction(Object comm) throws Exception {
+ PasswordSettingsCommand command = (PasswordSettingsCommand) comm;
+ User user = securityService.getUserByName(command.getUsername());
+ user.setPassword(command.getPassword());
+ securityService.updateUser(user);
+
+ command.setPassword(null);
+ command.setConfirmPassword(null);
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java
new file mode 100644
index 00000000..3bc3f7a5
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java
@@ -0,0 +1,164 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import org.springframework.web.servlet.mvc.*;
+import org.apache.commons.lang.StringUtils;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.command.*;
+import net.sourceforge.subsonic.domain.*;
+
+import javax.servlet.http.*;
+import java.util.*;
+
+/**
+ * Controller for the page used to administrate per-user settings.
+ *
+ * @author Sindre Mehus
+ */
+public class PersonalSettingsController extends SimpleFormController {
+
+ private SettingsService settingsService;
+ private SecurityService securityService;
+
+ @Override
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ PersonalSettingsCommand command = new PersonalSettingsCommand();
+
+ User user = securityService.getCurrentUser(request);
+ UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
+
+ command.setUser(user);
+ command.setLocaleIndex("-1");
+ command.setThemeIndex("-1");
+ command.setAvatars(settingsService.getAllSystemAvatars());
+ command.setCustomAvatar(settingsService.getCustomAvatar(user.getUsername()));
+ command.setAvatarId(getAvatarId(userSettings));
+ command.setPartyModeEnabled(userSettings.isPartyModeEnabled());
+ command.setShowNowPlayingEnabled(userSettings.isShowNowPlayingEnabled());
+ command.setShowChatEnabled(userSettings.isShowChatEnabled());
+ command.setNowPlayingAllowed(userSettings.isNowPlayingAllowed());
+ command.setMainVisibility(userSettings.getMainVisibility());
+ command.setPlaylistVisibility(userSettings.getPlaylistVisibility());
+ command.setFinalVersionNotificationEnabled(userSettings.isFinalVersionNotificationEnabled());
+ command.setBetaVersionNotificationEnabled(userSettings.isBetaVersionNotificationEnabled());
+ command.setLastFmEnabled(userSettings.isLastFmEnabled());
+ command.setLastFmUsername(userSettings.getLastFmUsername());
+ command.setLastFmPassword(userSettings.getLastFmPassword());
+
+ Locale currentLocale = userSettings.getLocale();
+ Locale[] locales = settingsService.getAvailableLocales();
+ String[] localeStrings = new String[locales.length];
+ for (int i = 0; i < locales.length; i++) {
+ localeStrings[i] = locales[i].getDisplayName(locales[i]);
+ if (locales[i].equals(currentLocale)) {
+ command.setLocaleIndex(String.valueOf(i));
+ }
+ }
+ command.setLocales(localeStrings);
+
+ String currentThemeId = userSettings.getThemeId();
+ Theme[] themes = settingsService.getAvailableThemes();
+ command.setThemes(themes);
+ for (int i = 0; i < themes.length; i++) {
+ if (themes[i].getId().equals(currentThemeId)) {
+ command.setThemeIndex(String.valueOf(i));
+ break;
+ }
+ }
+
+ return command;
+ }
+
+ @Override
+ protected void doSubmitAction(Object comm) throws Exception {
+ PersonalSettingsCommand command = (PersonalSettingsCommand) comm;
+
+ int localeIndex = Integer.parseInt(command.getLocaleIndex());
+ Locale locale = null;
+ if (localeIndex != -1) {
+ locale = settingsService.getAvailableLocales()[localeIndex];
+ }
+
+ int themeIndex = Integer.parseInt(command.getThemeIndex());
+ String themeId = null;
+ if (themeIndex != -1) {
+ themeId = settingsService.getAvailableThemes()[themeIndex].getId();
+ }
+
+ String username = command.getUser().getUsername();
+ UserSettings settings = settingsService.getUserSettings(username);
+
+ settings.setLocale(locale);
+ settings.setThemeId(themeId);
+ settings.setPartyModeEnabled(command.isPartyModeEnabled());
+ settings.setShowNowPlayingEnabled(command.isShowNowPlayingEnabled());
+ settings.setShowChatEnabled(command.isShowChatEnabled());
+ settings.setNowPlayingAllowed(command.isNowPlayingAllowed());
+ settings.setMainVisibility(command.getMainVisibility());
+ settings.setPlaylistVisibility(command.getPlaylistVisibility());
+ settings.setFinalVersionNotificationEnabled(command.isFinalVersionNotificationEnabled());
+ settings.setBetaVersionNotificationEnabled(command.isBetaVersionNotificationEnabled());
+ settings.setLastFmEnabled(command.isLastFmEnabled());
+ settings.setLastFmUsername(command.getLastFmUsername());
+ settings.setSystemAvatarId(getSystemAvatarId(command));
+ settings.setAvatarScheme(getAvatarScheme(command));
+
+ if (StringUtils.isNotBlank(command.getLastFmPassword())) {
+ settings.setLastFmPassword(command.getLastFmPassword());
+ }
+
+ settings.setChanged(new Date());
+ settingsService.updateUserSettings(settings);
+
+ command.setReloadNeeded(true);
+ }
+
+ private int getAvatarId(UserSettings userSettings) {
+ AvatarScheme avatarScheme = userSettings.getAvatarScheme();
+ return avatarScheme == AvatarScheme.SYSTEM ? userSettings.getSystemAvatarId() : avatarScheme.getCode();
+ }
+
+ private AvatarScheme getAvatarScheme(PersonalSettingsCommand command) {
+ if (command.getAvatarId() == AvatarScheme.NONE.getCode()) {
+ return AvatarScheme.NONE;
+ }
+ if (command.getAvatarId() == AvatarScheme.CUSTOM.getCode()) {
+ return AvatarScheme.CUSTOM;
+ }
+ return AvatarScheme.SYSTEM;
+ }
+
+ private Integer getSystemAvatarId(PersonalSettingsCommand command) {
+ int avatarId = command.getAvatarId();
+ if (avatarId == AvatarScheme.NONE.getCode() ||
+ avatarId == AvatarScheme.CUSTOM.getCode()) {
+ return null;
+ }
+ return avatarId;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java
new file mode 100644
index 00000000..0074dda1
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java
@@ -0,0 +1,77 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+
+/**
+ * Controller for the playlist frame.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayQueueController extends ParameterizableViewController {
+
+ private PlayerService playerService;
+ private SecurityService securityService;
+ private SettingsService settingsService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ User user = securityService.getCurrentUser(request);
+ UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
+ Player player = playerService.getPlayer(request, response);
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("user", user);
+ map.put("player", player);
+ map.put("players", playerService.getPlayersForUserAndClientId(user.getUsername(), null));
+ map.put("visibility", userSettings.getPlaylistVisibility());
+ map.put("partyMode", userSettings.isPartyModeEnabled());
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java
new file mode 100644
index 00000000..813d94a5
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java
@@ -0,0 +1,150 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+
+import net.sourceforge.subsonic.command.PlayerSettingsCommand;
+import net.sourceforge.subsonic.domain.CoverArtScheme;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayerTechnology;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import net.sourceforge.subsonic.domain.Transcoding;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.TranscodingService;
+
+/**
+ * Controller for the player settings page.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayerSettingsController extends SimpleFormController {
+
+ private PlayerService playerService;
+ private SecurityService securityService;
+ private TranscodingService transcodingService;
+
+ @Override
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+
+ handleRequestParameters(request);
+ List<Player> players = getPlayers(request);
+
+ User user = securityService.getCurrentUser(request);
+ PlayerSettingsCommand command = new PlayerSettingsCommand();
+ Player player = null;
+ String playerId = request.getParameter("id");
+ if (playerId != null) {
+ player = playerService.getPlayerById(playerId);
+ } else if (!players.isEmpty()) {
+ player = players.get(0);
+ }
+
+ if (player != null) {
+ command.setPlayerId(player.getId());
+ command.setName(player.getName());
+ command.setDescription(player.toString());
+ command.setType(player.getType());
+ command.setLastSeen(player.getLastSeen());
+ command.setDynamicIp(player.isDynamicIp());
+ command.setAutoControlEnabled(player.isAutoControlEnabled());
+ command.setCoverArtSchemeName(player.getCoverArtScheme().name());
+ command.setTranscodeSchemeName(player.getTranscodeScheme().name());
+ command.setTechnologyName(player.getTechnology().name());
+ command.setAllTranscodings(transcodingService.getAllTranscodings());
+ List<Transcoding> activeTranscodings = transcodingService.getTranscodingsForPlayer(player);
+ int[] activeTranscodingIds = new int[activeTranscodings.size()];
+ for (int i = 0; i < activeTranscodings.size(); i++) {
+ activeTranscodingIds[i] = activeTranscodings.get(i).getId();
+ }
+ command.setActiveTranscodingIds(activeTranscodingIds);
+ }
+
+ command.setTranscodingSupported(transcodingService.isDownsamplingSupported(null));
+ command.setTranscodeDirectory(transcodingService.getTranscodeDirectory().getPath());
+ command.setCoverArtSchemes(CoverArtScheme.values());
+ command.setTranscodeSchemes(TranscodeScheme.values());
+ command.setTechnologies(PlayerTechnology.values());
+ command.setPlayers(players.toArray(new Player[players.size()]));
+ command.setAdmin(user.isAdminRole());
+
+ return command;
+ }
+
+ @Override
+ protected void doSubmitAction(Object comm) throws Exception {
+ PlayerSettingsCommand command = (PlayerSettingsCommand) comm;
+ Player player = playerService.getPlayerById(command.getPlayerId());
+
+ player.setAutoControlEnabled(command.isAutoControlEnabled());
+ player.setCoverArtScheme(CoverArtScheme.valueOf(command.getCoverArtSchemeName()));
+ player.setDynamicIp(command.isDynamicIp());
+ player.setName(StringUtils.trimToNull(command.getName()));
+ player.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName()));
+ player.setTechnology(PlayerTechnology.valueOf(command.getTechnologyName()));
+
+ playerService.updatePlayer(player);
+ transcodingService.setTranscodingsForPlayer(player, command.getActiveTranscodingIds());
+
+ command.setReloadNeeded(true);
+ }
+
+ private List<Player> getPlayers(HttpServletRequest request) {
+ User user = securityService.getCurrentUser(request);
+ String username = user.getUsername();
+ List<Player> players = playerService.getAllPlayers();
+ List<Player> authorizedPlayers = new ArrayList<Player>();
+
+ for (Player player : players) {
+ // Only display authorized players.
+ if (user.isAdminRole() || username.equals(player.getUsername())) {
+ authorizedPlayers.add(player);
+ }
+ }
+ return authorizedPlayers;
+ }
+
+ private void handleRequestParameters(HttpServletRequest request) {
+ if (request.getParameter("delete") != null) {
+ playerService.removePlayerById(request.getParameter("delete"));
+ } else if (request.getParameter("clone") != null) {
+ playerService.clonePlayer(request.getParameter("clone"));
+ }
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java
new file mode 100644
index 00000000..6b24a3c5
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java
@@ -0,0 +1,82 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Controller for the main page.
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistController extends ParameterizableViewController {
+
+ private SecurityService securityService;
+ private PlaylistService playlistService;
+ private SettingsService settingsService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ User user = securityService.getCurrentUser(request);
+ String username = user.getUsername();
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ Playlist playlist = playlistService.getPlaylist(id);
+ if (playlist == null) {
+ return new ModelAndView(new RedirectView("notFound.view"));
+ }
+
+ map.put("playlist", playlist);
+ map.put("user", user);
+ map.put("editAllowed", username.equals(playlist.getUsername()) || securityService.isAdmin(username));
+ map.put("partyMode", userSettings.isPartyModeEnabled());
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java
new file mode 100644
index 00000000..dbc6854b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java
@@ -0,0 +1,152 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Controller for the page used to generate the Podcast XML file.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastController extends ParameterizableViewController {
+
+ private static final DateFormat RSS_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+ private PlaylistService playlistService;
+ private SettingsService settingsService;
+ private SecurityService securityService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ String url = request.getRequestURL().toString();
+ String username = securityService.getCurrentUsername(request);
+ List<Playlist> playlists = playlistService.getReadablePlaylistsForUser(username);
+ List<Podcast> podcasts = new ArrayList<Podcast>();
+
+ for (Playlist playlist : playlists) {
+
+ List<MediaFile> songs = playlistService.getFilesInPlaylist(playlist.getId());
+ if (songs.isEmpty()) {
+ continue;
+ }
+ long length = 0L;
+ for (MediaFile song : songs) {
+ length += song.getFileSize();
+ }
+ String publishDate = RSS_DATE_FORMAT.format(playlist.getCreated());
+
+ // Resolve content type.
+ String suffix = songs.get(0).getFormat();
+ String type = StringUtil.getMimeType(suffix);
+
+ String enclosureUrl = url.replaceFirst("/podcast.*", "/stream?playlist=" + playlist.getId() + "&amp;suffix=." + suffix);
+
+ // Rewrite URLs in case we're behind a proxy.
+ if (settingsService.isRewriteUrlEnabled()) {
+ String referer = request.getHeader("referer");
+ url = StringUtil.rewriteUrl(url, referer);
+ }
+
+ // Change protocol and port, if specified. (To make it work with players that don't support SSL.)
+ int streamPort = settingsService.getStreamPort();
+ if (streamPort != 0) {
+ enclosureUrl = StringUtil.toHttpUrl(enclosureUrl, streamPort);
+ }
+
+ podcasts.add(new Podcast(playlist.getName(), publishDate, enclosureUrl, length, type));
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ map.put("url", url);
+ map.put("podcasts", podcasts);
+
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ /**
+ * Contains information about a single Podcast.
+ */
+ public static class Podcast {
+ private String name;
+ private String publishDate;
+ private String enclosureUrl;
+ private long length;
+ private String type;
+
+ public Podcast(String name, String publishDate, String enclosureUrl, long length, String type) {
+ this.name = name;
+ this.publishDate = publishDate;
+ this.enclosureUrl = enclosureUrl;
+ this.length = length;
+ this.type = type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPublishDate() {
+ return publishDate;
+ }
+
+ public String getEnclosureUrl() {
+ return enclosureUrl;
+ }
+
+ public long getLength() {
+ return length;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java
new file mode 100644
index 00000000..c955e884
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java
@@ -0,0 +1,102 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.PodcastEpisode;
+import net.sourceforge.subsonic.domain.PodcastStatus;
+import net.sourceforge.subsonic.service.PodcastService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.AbstractController;
+import org.springframework.web.servlet.view.RedirectView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.List;
+
+/**
+ * Controller for the "Podcast receiver" page.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastReceiverAdminController extends AbstractController {
+
+ private PodcastService podcastService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ handleParameters(request);
+ return new ModelAndView(new RedirectView("podcastReceiver.view?expandedChannels=" + request.getParameter("expandedChannels")));
+ }
+
+ private void handleParameters(HttpServletRequest request) {
+ if (request.getParameter("add") != null) {
+ String url = request.getParameter("add");
+ podcastService.createChannel(url);
+ }
+ if (request.getParameter("downloadChannel") != null ||
+ request.getParameter("downloadEpisode") != null) {
+ download(StringUtil.parseInts(request.getParameter("downloadChannel")),
+ StringUtil.parseInts(request.getParameter("downloadEpisode")));
+ }
+ if (request.getParameter("deleteChannel") != null) {
+ for (int channelId : StringUtil.parseInts(request.getParameter("deleteChannel"))) {
+ podcastService.deleteChannel(channelId);
+ }
+ }
+ if (request.getParameter("deleteEpisode") != null) {
+ for (int episodeId : StringUtil.parseInts(request.getParameter("deleteEpisode"))) {
+ podcastService.deleteEpisode(episodeId, true);
+ }
+ }
+ if (request.getParameter("refresh") != null) {
+ podcastService.refreshAllChannels(true);
+ }
+ }
+
+ private void download(int[] channelIds, int[] episodeIds) {
+ SortedSet<Integer> uniqueEpisodeIds = new TreeSet<Integer>();
+ for (int episodeId : episodeIds) {
+ uniqueEpisodeIds.add(episodeId);
+ }
+ for (int channelId : channelIds) {
+ List<PodcastEpisode> episodes = podcastService.getEpisodes(channelId, false);
+ for (PodcastEpisode episode : episodes) {
+ uniqueEpisodeIds.add(episode.getId());
+ }
+ }
+
+ for (Integer episodeId : uniqueEpisodeIds) {
+ PodcastEpisode episode = podcastService.getEpisode(episodeId, false);
+ if (episode != null && episode.getUrl() != null &&
+ (episode.getStatus() == PodcastStatus.NEW ||
+ episode.getStatus() == PodcastStatus.ERROR ||
+ episode.getStatus() == PodcastStatus.SKIPPED)) {
+
+ podcastService.downloadEpisode(episode);
+ }
+ }
+ }
+
+ public void setPodcastService(PodcastService podcastService) {
+ this.podcastService = podcastService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java
new file mode 100644
index 00000000..93640c22
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java
@@ -0,0 +1,85 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import net.sourceforge.subsonic.domain.PodcastChannel;
+import net.sourceforge.subsonic.domain.PodcastEpisode;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.PodcastService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Controller for the "Podcast receiver" page.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastReceiverController extends ParameterizableViewController {
+
+ private PodcastService podcastService;
+ private SecurityService securityService;
+ private SettingsService settingsService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+
+ Map<PodcastChannel, List<PodcastEpisode>> channels = new LinkedHashMap<PodcastChannel, List<PodcastEpisode>>();
+ for (PodcastChannel channel : podcastService.getAllChannels()) {
+ channels.put(channel, podcastService.getEpisodes(channel.getId(), false));
+ }
+
+ User user = securityService.getCurrentUser(request);
+ UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
+
+ map.put("user", user);
+ map.put("partyMode", userSettings.isPartyModeEnabled());
+ map.put("channels", channels);
+ map.put("expandedChannels", StringUtil.parseInts(request.getParameter("expandedChannels")));
+ return result;
+ }
+
+ public void setPodcastService(PodcastService podcastService) {
+ this.podcastService = podcastService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java
new file mode 100644
index 00000000..b6389616
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import org.springframework.web.servlet.mvc.SimpleFormController;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.PodcastService;
+import net.sourceforge.subsonic.command.PodcastSettingsCommand;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Controller for the page used to administrate the Podcast receiver.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastSettingsController extends SimpleFormController {
+
+ private SettingsService settingsService;
+ private PodcastService podcastService;
+
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ PodcastSettingsCommand command = new PodcastSettingsCommand();
+
+ command.setInterval(String.valueOf(settingsService.getPodcastUpdateInterval()));
+ command.setEpisodeRetentionCount(String.valueOf(settingsService.getPodcastEpisodeRetentionCount()));
+ command.setEpisodeDownloadCount(String.valueOf(settingsService.getPodcastEpisodeDownloadCount()));
+ command.setFolder(settingsService.getPodcastFolder());
+ return command;
+ }
+
+ protected void doSubmitAction(Object comm) throws Exception {
+ PodcastSettingsCommand command = (PodcastSettingsCommand) comm;
+
+ settingsService.setPodcastUpdateInterval(Integer.parseInt(command.getInterval()));
+ settingsService.setPodcastEpisodeRetentionCount(Integer.parseInt(command.getEpisodeRetentionCount()));
+ settingsService.setPodcastEpisodeDownloadCount(Integer.parseInt(command.getEpisodeDownloadCount()));
+ settingsService.setPodcastFolder(command.getFolder());
+ settingsService.save();
+
+ podcastService.schedule();
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPodcastService(PodcastService podcastService) {
+ this.podcastService = podcastService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java
new file mode 100644
index 00000000..9535e059
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java
@@ -0,0 +1,68 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.io.InputStream;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+
+/**
+ * A proxy for external HTTP requests.
+ *
+ * @author Sindre Mehus
+ */
+public class ProxyController implements Controller {
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String url = ServletRequestUtils.getRequiredStringParameter(request, "url");
+
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 15000);
+ HttpGet method = new HttpGet(url);
+
+ InputStream in = null;
+ try {
+ HttpResponse resp = client.execute(method);
+ int statusCode = resp.getStatusLine().getStatusCode();
+ if (statusCode != HttpStatus.SC_OK) {
+ response.sendError(statusCode);
+ } else {
+ in = resp.getEntity().getContent();
+ IOUtils.copy(in, response.getOutputStream());
+ }
+ } finally {
+ IOUtils.closeQuietly(in);
+ client.getConnectionManager().shutdown();
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java
new file mode 100644
index 00000000..2d4fa73c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java
@@ -0,0 +1,1983 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.subsonic.ajax.PlayQueueService;
+import net.sourceforge.subsonic.domain.Playlist;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.ajax.ChatService;
+import net.sourceforge.subsonic.ajax.LyricsInfo;
+import net.sourceforge.subsonic.ajax.LyricsService;
+import net.sourceforge.subsonic.command.UserSettingsCommand;
+import net.sourceforge.subsonic.dao.AlbumDao;
+import net.sourceforge.subsonic.dao.ArtistDao;
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.Album;
+import net.sourceforge.subsonic.domain.Artist;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.MusicIndex;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayerTechnology;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.PodcastChannel;
+import net.sourceforge.subsonic.domain.PodcastEpisode;
+import net.sourceforge.subsonic.domain.RandomSearchCriteria;
+import net.sourceforge.subsonic.domain.SearchCriteria;
+import net.sourceforge.subsonic.domain.SearchResult;
+import net.sourceforge.subsonic.domain.Share;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.AudioScrobblerService;
+import net.sourceforge.subsonic.service.JukeboxService;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.PodcastService;
+import net.sourceforge.subsonic.service.RatingService;
+import net.sourceforge.subsonic.service.SearchService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.ShareService;
+import net.sourceforge.subsonic.service.StatusService;
+import net.sourceforge.subsonic.service.TranscodingService;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.XMLBuilder;
+
+import static net.sourceforge.subsonic.security.RESTRequestParameterProcessingFilter.decrypt;
+import static net.sourceforge.subsonic.util.XMLBuilder.Attribute;
+import static net.sourceforge.subsonic.util.XMLBuilder.AttributeSet;
+
+/**
+ * Multi-controller used for the REST API.
+ * <p/>
+ * For documentation, please refer to api.jsp.
+ *
+ * @author Sindre Mehus
+ */
+public class RESTController extends MultiActionController {
+
+ private static final Logger LOG = Logger.getLogger(RESTController.class);
+
+ private SettingsService settingsService;
+ private SecurityService securityService;
+ private PlayerService playerService;
+ private MediaFileService mediaFileService;
+ private TranscodingService transcodingService;
+ private DownloadController downloadController;
+ private CoverArtController coverArtController;
+ private AvatarController avatarController;
+ private UserSettingsController userSettingsController;
+ private LeftController leftController;
+ private HomeController homeController;
+ private StatusService statusService;
+ private StreamController streamController;
+ private ShareService shareService;
+ private PlaylistService playlistService;
+ private ChatService chatService;
+ private LyricsService lyricsService;
+ private PlayQueueService playQueueService;
+ private JukeboxService jukeboxService;
+ private AudioScrobblerService audioScrobblerService;
+ private PodcastService podcastService;
+ private RatingService ratingService;
+ private SearchService searchService;
+ private MediaFileDao mediaFileDao;
+ private ArtistDao artistDao;
+ private AlbumDao albumDao;
+
+ public void ping(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getLicense(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ String email = settingsService.getLicenseEmail();
+ String key = settingsService.getLicenseCode();
+ Date date = settingsService.getLicenseDate();
+ boolean valid = settingsService.isLicenseValid();
+
+ AttributeSet attributes = new AttributeSet();
+ attributes.add("valid", valid);
+ if (valid) {
+ attributes.add("email", email);
+ attributes.add("key", key);
+ attributes.add("date", StringUtil.toISO8601(date));
+ }
+
+ builder.add("license", attributes, true);
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getMusicFolders(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("musicFolders", false);
+
+ for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) {
+ AttributeSet attributes = new AttributeSet();
+ attributes.add("id", musicFolder.getId());
+ attributes.add("name", musicFolder.getName());
+ builder.add("musicFolder", attributes, true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getIndexes(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ long ifModifiedSince = ServletRequestUtils.getLongParameter(request, "ifModifiedSince", 0L);
+ long lastModified = leftController.getLastModified(request);
+
+ if (lastModified <= ifModifiedSince) {
+ builder.endAll();
+ response.getWriter().print(builder);
+ return;
+ }
+
+ builder.add("indexes", "lastModified", lastModified, false);
+
+ List<MusicFolder> musicFolders = settingsService.getAllMusicFolders();
+ Integer musicFolderId = ServletRequestUtils.getIntParameter(request, "musicFolderId");
+ if (musicFolderId != null) {
+ for (MusicFolder musicFolder : musicFolders) {
+ if (musicFolderId.equals(musicFolder.getId())) {
+ musicFolders = Arrays.asList(musicFolder);
+ break;
+ }
+ }
+ }
+
+ List<MediaFile> shortcuts = leftController.getShortcuts(musicFolders, settingsService.getShortcutsAsArray());
+ for (MediaFile shortcut : shortcuts) {
+ builder.add("shortcut", true,
+ new Attribute("name", shortcut.getName()),
+ new Attribute("id", shortcut.getId()));
+ }
+
+ SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists = leftController.getMusicFolderContent(musicFolders).getIndexedArtists();
+
+ for (Map.Entry<MusicIndex, SortedSet<MusicIndex.Artist>> entry : indexedArtists.entrySet()) {
+ builder.add("index", "name", entry.getKey().getIndex(), false);
+
+ for (MusicIndex.Artist artist : entry.getValue()) {
+ for (MediaFile mediaFile : artist.getMediaFiles()) {
+ if (mediaFile.isDirectory()) {
+ builder.add("artist", true,
+ new Attribute("name", artist.getName()),
+ new Attribute("id", mediaFile.getId()));
+ }
+ }
+ }
+ builder.end();
+ }
+
+ // Add children
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+ List<MediaFile> singleSongs = leftController.getSingleSongs(musicFolders);
+
+ for (MediaFile singleSong : singleSongs) {
+ builder.add("child", createAttributesForMediaFile(player, singleSong, username), true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getArtists(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ String username = securityService.getCurrentUsername(request);
+
+ builder.add("artists", false);
+
+ List<Artist> artists = artistDao.getAlphabetialArtists(0, Integer.MAX_VALUE);
+ for (Artist artist : artists) {
+ AttributeSet attributes = createAttributesForArtist(artist, username);
+ builder.add("artist", attributes, true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ private AttributeSet createAttributesForArtist(Artist artist, String username) {
+ AttributeSet attributes = new AttributeSet();
+ attributes.add("id", artist.getId());
+ attributes.add("name", artist.getName());
+ if (artist.getCoverArtPath() != null) {
+ attributes.add("coverArt", CoverArtController.ARTIST_COVERART_PREFIX + artist.getId());
+ }
+ attributes.add("albumCount", artist.getAlbumCount());
+ attributes.add("starred", StringUtil.toISO8601(artistDao.getArtistStarredDate(artist.getId(), username)));
+ return attributes;
+ }
+
+ public void getArtist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ String username = securityService.getCurrentUsername(request);
+ Artist artist;
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ artist = artistDao.getArtist(id);
+ if (artist == null) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.NOT_FOUND, "Artist not found.");
+ return;
+ }
+
+ builder.add("artist", createAttributesForArtist(artist, username), false);
+ for (Album album : albumDao.getAlbumsForArtist(artist.getName())) {
+ builder.add("album", createAttributesForAlbum(album, username), true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ private AttributeSet createAttributesForAlbum(Album album, String username) {
+ AttributeSet attributes;
+ attributes = new AttributeSet();
+ attributes.add("id", album.getId());
+ attributes.add("name", album.getName());
+ attributes.add("artist", album.getArtist());
+ if (album.getArtist() != null) {
+ Artist artist = artistDao.getArtist(album.getArtist());
+ if (artist != null) {
+ attributes.add("artistId", artist.getId());
+ }
+ }
+ if (album.getCoverArtPath() != null) {
+ attributes.add("coverArt", CoverArtController.ALBUM_COVERART_PREFIX + album.getId());
+ }
+ attributes.add("songCount", album.getSongCount());
+ attributes.add("duration", album.getDurationSeconds());
+ attributes.add("created", StringUtil.toISO8601(album.getCreated()));
+ attributes.add("starred", StringUtil.toISO8601(albumDao.getAlbumStarredDate(album.getId(), username)));
+
+ return attributes;
+ }
+
+ private AttributeSet createAttributesForPlaylist(Playlist playlist) {
+ AttributeSet attributes;
+ attributes = new AttributeSet();
+ attributes.add("id", playlist.getId());
+ attributes.add("name", playlist.getName());
+ attributes.add("comment", playlist.getComment());
+ attributes.add("owner", playlist.getUsername());
+ attributes.add("public", playlist.isPublic());
+ attributes.add("songCount", playlist.getFileCount());
+ attributes.add("duration", playlist.getDurationSeconds());
+ attributes.add("created", StringUtil.toISO8601(playlist.getCreated()));
+ return attributes;
+ }
+
+ public void getAlbum(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ Album album;
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ album = albumDao.getAlbum(id);
+ if (album == null) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.NOT_FOUND, "Album not found.");
+ return;
+ }
+
+ builder.add("album", createAttributesForAlbum(album, username), false);
+ for (MediaFile mediaFile : mediaFileDao.getSongsForAlbum(album.getArtist(), album.getName())) {
+ builder.add("song", createAttributesForMediaFile(player, mediaFile, username) , true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getSong(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ MediaFile song;
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ song = mediaFileDao.getMediaFile(id);
+ if (song == null || song.isDirectory()) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.NOT_FOUND, "Song not found.");
+ return;
+ }
+
+ builder.add("song", createAttributesForMediaFile(player, song, username), true);
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getMusicDirectory(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ MediaFile dir;
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ dir = mediaFileService.getMediaFile(id);
+ if (dir == null) {
+ throw new Exception();
+ }
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.NOT_FOUND, "Directory not found");
+ return;
+ }
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("directory", false,
+ new Attribute("id", dir.getId()),
+ new Attribute("name", dir.getName()));
+
+ for (MediaFile child : mediaFileService.getChildrenOf(dir, true, true, true)) {
+ AttributeSet attributes = createAttributesForMediaFile(player, child, username);
+ builder.add("child", attributes, true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ @Deprecated
+ public void search(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ String any = request.getParameter("any");
+ String artist = request.getParameter("artist");
+ String album = request.getParameter("album");
+ String title = request.getParameter("title");
+
+ StringBuilder query = new StringBuilder();
+ if (any != null) {
+ query.append(any).append(" ");
+ }
+ if (artist != null) {
+ query.append(artist).append(" ");
+ }
+ if (album != null) {
+ query.append(album).append(" ");
+ }
+ if (title != null) {
+ query.append(title);
+ }
+
+ SearchCriteria criteria = new SearchCriteria();
+ criteria.setQuery(query.toString().trim());
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "count", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "offset", 0));
+
+ SearchResult result = searchService.search(criteria, SearchService.IndexType.SONG);
+ builder.add("searchResult", false,
+ new Attribute("offset", result.getOffset()),
+ new Attribute("totalHits", result.getTotalHits()));
+
+ for (MediaFile mediaFile : result.getMediaFiles()) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("match", attributes, true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void search2(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ builder.add("searchResult2", false);
+
+ String query = request.getParameter("query");
+ SearchCriteria criteria = new SearchCriteria();
+ criteria.setQuery(StringUtils.trimToEmpty(query));
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "artistCount", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "artistOffset", 0));
+ SearchResult artists = searchService.search(criteria, SearchService.IndexType.ARTIST);
+ for (MediaFile mediaFile : artists.getMediaFiles()) {
+ builder.add("artist", true,
+ new Attribute("name", mediaFile.getName()),
+ new Attribute("id", mediaFile.getId()));
+ }
+
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "albumCount", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "albumOffset", 0));
+ SearchResult albums = searchService.search(criteria, SearchService.IndexType.ALBUM);
+ for (MediaFile mediaFile : albums.getMediaFiles()) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("album", attributes, true);
+ }
+
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "songCount", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "songOffset", 0));
+ SearchResult songs = searchService.search(criteria, SearchService.IndexType.SONG);
+ for (MediaFile mediaFile : songs.getMediaFiles()) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("song", attributes, true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void search3(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ builder.add("searchResult3", false);
+
+ String query = request.getParameter("query");
+ SearchCriteria criteria = new SearchCriteria();
+ criteria.setQuery(StringUtils.trimToEmpty(query));
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "artistCount", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "artistOffset", 0));
+ SearchResult searchResult = searchService.search(criteria, SearchService.IndexType.ARTIST_ID3);
+ for (Artist artist : searchResult.getArtists()) {
+ builder.add("artist", createAttributesForArtist(artist, username), true);
+ }
+
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "albumCount", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "albumOffset", 0));
+ searchResult = searchService.search(criteria, SearchService.IndexType.ALBUM_ID3);
+ for (Album album : searchResult.getAlbums()) {
+ builder.add("album", createAttributesForAlbum(album, username), true);
+ }
+
+ criteria.setCount(ServletRequestUtils.getIntParameter(request, "songCount", 20));
+ criteria.setOffset(ServletRequestUtils.getIntParameter(request, "songOffset", 0));
+ searchResult = searchService.search(criteria, SearchService.IndexType.SONG);
+ for (MediaFile song : searchResult.getMediaFiles()) {
+ builder.add("song", createAttributesForMediaFile(player, song, username), true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getPlaylists(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ User user = securityService.getCurrentUser(request);
+ String authenticatedUsername = user.getUsername();
+ String requestedUsername = request.getParameter("username");
+
+ if (requestedUsername == null) {
+ requestedUsername = authenticatedUsername;
+ } else if (!user.isAdminRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, authenticatedUsername + " is not authorized to get playlists for " + requestedUsername);
+ return;
+ }
+
+ builder.add("playlists", false);
+
+ for (Playlist playlist : playlistService.getReadablePlaylistsForUser(requestedUsername)) {
+ List<String> sharedUsers = playlistService.getPlaylistUsers(playlist.getId());
+ builder.add("playlist", createAttributesForPlaylist(playlist), sharedUsers.isEmpty());
+ if (!sharedUsers.isEmpty()) {
+ for (String username : sharedUsers) {
+ builder.add("allowedUser", (Iterable<Attribute>) null, username, true);
+ }
+ builder.end();
+ }
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+
+ Playlist playlist = playlistService.getPlaylist(id);
+ if (playlist == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id);
+ return;
+ }
+ if (!playlistService.isReadAllowed(playlist, username)) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id);
+ return;
+ }
+ builder.add("playlist", createAttributesForPlaylist(playlist), false);
+ for (String allowedUser : playlistService.getPlaylistUsers(playlist.getId())) {
+ builder.add("allowedUser", (Iterable<Attribute>) null, allowedUser, true);
+ }
+ for (MediaFile mediaFile : playlistService.getFilesInPlaylist(id)) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("entry", attributes, true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void jukeboxControl(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request, true);
+
+ User user = securityService.getCurrentUser(request);
+ if (!user.isJukeboxRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to use jukebox.");
+ return;
+ }
+
+ try {
+ boolean returnPlaylist = false;
+ String action = ServletRequestUtils.getRequiredStringParameter(request, "action");
+ if ("start".equals(action)) {
+ playQueueService.doStart(request, response);
+ } else if ("stop".equals(action)) {
+ playQueueService.doStop(request, response);
+ } else if ("skip".equals(action)) {
+ int index = ServletRequestUtils.getRequiredIntParameter(request, "index");
+ int offset = ServletRequestUtils.getIntParameter(request, "offset", 0);
+ playQueueService.doSkip(request, response, index, offset);
+ } else if ("add".equals(action)) {
+ int[] ids = ServletRequestUtils.getIntParameters(request, "id");
+ playQueueService.doAdd(request, response, ids);
+ } else if ("set".equals(action)) {
+ int[] ids = ServletRequestUtils.getIntParameters(request, "id");
+ playQueueService.doSet(request, response, ids);
+ } else if ("clear".equals(action)) {
+ playQueueService.doClear(request, response);
+ } else if ("remove".equals(action)) {
+ int index = ServletRequestUtils.getRequiredIntParameter(request, "index");
+ playQueueService.doRemove(request, response, index);
+ } else if ("shuffle".equals(action)) {
+ playQueueService.doShuffle(request, response);
+ } else if ("setGain".equals(action)) {
+ float gain = ServletRequestUtils.getRequiredFloatParameter(request, "gain");
+ jukeboxService.setGain(gain);
+ } else if ("get".equals(action)) {
+ returnPlaylist = true;
+ } else if ("status".equals(action)) {
+ // No action necessary.
+ } else {
+ throw new Exception("Unknown jukebox action: '" + action + "'.");
+ }
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+ Player jukeboxPlayer = jukeboxService.getPlayer();
+ boolean controlsJukebox = jukeboxPlayer != null && jukeboxPlayer.getId().equals(player.getId());
+ PlayQueue playQueue = player.getPlayQueue();
+
+ List<Attribute> attrs = new ArrayList<Attribute>(Arrays.asList(
+ new Attribute("currentIndex", controlsJukebox && !playQueue.isEmpty() ? playQueue.getIndex() : -1),
+ new Attribute("playing", controlsJukebox && !playQueue.isEmpty() && playQueue.getStatus() == PlayQueue.Status.PLAYING),
+ new Attribute("gain", jukeboxService.getGain()),
+ new Attribute("position", controlsJukebox && !playQueue.isEmpty() ? jukeboxService.getPosition() : 0)));
+
+ if (returnPlaylist) {
+ builder.add("jukeboxPlaylist", attrs, false);
+ List<MediaFile> result;
+ synchronized (playQueue) {
+ result = playQueue.getFiles();
+ }
+ for (MediaFile mediaFile : result) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("entry", attributes, true);
+ }
+ } else {
+ builder.add("jukeboxStatus", attrs, false);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void createPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request, true);
+ String username = securityService.getCurrentUsername(request);
+
+ try {
+
+ Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlistId");
+ String name = request.getParameter("name");
+ if (playlistId == null && name == null) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, "Playlist ID or name must be specified.");
+ return;
+ }
+
+ Playlist playlist;
+ if (playlistId != null) {
+ playlist = playlistService.getPlaylist(playlistId);
+ if (playlist == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + playlistId);
+ return;
+ }
+ if (!playlistService.isWriteAllowed(playlist, username)) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + playlistId);
+ return;
+ }
+ } else {
+ playlist = new Playlist();
+ playlist.setName(name);
+ playlist.setCreated(new Date());
+ playlist.setChanged(new Date());
+ playlist.setPublic(false);
+ playlist.setUsername(username);
+ playlistService.createPlaylist(playlist);
+ }
+
+ List<MediaFile> songs = new ArrayList<MediaFile>();
+ for (int id : ServletRequestUtils.getIntParameters(request, "songId")) {
+ MediaFile song = mediaFileService.getMediaFile(id);
+ if (song != null) {
+ songs.add(song);
+ }
+ }
+ playlistService.setFilesInPlaylist(playlist.getId(), songs);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void updatePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request, true);
+ String username = securityService.getCurrentUsername(request);
+
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "playlistId");
+ Playlist playlist = playlistService.getPlaylist(id);
+ if (playlist == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id);
+ return;
+ }
+ if (!playlistService.isWriteAllowed(playlist, username)) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id);
+ return;
+ }
+
+ String name = request.getParameter("name");
+ if (name != null) {
+ playlist.setName(name);
+ }
+ String comment = request.getParameter("comment");
+ if (comment != null) {
+ playlist.setComment(comment);
+ }
+ Boolean isPublic = ServletRequestUtils.getBooleanParameter(request, "public");
+ if (isPublic != null) {
+ playlist.setPublic(isPublic);
+ }
+ playlistService.updatePlaylist(playlist);
+
+ // TODO: Add later
+// for (String usernameToAdd : ServletRequestUtils.getStringParameters(request, "usernameToAdd")) {
+// if (securityService.getUserByName(usernameToAdd) != null) {
+// playlistService.addPlaylistUser(id, usernameToAdd);
+// }
+// }
+// for (String usernameToRemove : ServletRequestUtils.getStringParameters(request, "usernameToRemove")) {
+// if (securityService.getUserByName(usernameToRemove) != null) {
+// playlistService.deletePlaylistUser(id, usernameToRemove);
+// }
+// }
+ List<MediaFile> songs = playlistService.getFilesInPlaylist(id);
+ boolean songsChanged = false;
+
+ SortedSet<Integer> tmp = new TreeSet<Integer>();
+ for (int songIndexToRemove : ServletRequestUtils.getIntParameters(request, "songIndexToRemove")) {
+ tmp.add(songIndexToRemove);
+ }
+ List<Integer> songIndexesToRemove = new ArrayList<Integer>(tmp);
+ Collections.reverse(songIndexesToRemove);
+ for (Integer songIndexToRemove : songIndexesToRemove) {
+ songs.remove(songIndexToRemove.intValue());
+ songsChanged = true;
+ }
+ for (int songToAdd : ServletRequestUtils.getIntParameters(request, "songIdToAdd")) {
+ MediaFile song = mediaFileService.getMediaFile(songToAdd);
+ if (song != null) {
+ songs.add(song);
+ songsChanged = true;
+ }
+ }
+ if (songsChanged) {
+ playlistService.setFilesInPlaylist(id, songs);
+ }
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void deletePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request, true);
+ String username = securityService.getCurrentUsername(request);
+
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ Playlist playlist = playlistService.getPlaylist(id);
+ if (playlist == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id);
+ return;
+ }
+ if (!playlistService.isWriteAllowed(playlist, username)) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id);
+ return;
+ }
+ playlistService.deletePlaylist(id);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getAlbumList(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("albumList", false);
+
+ try {
+ int size = ServletRequestUtils.getIntParameter(request, "size", 10);
+ int offset = ServletRequestUtils.getIntParameter(request, "offset", 0);
+ size = Math.max(0, Math.min(size, 500));
+ String type = ServletRequestUtils.getRequiredStringParameter(request, "type");
+
+ List<HomeController.Album> albums;
+ if ("highest".equals(type)) {
+ albums = homeController.getHighestRated(offset, size);
+ } else if ("frequent".equals(type)) {
+ albums = homeController.getMostFrequent(offset, size);
+ } else if ("recent".equals(type)) {
+ albums = homeController.getMostRecent(offset, size);
+ } else if ("newest".equals(type)) {
+ albums = homeController.getNewest(offset, size);
+ } else if ("starred".equals(type)) {
+ albums = homeController.getStarred(offset, size, username);
+ } else if ("alphabeticalByArtist".equals(type)) {
+ albums = homeController.getAlphabetical(offset, size, true);
+ } else if ("alphabeticalByName".equals(type)) {
+ albums = homeController.getAlphabetical(offset, size, false);
+ } else if ("random".equals(type)) {
+ albums = homeController.getRandom(size);
+ } else {
+ throw new Exception("Invalid list type: " + type);
+ }
+
+ for (HomeController.Album album : albums) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(album.getPath());
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("album", attributes, true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getAlbumList2(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("albumList2", false);
+
+ try {
+ int size = ServletRequestUtils.getIntParameter(request, "size", 10);
+ int offset = ServletRequestUtils.getIntParameter(request, "offset", 0);
+ size = Math.max(0, Math.min(size, 500));
+ String type = ServletRequestUtils.getRequiredStringParameter(request, "type");
+ String username = securityService.getCurrentUsername(request);
+
+ List<Album> albums;
+ if ("frequent".equals(type)) {
+ albums = albumDao.getMostFrequentlyPlayedAlbums(offset, size);
+ } else if ("recent".equals(type)) {
+ albums = albumDao.getMostRecentlyPlayedAlbums(offset, size);
+ } else if ("newest".equals(type)) {
+ albums = albumDao.getNewestAlbums(offset, size);
+ } else if ("alphabeticalByArtist".equals(type)) {
+ albums = albumDao.getAlphabetialAlbums(offset, size, true);
+ } else if ("alphabeticalByName".equals(type)) {
+ albums = albumDao.getAlphabetialAlbums(offset, size, false);
+ } else if ("starred".equals(type)) {
+ albums = albumDao.getStarredAlbums(offset, size, securityService.getCurrentUser(request).getUsername());
+ } else if ("random".equals(type)) {
+ albums = searchService.getRandomAlbumsId3(size);
+ } else {
+ throw new Exception("Invalid list type: " + type);
+ }
+ for (Album album : albums) {
+ builder.add("album", createAttributesForAlbum(album, username), true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getRandomSongs(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("randomSongs", false);
+
+ try {
+ int size = ServletRequestUtils.getIntParameter(request, "size", 10);
+ size = Math.max(0, Math.min(size, 500));
+ String genre = ServletRequestUtils.getStringParameter(request, "genre");
+ Integer fromYear = ServletRequestUtils.getIntParameter(request, "fromYear");
+ Integer toYear = ServletRequestUtils.getIntParameter(request, "toYear");
+ Integer musicFolderId = ServletRequestUtils.getIntParameter(request, "musicFolderId");
+ RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolderId);
+
+ for (MediaFile mediaFile : searchService.getRandomSongs(criteria)) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("song", attributes, true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getVideos(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("videos", false);
+ try {
+ int size = ServletRequestUtils.getIntParameter(request, "size", Integer.MAX_VALUE);
+ int offset = ServletRequestUtils.getIntParameter(request, "offset", 0);
+
+ for (MediaFile mediaFile : mediaFileDao.getVideos(size, offset)) {
+ builder.add("video", createAttributesForMediaFile(player, mediaFile, username), true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getNowPlaying(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("nowPlaying", false);
+
+ for (TransferStatus status : statusService.getAllStreamStatuses()) {
+
+ Player player = status.getPlayer();
+ File file = status.getFile();
+ if (player != null && player.getUsername() != null && file != null) {
+
+ String username = player.getUsername();
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ if (!userSettings.isNowPlayingAllowed()) {
+ continue;
+ }
+
+ MediaFile mediaFile = mediaFileService.getMediaFile(file);
+
+ long minutesAgo = status.getMillisSinceLastUpdate() / 1000L / 60L;
+ if (minutesAgo < 60) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ attributes.add("username", username);
+ attributes.add("playerId", player.getId());
+ attributes.add("playerName", player.getName());
+ attributes.add("minutesAgo", minutesAgo);
+ builder.add("entry", attributes, true);
+ }
+ }
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ private AttributeSet createAttributesForMediaFile(Player player, MediaFile mediaFile, String username) {
+ MediaFile parent = mediaFileService.getParentOf(mediaFile);
+ AttributeSet attributes = new AttributeSet();
+ attributes.add("id", mediaFile.getId());
+ try {
+ if (!mediaFileService.isRoot(parent)) {
+ attributes.add("parent", parent.getId());
+ }
+ } catch (SecurityException x) {
+ // Ignored.
+ }
+ attributes.add("title", mediaFile.getName());
+ attributes.add("album", mediaFile.getAlbumName());
+ attributes.add("artist", mediaFile.getArtist());
+ attributes.add("isDir", mediaFile.isDirectory());
+ attributes.add("coverArt", findCoverArt(mediaFile, parent));
+ attributes.add("created", StringUtil.toISO8601(mediaFile.getCreated()));
+ attributes.add("starred", StringUtil.toISO8601(mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username)));
+ attributes.add("userRating", ratingService.getRatingForUser(username, mediaFile));
+ attributes.add("averageRating", ratingService.getAverageRating(mediaFile));
+
+ if (mediaFile.isFile()) {
+ attributes.add("duration", mediaFile.getDurationSeconds());
+ attributes.add("bitRate", mediaFile.getBitRate());
+ attributes.add("track", mediaFile.getTrackNumber());
+ attributes.add("discNumber", mediaFile.getDiscNumber());
+ attributes.add("year", mediaFile.getYear());
+ attributes.add("genre", mediaFile.getGenre());
+ attributes.add("size", mediaFile.getFileSize());
+ String suffix = mediaFile.getFormat();
+ attributes.add("suffix", suffix);
+ attributes.add("contentType", StringUtil.getMimeType(suffix));
+ attributes.add("isVideo", mediaFile.isVideo());
+ attributes.add("path", getRelativePath(mediaFile));
+
+ if (mediaFile.getArtist() != null && mediaFile.getAlbumName() != null) {
+ Album album = albumDao.getAlbum(mediaFile.getAlbumArtist(), mediaFile.getAlbumName());
+ if (album != null) {
+ attributes.add("albumId", album.getId());
+ }
+ }
+ if (mediaFile.getArtist() != null) {
+ Artist artist = artistDao.getArtist(mediaFile.getArtist());
+ if (artist != null) {
+ attributes.add("artistId", artist.getId());
+ }
+ }
+ switch (mediaFile.getMediaType()) {
+ case MUSIC:
+ attributes.add("type", "music");
+ break;
+ case PODCAST:
+ attributes.add("type", "podcast");
+ break;
+ case AUDIOBOOK:
+ attributes.add("type", "audiobook");
+ break;
+ default:
+ break;
+ }
+
+ if (transcodingService.isTranscodingRequired(mediaFile, player)) {
+ String transcodedSuffix = transcodingService.getSuffix(player, mediaFile, null);
+ attributes.add("transcodedSuffix", transcodedSuffix);
+ attributes.add("transcodedContentType", StringUtil.getMimeType(transcodedSuffix));
+ }
+ }
+ return attributes;
+ }
+
+ private Integer findCoverArt(MediaFile mediaFile, MediaFile parent) {
+ MediaFile dir = mediaFile.isDirectory() ? mediaFile : parent;
+ if (dir != null && dir.getCoverArtPath() != null) {
+ return dir.getId();
+ }
+ return null;
+ }
+
+ private String getRelativePath(MediaFile musicFile) {
+
+ String filePath = musicFile.getPath();
+
+ // Convert slashes.
+ filePath = filePath.replace('\\', '/');
+
+ String filePathLower = filePath.toLowerCase();
+
+ List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(false, true);
+ for (MusicFolder musicFolder : musicFolders) {
+ String folderPath = musicFolder.getPath().getPath();
+ folderPath = folderPath.replace('\\', '/');
+ String folderPathLower = folderPath.toLowerCase();
+
+ if (filePathLower.startsWith(folderPathLower)) {
+ String relativePath = filePath.substring(folderPath.length());
+ return relativePath.startsWith("/") ? relativePath.substring(1) : relativePath;
+ }
+ }
+
+ return null;
+ }
+
+ public ModelAndView download(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ User user = securityService.getCurrentUser(request);
+ if (!user.isDownloadRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to download files.");
+ return null;
+ }
+
+ long ifModifiedSince = request.getDateHeader("If-Modified-Since");
+ long lastModified = downloadController.getLastModified(request);
+
+ if (ifModifiedSince != -1 && lastModified != -1 && lastModified <= ifModifiedSince) {
+ response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+ return null;
+ }
+
+ if (lastModified != -1) {
+ response.setDateHeader("Last-Modified", lastModified);
+ }
+
+ return downloadController.handleRequest(request, response);
+ }
+
+ public ModelAndView stream(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ User user = securityService.getCurrentUser(request);
+ if (!user.isStreamRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to play files.");
+ return null;
+ }
+
+ streamController.handleRequest(request, response);
+ return null;
+ }
+
+ public void scrobble(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ Player player = playerService.getPlayer(request, response);
+
+ if (!settingsService.getUserSettings(player.getUsername()).isLastFmEnabled()) {
+ error(request, response, ErrorCode.GENERIC, "Scrobbling is not enabled for " + player.getUsername() + ".");
+ return;
+ }
+
+ try {
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ MediaFile file = mediaFileService.getMediaFile(id);
+ if (file == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "File not found: " + id);
+ return;
+ }
+ boolean submission = ServletRequestUtils.getBooleanParameter(request, "submission", true);
+ audioScrobblerService.register(file, player.getUsername(), submission);
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ return;
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void star(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ starOrUnstar(request, response, true);
+ }
+
+ public void unstar(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ starOrUnstar(request, response, false);
+ }
+
+ private void starOrUnstar(HttpServletRequest request, HttpServletResponse response, boolean star) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ try {
+ String username = securityService.getCurrentUser(request).getUsername();
+ for (int id : ServletRequestUtils.getIntParameters(request, "id")) {
+ MediaFile mediaFile = mediaFileDao.getMediaFile(id);
+ if (mediaFile == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Media file not found: " + id);
+ return;
+ }
+ if (star) {
+ mediaFileDao.starMediaFile(id, username);
+ } else {
+ mediaFileDao.unstarMediaFile(id, username);
+ }
+ }
+ for (int albumId : ServletRequestUtils.getIntParameters(request, "albumId")) {
+ Album album = albumDao.getAlbum(albumId);
+ if (album == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Album not found: " + albumId);
+ return;
+ }
+ if (star) {
+ albumDao.starAlbum(albumId, username);
+ } else {
+ albumDao.unstarAlbum(albumId, username);
+ }
+ }
+ for (int artistId : ServletRequestUtils.getIntParameters(request, "artistId")) {
+ Artist artist = artistDao.getArtist(artistId);
+ if (artist == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Artist not found: " + artistId);
+ return;
+ }
+ if (star) {
+ artistDao.starArtist(artistId, username);
+ } else {
+ artistDao.unstarArtist(artistId, username);
+ }
+ }
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ return;
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getStarred(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("starred", false);
+ for (MediaFile artist : mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username)) {
+ builder.add("artist", true,
+ new Attribute("name", artist.getName()),
+ new Attribute("id", artist.getId()));
+ }
+ for (MediaFile album : mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username)) {
+ builder.add("album", createAttributesForMediaFile(player, album, username), true);
+ }
+ for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username)) {
+ builder.add("song", createAttributesForMediaFile(player, song, username), true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getStarred2(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("starred2", false);
+ for (Artist artist : artistDao.getStarredArtists(0, Integer.MAX_VALUE, username)) {
+ builder.add("artist", createAttributesForArtist(artist, username), true);
+ }
+ for (Album album : albumDao.getStarredAlbums(0, Integer.MAX_VALUE, username)) {
+ builder.add("album", createAttributesForAlbum(album, username), true);
+ }
+ for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username)) {
+ builder.add("song", createAttributesForMediaFile(player, song, username), true);
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getPodcasts(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ builder.add("podcasts", false);
+
+ for (PodcastChannel channel : podcastService.getAllChannels()) {
+ AttributeSet channelAttrs = new AttributeSet();
+ channelAttrs.add("id", channel.getId());
+ channelAttrs.add("url", channel.getUrl());
+ channelAttrs.add("status", channel.getStatus().toString().toLowerCase());
+ channelAttrs.add("title", channel.getTitle());
+ channelAttrs.add("description", channel.getDescription());
+ channelAttrs.add("errorMessage", channel.getErrorMessage());
+ builder.add("channel", channelAttrs, false);
+
+ List<PodcastEpisode> episodes = podcastService.getEpisodes(channel.getId(), false);
+ for (PodcastEpisode episode : episodes) {
+ AttributeSet episodeAttrs = new AttributeSet();
+
+ String path = episode.getPath();
+ if (path != null) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(path);
+ episodeAttrs.addAll(createAttributesForMediaFile(player, mediaFile, username));
+ episodeAttrs.add("streamId", mediaFile.getId());
+ }
+
+ episodeAttrs.add("id", episode.getId()); // Overwrites the previous "id" attribute.
+ episodeAttrs.add("status", episode.getStatus().toString().toLowerCase());
+ episodeAttrs.add("title", episode.getTitle());
+ episodeAttrs.add("description", episode.getDescription());
+ episodeAttrs.add("publishDate", episode.getPublishDate());
+
+ builder.add("episode", episodeAttrs, true);
+ }
+
+ builder.end(); // <channel>
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void getShares(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ User user = securityService.getCurrentUser(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ builder.add("shares", false);
+ for (Share share : shareService.getSharesForUser(user)) {
+ builder.add("share", createAttributesForShare(share), false);
+
+ for (MediaFile mediaFile : shareService.getSharedFiles(share.getId())) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("entry", attributes, true);
+ }
+
+ builder.end();
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void createShare(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ Player player = playerService.getPlayer(request, response);
+ String username = securityService.getCurrentUsername(request);
+
+ User user = securityService.getCurrentUser(request);
+ if (!user.isShareRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to share media.");
+ return;
+ }
+
+ if (!settingsService.isUrlRedirectionEnabled()) {
+ error(request, response, ErrorCode.GENERIC, "Sharing is only supported for *.subsonic.org domain names.");
+ return;
+ }
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ try {
+
+ List<MediaFile> files = new ArrayList<MediaFile>();
+ for (int id : ServletRequestUtils.getRequiredIntParameters(request, "id")) {
+ files.add(mediaFileService.getMediaFile(id));
+ }
+
+ // TODO: Update api.jsp
+
+ Share share = shareService.createShare(request, files);
+ share.setDescription(request.getParameter("description"));
+ long expires = ServletRequestUtils.getLongParameter(request, "expires", 0L);
+ if (expires != 0) {
+ share.setExpires(new Date(expires));
+ }
+ shareService.updateShare(share);
+
+ builder.add("shares", false);
+ builder.add("share", createAttributesForShare(share), false);
+
+ for (MediaFile mediaFile : shareService.getSharedFiles(share.getId())) {
+ AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username);
+ builder.add("entry", attributes, true);
+ }
+
+ builder.endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void deleteShare(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ try {
+ request = wrapRequest(request);
+ User user = securityService.getCurrentUser(request);
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+
+ Share share = shareService.getShareById(id);
+ if (share == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Shared media not found.");
+ return;
+ }
+ if (!user.isAdminRole() && !share.getUsername().equals(user.getUsername())) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, "Not authorized to delete shared media.");
+ return;
+ }
+
+ shareService.deleteShare(id);
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void updateShare(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ try {
+ request = wrapRequest(request);
+ User user = securityService.getCurrentUser(request);
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+
+ Share share = shareService.getShareById(id);
+ if (share == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "Shared media not found.");
+ return;
+ }
+ if (!user.isAdminRole() && !share.getUsername().equals(user.getUsername())) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, "Not authorized to modify shared media.");
+ return;
+ }
+
+ share.setDescription(request.getParameter("description"));
+ String expiresString = request.getParameter("expires");
+ if (expiresString != null) {
+ long expires = Long.parseLong(expiresString);
+ share.setExpires(expires == 0L ? null : new Date(expires));
+ }
+ shareService.updateShare(share);
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ private List<Attribute> createAttributesForShare(Share share) {
+ List<Attribute> attributes = new ArrayList<Attribute>();
+
+ attributes.add(new Attribute("id", share.getId()));
+ attributes.add(new Attribute("url", shareService.getShareUrl(share)));
+ attributes.add(new Attribute("username", share.getUsername()));
+ attributes.add(new Attribute("created", StringUtil.toISO8601(share.getCreated())));
+ attributes.add(new Attribute("visitCount", share.getVisitCount()));
+ attributes.add(new Attribute("description", share.getDescription()));
+ attributes.add(new Attribute("expires", StringUtil.toISO8601(share.getExpires())));
+ attributes.add(new Attribute("lastVisited", StringUtil.toISO8601(share.getLastVisited())));
+
+ return attributes;
+ }
+
+ public ModelAndView videoPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ MediaFile file = mediaFileService.getMediaFile(id);
+
+ int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0);
+ timeOffset = Math.max(0, timeOffset);
+ Integer duration = file.getDurationSeconds();
+ if (duration != null) {
+ map.put("skipOffsets", VideoPlayerController.createSkipOffsets(duration));
+ timeOffset = Math.min(duration, timeOffset);
+ duration -= timeOffset;
+ }
+
+ map.put("id", request.getParameter("id"));
+ map.put("u", request.getParameter("u"));
+ map.put("p", request.getParameter("p"));
+ map.put("c", request.getParameter("c"));
+ map.put("v", request.getParameter("v"));
+ map.put("video", file);
+ map.put("maxBitRate", ServletRequestUtils.getIntParameter(request, "maxBitRate", VideoPlayerController.DEFAULT_BIT_RATE));
+ map.put("duration", duration);
+ map.put("timeOffset", timeOffset);
+ map.put("bitRates", VideoPlayerController.BIT_RATES);
+ map.put("autoplay", ServletRequestUtils.getBooleanParameter(request, "autoplay", true));
+
+ ModelAndView result = new ModelAndView("rest/videoPlayer");
+ result.addObject("model", map);
+ return result;
+ }
+
+ public ModelAndView getCoverArt(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ return coverArtController.handleRequest(request, response);
+ }
+
+ public ModelAndView getAvatar(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ return avatarController.handleRequest(request, response);
+ }
+
+ public void changePassword(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ try {
+
+ String username = ServletRequestUtils.getRequiredStringParameter(request, "username");
+ String password = decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password"));
+
+ User authUser = securityService.getCurrentUser(request);
+ if (!authUser.isAdminRole() && !username.equals(authUser.getUsername())) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, authUser.getUsername() + " is not authorized to change password for " + username);
+ return;
+ }
+
+ User user = securityService.getUserByName(username);
+ user.setPassword(password);
+ securityService.updateUser(user);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+
+ String username;
+ try {
+ username = ServletRequestUtils.getRequiredStringParameter(request, "username");
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ return;
+ }
+
+ User currentUser = securityService.getCurrentUser(request);
+ if (!username.equals(currentUser.getUsername()) && !currentUser.isAdminRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, currentUser.getUsername() + " is not authorized to get details for other users.");
+ return;
+ }
+
+ User requestedUser = securityService.getUserByName(username);
+ if (requestedUser == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "No such user: " + username);
+ return;
+ }
+
+ UserSettings userSettings = settingsService.getUserSettings(username);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ List<Attribute> attributes = Arrays.asList(
+ new Attribute("username", requestedUser.getUsername()),
+ new Attribute("email", requestedUser.getEmail()),
+ new Attribute("scrobblingEnabled", userSettings.isLastFmEnabled()),
+ new Attribute("adminRole", requestedUser.isAdminRole()),
+ new Attribute("settingsRole", requestedUser.isSettingsRole()),
+ new Attribute("downloadRole", requestedUser.isDownloadRole()),
+ new Attribute("uploadRole", requestedUser.isUploadRole()),
+ new Attribute("playlistRole", true), // Since 1.8.0
+ new Attribute("coverArtRole", requestedUser.isCoverArtRole()),
+ new Attribute("commentRole", requestedUser.isCommentRole()),
+ new Attribute("podcastRole", requestedUser.isPodcastRole()),
+ new Attribute("streamRole", requestedUser.isStreamRole()),
+ new Attribute("jukeboxRole", requestedUser.isJukeboxRole()),
+ new Attribute("shareRole", requestedUser.isShareRole())
+ );
+
+ builder.add("user", attributes, true);
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void createUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ User user = securityService.getCurrentUser(request);
+ if (!user.isAdminRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to create new users.");
+ return;
+ }
+
+ try {
+ UserSettingsCommand command = new UserSettingsCommand();
+ command.setUsername(ServletRequestUtils.getRequiredStringParameter(request, "username"));
+ command.setPassword(decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password")));
+ command.setEmail(ServletRequestUtils.getRequiredStringParameter(request, "email"));
+ command.setLdapAuthenticated(ServletRequestUtils.getBooleanParameter(request, "ldapAuthenticated", false));
+ command.setAdminRole(ServletRequestUtils.getBooleanParameter(request, "adminRole", false));
+ command.setCommentRole(ServletRequestUtils.getBooleanParameter(request, "commentRole", false));
+ command.setCoverArtRole(ServletRequestUtils.getBooleanParameter(request, "coverArtRole", false));
+ command.setDownloadRole(ServletRequestUtils.getBooleanParameter(request, "downloadRole", false));
+ command.setStreamRole(ServletRequestUtils.getBooleanParameter(request, "streamRole", true));
+ command.setUploadRole(ServletRequestUtils.getBooleanParameter(request, "uploadRole", false));
+ command.setJukeboxRole(ServletRequestUtils.getBooleanParameter(request, "jukeboxRole", false));
+ command.setPodcastRole(ServletRequestUtils.getBooleanParameter(request, "podcastRole", false));
+ command.setSettingsRole(ServletRequestUtils.getBooleanParameter(request, "settingsRole", true));
+ command.setTranscodeSchemeName(ServletRequestUtils.getStringParameter(request, "transcodeScheme", TranscodeScheme.OFF.name()));
+ command.setShareRole(ServletRequestUtils.getBooleanParameter(request, "shareRole", false));
+
+ userSettingsController.createUser(command);
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void deleteUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ User user = securityService.getCurrentUser(request);
+ if (!user.isAdminRole()) {
+ error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to delete users.");
+ return;
+ }
+
+ try {
+ String username = ServletRequestUtils.getRequiredStringParameter(request, "username");
+ securityService.deleteUser(username);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ } catch (Exception x) {
+ LOG.warn("Error in REST API.", x);
+ error(request, response, ErrorCode.GENERIC, getErrorMessage(x));
+ }
+ }
+
+ public void getChatMessages(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+
+ long since = ServletRequestUtils.getLongParameter(request, "since", 0L);
+
+ builder.add("chatMessages", false);
+
+ for (ChatService.Message message : chatService.getMessages(0L).getMessages()) {
+ long time = message.getDate().getTime();
+ if (time > since) {
+ builder.add("chatMessage", true, new Attribute("username", message.getUsername()),
+ new Attribute("time", time), new Attribute("message", message.getContent()));
+ }
+ }
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void addChatMessage(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ try {
+ chatService.doAddMessage(ServletRequestUtils.getRequiredStringParameter(request, "message"), request);
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ }
+ }
+
+ public void getLyrics(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ String artist = request.getParameter("artist");
+ String title = request.getParameter("title");
+ LyricsInfo lyrics = lyricsService.getLyrics(artist, title);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true);
+ AttributeSet attributes = new AttributeSet();
+ attributes.add("artist", lyrics.getArtist());
+ attributes.add("title", lyrics.getTitle());
+ builder.add("lyrics", attributes, lyrics.getLyrics(), true);
+
+ builder.endAll();
+ response.getWriter().print(builder);
+ }
+
+ public void setRating(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request = wrapRequest(request);
+ try {
+ Integer rating = ServletRequestUtils.getRequiredIntParameter(request, "rating");
+ if (rating == 0) {
+ rating = null;
+ }
+
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ MediaFile mediaFile = mediaFileService.getMediaFile(id);
+ if (mediaFile == null) {
+ error(request, response, ErrorCode.NOT_FOUND, "File not found: " + id);
+ return;
+ }
+
+ String username = securityService.getCurrentUsername(request);
+ ratingService.setRatingForUser(username, mediaFile, rating);
+
+ XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
+ response.getWriter().print(builder);
+ } catch (ServletRequestBindingException x) {
+ error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x));
+ }
+ }
+
+ private HttpServletRequest wrapRequest(HttpServletRequest request) {
+ return wrapRequest(request, false);
+ }
+
+ private HttpServletRequest wrapRequest(final HttpServletRequest request, boolean jukebox) {
+ final String playerId = createPlayerIfNecessary(request, jukebox);
+ return new HttpServletRequestWrapper(request) {
+ @Override
+ public String getParameter(String name) {
+ // Returns the correct player to be used in PlayerService.getPlayer()
+ if ("player".equals(name)) {
+ return playerId;
+ }
+
+ // Support old style ID parameters.
+ if ("id".equals(name)) {
+ return mapId(request.getParameter("id"));
+ }
+
+ return super.getParameter(name);
+ }
+ };
+ }
+
+ private String mapId(String id) {
+ if (id == null || id.startsWith(CoverArtController.ALBUM_COVERART_PREFIX) ||
+ id.startsWith(CoverArtController.ARTIST_COVERART_PREFIX) || StringUtils.isNumeric(id)) {
+ return id;
+ }
+
+ try {
+ String path = StringUtil.utf8HexDecode(id);
+ MediaFile mediaFile = mediaFileService.getMediaFile(path);
+ return String.valueOf(mediaFile.getId());
+ } catch (Exception x) {
+ return id;
+ }
+ }
+
+ private String getErrorMessage(Exception x) {
+ if (x.getMessage() != null) {
+ return x.getMessage();
+ }
+ return x.getClass().getSimpleName();
+ }
+
+ private void error(HttpServletRequest request, HttpServletResponse response, ErrorCode code, String message) throws IOException {
+ XMLBuilder builder = createXMLBuilder(request, response, false);
+ builder.add("error", true,
+ new XMLBuilder.Attribute("code", code.getCode()),
+ new XMLBuilder.Attribute("message", message));
+ builder.end();
+ response.getWriter().print(builder);
+ }
+
+ private XMLBuilder createXMLBuilder(HttpServletRequest request, HttpServletResponse response, boolean ok) throws IOException {
+ String format = ServletRequestUtils.getStringParameter(request, "f", "xml");
+ boolean json = "json".equals(format);
+ boolean jsonp = "jsonp".equals(format);
+ XMLBuilder builder;
+
+ response.setCharacterEncoding(StringUtil.ENCODING_UTF8);
+
+ if (json) {
+ builder = XMLBuilder.createJSONBuilder();
+ response.setContentType("application/json");
+ } else if (jsonp) {
+ builder = XMLBuilder.createJSONPBuilder(request.getParameter("callback"));
+ response.setContentType("text/javascript");
+ } else {
+ builder = XMLBuilder.createXMLBuilder();
+ response.setContentType("text/xml");
+ }
+
+ builder.preamble(StringUtil.ENCODING_UTF8);
+ builder.add("subsonic-response", false,
+ new Attribute("xmlns", "http://subsonic.org/restapi"),
+ new Attribute("status", ok ? "ok" : "failed"),
+ new Attribute("version", StringUtil.getRESTProtocolVersion()));
+ return builder;
+ }
+
+ private String createPlayerIfNecessary(HttpServletRequest request, boolean jukebox) {
+ String username = request.getRemoteUser();
+ String clientId = request.getParameter("c");
+ if (jukebox) {
+ clientId += "-jukebox";
+ }
+
+ List<Player> players = playerService.getPlayersForUserAndClientId(username, clientId);
+
+ // If not found, create it.
+ if (players.isEmpty()) {
+ Player player = new Player();
+ player.setIpAddress(request.getRemoteAddr());
+ player.setUsername(username);
+ player.setClientId(clientId);
+ player.setName(clientId);
+ player.setTechnology(jukebox ? PlayerTechnology.JUKEBOX : PlayerTechnology.EXTERNAL_WITH_PLAYLIST);
+ playerService.createPlayer(player);
+ players = playerService.getPlayersForUserAndClientId(username, clientId);
+ }
+
+ // Return the player ID.
+ return !players.isEmpty() ? players.get(0).getId() : null;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+
+ public void setDownloadController(DownloadController downloadController) {
+ this.downloadController = downloadController;
+ }
+
+ public void setCoverArtController(CoverArtController coverArtController) {
+ this.coverArtController = coverArtController;
+ }
+
+ public void setUserSettingsController(UserSettingsController userSettingsController) {
+ this.userSettingsController = userSettingsController;
+ }
+
+ public void setLeftController(LeftController leftController) {
+ this.leftController = leftController;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setStreamController(StreamController streamController) {
+ this.streamController = streamController;
+ }
+
+ public void setChatService(ChatService chatService) {
+ this.chatService = chatService;
+ }
+
+ public void setHomeController(HomeController homeController) {
+ this.homeController = homeController;
+ }
+
+ public void setLyricsService(LyricsService lyricsService) {
+ this.lyricsService = lyricsService;
+ }
+
+ public void setPlayQueueService(PlayQueueService playQueueService) {
+ this.playQueueService = playQueueService;
+ }
+
+ public void setJukeboxService(JukeboxService jukeboxService) {
+ this.jukeboxService = jukeboxService;
+ }
+
+ public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
+ this.audioScrobblerService = audioScrobblerService;
+ }
+
+ public void setPodcastService(PodcastService podcastService) {
+ this.podcastService = podcastService;
+ }
+
+ public void setRatingService(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+
+ public void setShareService(ShareService shareService) {
+ this.shareService = shareService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setAvatarController(AvatarController avatarController) {
+ this.avatarController = avatarController;
+ }
+
+ public void setArtistDao(ArtistDao artistDao) {
+ this.artistDao = artistDao;
+ }
+
+ public void setAlbumDao(AlbumDao albumDao) {
+ this.albumDao = albumDao;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ public static enum ErrorCode {
+
+ GENERIC(0, "A generic error."),
+ MISSING_PARAMETER(10, "Required parameter is missing."),
+ PROTOCOL_MISMATCH_CLIENT_TOO_OLD(20, "Incompatible Subsonic REST protocol version. Client must upgrade."),
+ PROTOCOL_MISMATCH_SERVER_TOO_OLD(30, "Incompatible Subsonic REST protocol version. Server must upgrade."),
+ NOT_AUTHENTICATED(40, "Wrong username or password."),
+ NOT_AUTHORIZED(50, "User is not authorized for the given operation."),
+ NOT_LICENSED(60, "The trial period for the Subsonic server is over. Please donate to get a license key. Visit subsonic.org for details."),
+ NOT_FOUND(70, "Requested data was not found.");
+
+ private final int code;
+ private final String message;
+
+ ErrorCode(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java
new file mode 100644
index 00000000..a3738684
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java
@@ -0,0 +1,101 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.RandomSearchCriteria;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SearchService;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for the creating a random play queue.
+ *
+ * @author Sindre Mehus
+ */
+public class RandomPlayQueueController extends ParameterizableViewController {
+
+ private PlayerService playerService;
+ private List<ReloadFrame> reloadFrames;
+ private SearchService searchService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ int size = ServletRequestUtils.getRequiredIntParameter(request, "size");
+ String genre = request.getParameter("genre");
+ if (StringUtils.equalsIgnoreCase("any", genre)) {
+ genre = null;
+ }
+
+ Integer fromYear = null;
+ Integer toYear = null;
+
+ String year = request.getParameter("year");
+ if (!StringUtils.equalsIgnoreCase("any", year)) {
+ String[] tmp = StringUtils.split(year);
+ fromYear = Integer.parseInt(tmp[0]);
+ toYear = Integer.parseInt(tmp[1]);
+ }
+
+ Integer musicFolderId = ServletRequestUtils.getRequiredIntParameter(request, "musicFolderId");
+ if (musicFolderId == -1) {
+ musicFolderId = null;
+ }
+
+ Player player = playerService.getPlayer(request, response);
+ PlayQueue playQueue = player.getPlayQueue();
+
+ RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolderId);
+ playQueue.addFiles(false, searchService.getRandomSongs(criteria));
+
+ if (request.getParameter("autoRandom") != null) {
+ playQueue.setRandomSearchCriteria(criteria);
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("reloadFrames", reloadFrames);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setReloadFrames(List<ReloadFrame> reloadFrames) {
+ this.reloadFrames = reloadFrames;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java
new file mode 100644
index 00000000..093b7fa1
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java
@@ -0,0 +1,52 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+/**
+ * Used in subsonic-servlet.xml to specify frame reloading.
+ *
+ * @author Sindre Mehus
+ */
+public class ReloadFrame {
+ private String frame;
+ private String view;
+
+ public ReloadFrame() {}
+
+ public ReloadFrame(String frame, String view) {
+ this.frame = frame;
+ this.view = view;
+ }
+
+ public String getFrame() {
+ return frame;
+ }
+
+ public void setFrame(String frame) {
+ this.frame = frame;
+ }
+
+ public String getView() {
+ return view;
+ }
+
+ public void setView(String view) {
+ this.view = view;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java
new file mode 100644
index 00000000..405c2dc7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java
@@ -0,0 +1,66 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.dao.UserDao;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.SecurityService;
+
+/**
+ * Controller for the right frame.
+ *
+ * @author Sindre Mehus
+ */
+public class RightController extends ParameterizableViewController {
+
+ private SettingsService settingsService;
+ private SecurityService securityService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+ ModelAndView result = super.handleRequestInternal(request, response);
+
+ UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request));
+ map.put("showNowPlaying", userSettings.isShowNowPlayingEnabled());
+ map.put("showChat", userSettings.isShowChatEnabled());
+ map.put("user", securityService.getCurrentUser(request));
+
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java
new file mode 100644
index 00000000..387ec7db
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java
@@ -0,0 +1,106 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.validation.BindException;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.SimpleFormController;
+
+import net.sourceforge.subsonic.command.SearchCommand;
+import net.sourceforge.subsonic.domain.SearchCriteria;
+import net.sourceforge.subsonic.domain.SearchResult;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.SearchService;
+
+/**
+ * Controller for the search page.
+ *
+ * @author Sindre Mehus
+ */
+public class SearchController extends SimpleFormController {
+
+ private static final int MATCH_COUNT = 25;
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private PlayerService playerService;
+ private SearchService searchService;
+
+ @Override
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ return new SearchCommand();
+ }
+
+ @Override
+ protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors)
+ throws Exception {
+ SearchCommand command = (SearchCommand) com;
+
+ User user = securityService.getCurrentUser(request);
+ UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
+ command.setUser(user);
+ command.setPartyModeEnabled(userSettings.isPartyModeEnabled());
+
+ String any = StringUtils.trimToNull(command.getQuery());
+
+ if (any != null) {
+
+ SearchCriteria criteria = new SearchCriteria();
+ criteria.setCount(MATCH_COUNT);
+ criteria.setQuery(any);
+
+ SearchResult artists = searchService.search(criteria, SearchService.IndexType.ARTIST);
+ command.setArtists(artists.getMediaFiles());
+
+ SearchResult albums = searchService.search(criteria, SearchService.IndexType.ALBUM);
+ command.setAlbums(albums.getMediaFiles());
+
+ SearchResult songs = searchService.search(criteria, SearchService.IndexType.SONG);
+ command.setSongs(songs.getMediaFiles());
+
+ command.setPlayer(playerService.getPlayer(request, response));
+ }
+
+ return new ModelAndView(getSuccessView(), errors.getModel());
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java
new file mode 100644
index 00000000..8b3ebca7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java
@@ -0,0 +1,58 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.util.*;
+import net.sourceforge.subsonic.filter.ParameterDecodingFilter;
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.view.*;
+import org.springframework.web.servlet.mvc.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Controller for updating music file metadata.
+ *
+ * @author Sindre Mehus
+ */
+public class SetMusicFileInfoController extends AbstractController {
+
+ private MediaFileService mediaFileService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String path = request.getParameter("path");
+ String action = request.getParameter("action");
+
+ MediaFile mediaFile = mediaFileService.getMediaFile(path);
+
+ if ("comment".equals(action)) {
+ mediaFile.setComment(StringUtil.toHtml(request.getParameter("comment")));
+ mediaFileService.updateMediaFile(mediaFile);
+ }
+
+ String url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + StringUtil.utf8HexEncode(path);
+ return new ModelAndView(new RedirectView(url));
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java
new file mode 100644
index 00000000..aaeaa4a4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java
@@ -0,0 +1,69 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.util.*;
+import net.sourceforge.subsonic.filter.ParameterDecodingFilter;
+import org.springframework.web.bind.*;
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.mvc.*;
+import org.springframework.web.servlet.view.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Controller for updating music file ratings.
+ *
+ * @author Sindre Mehus
+ */
+public class SetRatingController extends AbstractController {
+
+ private RatingService ratingService;
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String path = request.getParameter("path");
+ Integer rating = ServletRequestUtils.getIntParameter(request, "rating");
+ if (rating == 0) {
+ rating = null;
+ }
+
+ MediaFile mediaFile = mediaFileService.getMediaFile(path);
+ String username = securityService.getCurrentUsername(request);
+ ratingService.setRatingForUser(username, mediaFile, rating);
+
+ String url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + StringUtil.utf8HexEncode(path);
+ return new ModelAndView(new RedirectView(url));
+ }
+
+ public void setRatingService(RatingService ratingService) {
+ this.ratingService = ratingService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java
new file mode 100644
index 00000000..ed0c21c5
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java
@@ -0,0 +1,52 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.service.*;
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.view.*;
+import org.springframework.web.servlet.mvc.*;
+
+import javax.servlet.http.*;
+
+/**
+ * Controller for the main settings page.
+ *
+ * @author Sindre Mehus
+ */
+public class SettingsController extends AbstractController {
+
+ private SecurityService securityService;
+
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ User user = securityService.getCurrentUser(request);
+
+ // Redirect to music folder settings if admin.
+ String view = user.isAdminRole() ? "musicFolderSettings.view" : "personalSettings.view";
+
+ return new ModelAndView(new RedirectView(view));
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java
new file mode 100644
index 00000000..de2ea764
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java
@@ -0,0 +1,123 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.*;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.Share;
+
+/**
+ * Controller for sharing music on Twitter, Facebook etc.
+ *
+ * @author Sindre Mehus
+ */
+public class ShareManagementController extends MultiActionController {
+
+ private MediaFileService mediaFileService;
+ private SettingsService settingsService;
+ private ShareService shareService;
+ private PlayerService playerService;
+ private SecurityService securityService;
+
+ public ModelAndView createShare(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ List<MediaFile> files = getMediaFiles(request);
+ MediaFile dir = null;
+ if (!files.isEmpty()) {
+ dir = files.get(0);
+ if (!dir.isAlbum()) {
+ dir = mediaFileService.getParentOf(dir);
+ }
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("urlRedirectionEnabled", settingsService.isUrlRedirectionEnabled());
+ map.put("dir", dir);
+ map.put("user", securityService.getCurrentUser(request));
+ Share share = shareService.createShare(request, files);
+ map.put("playUrl", shareService.getShareUrl(share));
+
+ return new ModelAndView("createShare", "model", map);
+ }
+
+ private List<MediaFile> getMediaFiles(HttpServletRequest request) throws IOException {
+ String dir = request.getParameter("dir");
+ String playerId = request.getParameter("player");
+
+ List<MediaFile> result = new ArrayList<MediaFile>();
+
+ if (dir != null) {
+ MediaFile album = mediaFileService.getMediaFile(dir);
+ int[] indexes = ServletRequestUtils.getIntParameters(request, "i");
+ if (indexes.length == 0) {
+ return Arrays.asList(album);
+ }
+ List<MediaFile> children = mediaFileService.getChildrenOf(album, true, true, true);
+ for (int index : indexes) {
+ result.add(children.get(index));
+ }
+ } else if (playerId != null) {
+ Player player = playerService.getPlayerById(playerId);
+ PlayQueue playQueue = player.getPlayQueue();
+ List<MediaFile> result1;
+ synchronized (playQueue) {
+ result1 = playQueue.getFiles();
+ }
+ result = result1;
+ }
+
+ return result;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setShareService(ShareService shareService) {
+ this.shareService = shareService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java
new file mode 100644
index 00000000..2b8a958a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java
@@ -0,0 +1,161 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Share;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.ShareService;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for the page used to administrate the set of shared media.
+ *
+ * @author Sindre Mehus
+ */
+public class ShareSettingsController extends ParameterizableViewController {
+
+ private ShareService shareService;
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ if (isFormSubmission(request)) {
+ String error = handleParameters(request);
+ map.put("error", error);
+ }
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ map.put("shareBaseUrl", shareService.getShareBaseUrl());
+ map.put("shareInfos", getShareInfos(request));
+ map.put("user", securityService.getCurrentUser(request));
+
+ result.addObject("model", map);
+ return result;
+ }
+
+ /**
+ * Determine if the given request represents a form submission.
+ *
+ * @param request current HTTP request
+ * @return if the request represents a form submission
+ */
+ private boolean isFormSubmission(HttpServletRequest request) {
+ return "POST".equals(request.getMethod());
+ }
+
+ private String handleParameters(HttpServletRequest request) {
+ User user = securityService.getCurrentUser(request);
+ for (Share share : shareService.getSharesForUser(user)) {
+ int id = share.getId();
+
+ String description = getParameter(request, "description", id);
+ boolean delete = getParameter(request, "delete", id) != null;
+ String expireIn = getParameter(request, "expireIn", id);
+
+ if (delete) {
+ shareService.deleteShare(id);
+ } else {
+ if (expireIn != null) {
+ share.setExpires(parseExpireIn(expireIn));
+ }
+ share.setDescription(description);
+ shareService.updateShare(share);
+ }
+ }
+
+ return null;
+ }
+
+ private List<ShareInfo> getShareInfos(HttpServletRequest request) {
+ List<ShareInfo> result = new ArrayList<ShareInfo>();
+ User user = securityService.getCurrentUser(request);
+ for (Share share : shareService.getSharesForUser(user)) {
+ List<MediaFile> files = shareService.getSharedFiles(share.getId());
+ if (!files.isEmpty()) {
+ MediaFile file = files.get(0);
+ result.add(new ShareInfo(share, file.isDirectory() ? file : mediaFileService.getParentOf(file)));
+ }
+ }
+ return result;
+ }
+
+
+ private String getParameter(HttpServletRequest request, String name, int id) {
+ return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]"));
+ }
+
+ private Date parseExpireIn(String expireIn) {
+ int days = Integer.parseInt(expireIn);
+ if (days == 0) {
+ return null;
+ }
+
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.DAY_OF_YEAR, days);
+ return calendar.getTime();
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setShareService(ShareService shareService) {
+ this.shareService = shareService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public static class ShareInfo {
+ private final Share share;
+ private final MediaFile dir;
+
+ public ShareInfo(Share share, MediaFile dir) {
+ this.share = share;
+ this.dir = dir;
+ }
+
+ public Share getShare() {
+ return share;
+ }
+
+ public MediaFile getDir() {
+ return dir;
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java
new file mode 100644
index 00000000..2da8b5ad
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java
@@ -0,0 +1,96 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for showing a user's starred items.
+ *
+ * @author Sindre Mehus
+ */
+public class StarredController extends ParameterizableViewController {
+
+ private PlayerService playerService;
+ private MediaFileDao mediaFileDao;
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private MediaFileService mediaFileService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ User user = securityService.getCurrentUser(request);
+ String username = user.getUsername();
+ UserSettings userSettings = settingsService.getUserSettings(username);
+
+ List<MediaFile> artists = mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username);
+ List<MediaFile> albums = mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username);
+ List<MediaFile> songs = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username);
+ mediaFileService.populateStarredDate(artists, username);
+ mediaFileService.populateStarredDate(albums, username);
+ mediaFileService.populateStarredDate(songs, username);
+
+ map.put("user", user);
+ map.put("partyModeEnabled", userSettings.isPartyModeEnabled());
+ map.put("player", playerService.getPlayer(request, response));
+ map.put("artists", artists);
+ map.put("albums", albums);
+ map.put("songs", songs);
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java
new file mode 100644
index 00000000..878b8ae8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java
@@ -0,0 +1,149 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.service.*;
+import org.jfree.chart.*;
+import org.jfree.chart.axis.*;
+import org.jfree.chart.plot.*;
+import org.jfree.chart.renderer.xy.*;
+import org.jfree.data.*;
+import org.jfree.data.time.*;
+import org.springframework.web.servlet.*;
+
+import javax.servlet.http.*;
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+
+/**
+ * Controller for generating a chart showing bitrate vs time.
+ *
+ * @author Sindre Mehus
+ */
+public class StatusChartController extends AbstractChartController {
+
+ private StatusService statusService;
+
+ public static final int IMAGE_WIDTH = 350;
+ public static final int IMAGE_HEIGHT = 150;
+
+ public synchronized ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String type = request.getParameter("type");
+ int index = Integer.parseInt(request.getParameter("index"));
+
+ List<TransferStatus> statuses = Collections.emptyList();
+ if ("stream".equals(type)) {
+ statuses = statusService.getAllStreamStatuses();
+ } else if ("download".equals(type)) {
+ statuses = statusService.getAllDownloadStatuses();
+ } else if ("upload".equals(type)) {
+ statuses = statusService.getAllUploadStatuses();
+ }
+
+ if (index < 0 || index >= statuses.size()) {
+ return null;
+ }
+ TransferStatus status = statuses.get(index);
+
+ TimeSeries series = new TimeSeries("Kbps", Millisecond.class);
+ TransferStatus.SampleHistory history = status.getHistory();
+ long to = System.currentTimeMillis();
+ long from = to - status.getHistoryLengthMillis();
+ Range range = new DateRange(from, to);
+
+ if (!history.isEmpty()) {
+
+ TransferStatus.Sample previous = history.get(0);
+
+ for (int i = 1; i < history.size(); i++) {
+ TransferStatus.Sample sample = history.get(i);
+
+ long elapsedTimeMilis = sample.getTimestamp() - previous.getTimestamp();
+ long bytesStreamed = Math.max(0L, sample.getBytesTransfered() - previous.getBytesTransfered());
+
+ double kbps = (8.0 * bytesStreamed / 1024.0) / (elapsedTimeMilis / 1000.0);
+ series.addOrUpdate(new Millisecond(new Date(sample.getTimestamp())), kbps);
+
+ previous = sample;
+ }
+ }
+
+ // Compute moving average.
+ series = MovingAverage.createMovingAverage(series, "Kbps", 20000, 5000);
+
+ // Find min and max values.
+ double min = 100;
+ double max = 250;
+ for (Object obj : series.getItems()) {
+ TimeSeriesDataItem item = (TimeSeriesDataItem) obj;
+ double value = item.getValue().doubleValue();
+ if (item.getPeriod().getFirstMillisecond() > from) {
+ min = Math.min(min, value);
+ max = Math.max(max, value);
+ }
+ }
+
+ // Add 10% to max value.
+ max *= 1.1D;
+
+ // Subtract 10% from min value.
+ min *= 0.9D;
+
+ TimeSeriesCollection dataset = new TimeSeriesCollection();
+ dataset.addSeries(series);
+ JFreeChart chart = ChartFactory.createTimeSeriesChart(null, null, null, dataset, false, false, false);
+ XYPlot plot = (XYPlot) chart.getPlot();
+
+ plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);
+ Paint background = new GradientPaint(0, 0, Color.lightGray, 0, IMAGE_HEIGHT, Color.white);
+ plot.setBackgroundPaint(background);
+
+ XYItemRenderer renderer = plot.getRendererForDataset(dataset);
+ renderer.setSeriesPaint(0, Color.blue.darker());
+ renderer.setSeriesStroke(0, new BasicStroke(2f));
+
+ // Set theme-specific colors.
+ Color bgColor = getBackground(request);
+ Color fgColor = getForeground(request);
+
+ chart.setBackgroundPaint(bgColor);
+
+ ValueAxis domainAxis = plot.getDomainAxis();
+ domainAxis.setRange(range);
+ domainAxis.setTickLabelPaint(fgColor);
+ domainAxis.setTickMarkPaint(fgColor);
+ domainAxis.setAxisLinePaint(fgColor);
+
+ ValueAxis rangeAxis = plot.getRangeAxis();
+ rangeAxis.setRange(new Range(min, max));
+ rangeAxis.setTickLabelPaint(fgColor);
+ rangeAxis.setTickMarkPaint(fgColor);
+ rangeAxis.setAxisLinePaint(fgColor);
+
+ ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, IMAGE_WIDTH, IMAGE_HEIGHT);
+
+ return null;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java
new file mode 100644
index 00000000..964e7810
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java
@@ -0,0 +1,141 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.service.StatusService;
+import net.sourceforge.subsonic.util.FileUtil;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+import org.springframework.web.servlet.support.RequestContextUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Controller for the status page.
+ *
+ * @author Sindre Mehus
+ */
+public class StatusController extends ParameterizableViewController {
+
+ private StatusService statusService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ List<TransferStatus> streamStatuses = statusService.getAllStreamStatuses();
+ List<TransferStatus> downloadStatuses = statusService.getAllDownloadStatuses();
+ List<TransferStatus> uploadStatuses = statusService.getAllUploadStatuses();
+
+ Locale locale = RequestContextUtils.getLocale(request);
+ List<TransferStatusHolder> transferStatuses = new ArrayList<TransferStatusHolder>();
+
+ for (int i = 0; i < streamStatuses.size(); i++) {
+ long minutesAgo = streamStatuses.get(i).getMillisSinceLastUpdate() / 1000L / 60L;
+ if (minutesAgo < 60L) {
+ transferStatuses.add(new TransferStatusHolder(streamStatuses.get(i), true, false, false, i, locale));
+ }
+ }
+ for (int i = 0; i < downloadStatuses.size(); i++) {
+ transferStatuses.add(new TransferStatusHolder(downloadStatuses.get(i), false, true, false, i, locale));
+ }
+ for (int i = 0; i < uploadStatuses.size(); i++) {
+ transferStatuses.add(new TransferStatusHolder(uploadStatuses.get(i), false, false, true, i, locale));
+ }
+
+ map.put("transferStatuses", transferStatuses);
+ map.put("chartWidth", StatusChartController.IMAGE_WIDTH);
+ map.put("chartHeight", StatusChartController.IMAGE_HEIGHT);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public static class TransferStatusHolder {
+ private TransferStatus transferStatus;
+ private boolean isStream;
+ private boolean isDownload;
+ private boolean isUpload;
+ private int index;
+ private Locale locale;
+
+ public TransferStatusHolder(TransferStatus transferStatus, boolean isStream, boolean isDownload, boolean isUpload,
+ int index, Locale locale) {
+ this.transferStatus = transferStatus;
+ this.isStream = isStream;
+ this.isDownload = isDownload;
+ this.isUpload = isUpload;
+ this.index = index;
+ this.locale = locale;
+ }
+
+ public boolean isStream() {
+ return isStream;
+ }
+
+ public boolean isDownload() {
+ return isDownload;
+ }
+
+ public boolean isUpload() {
+ return isUpload;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public Player getPlayer() {
+ return transferStatus.getPlayer();
+ }
+
+ public String getPlayerType() {
+ Player player = transferStatus.getPlayer();
+ return player == null ? null : player.getType();
+ }
+
+ public String getUsername() {
+ Player player = transferStatus.getPlayer();
+ return player == null ? null : player.getUsername();
+ }
+
+ public String getPath() {
+ return FileUtil.getShortPath(transferStatus.getFile());
+ }
+
+ public String getBytes() {
+ return StringUtil.formatBytes(transferStatus.getBytesTransfered(), locale);
+ }
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java
new file mode 100644
index 00000000..a40f5da4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java
@@ -0,0 +1,419 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.awt.Dimension;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SearchService;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.math.LongRange;
+import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.Controller;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
+import net.sourceforge.subsonic.io.PlayQueueInputStream;
+import net.sourceforge.subsonic.io.RangeOutputStream;
+import net.sourceforge.subsonic.io.ShoutCastOutputStream;
+import net.sourceforge.subsonic.service.AudioScrobblerService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.StatusService;
+import net.sourceforge.subsonic.service.TranscodingService;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.Util;
+
+/**
+ * A controller which streams the content of a {@link net.sourceforge.subsonic.domain.PlayQueue} to a remote
+ * {@link Player}.
+ *
+ * @author Sindre Mehus
+ */
+public class StreamController implements Controller {
+
+ private static final Logger LOG = Logger.getLogger(StreamController.class);
+
+ private StatusService statusService;
+ private PlayerService playerService;
+ private PlaylistService playlistService;
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private TranscodingService transcodingService;
+ private AudioScrobblerService audioScrobblerService;
+ private MediaFileService mediaFileService;
+ private SearchService searchService;
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ TransferStatus status = null;
+ PlayQueueInputStream in = null;
+ Player player = playerService.getPlayer(request, response, false, true);
+ User user = securityService.getUserByName(player.getUsername());
+
+ try {
+
+ if (!user.isStreamRole()) {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername());
+ return null;
+ }
+
+ // If "playlist" request parameter is set, this is a Podcast request. In that case, create a separate
+ // play queue (in order to support multiple parallel Podcast streams).
+ Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist");
+ boolean isPodcast = playlistId != null;
+ if (isPodcast) {
+ PlayQueue playQueue = new PlayQueue();
+ playQueue.addFiles(false, playlistService.getFilesInPlaylist(playlistId));
+ player.setPlayQueue(playQueue);
+ Util.setContentLength(response, playQueue.length());
+ LOG.info("Incoming Podcast request for playlist " + playlistId);
+ }
+
+ String contentType = StringUtil.getMimeType(request.getParameter("suffix"));
+ response.setContentType(contentType);
+
+ String preferredTargetFormat = request.getParameter("format");
+ Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate");
+ if (Integer.valueOf(0).equals(maxBitRate)) {
+ maxBitRate = null;
+ }
+
+ VideoTranscodingSettings videoTranscodingSettings = null;
+
+ // Is this a request for a single file (typically from the embedded Flash player)?
+ // In that case, create a separate playlist (in order to support multiple parallel streams).
+ // Also, enable partial download (HTTP byte range).
+ MediaFile file = getSingleFile(request);
+ boolean isSingleFile = file != null;
+ LongRange range = null;
+
+ if (isSingleFile) {
+ PlayQueue playQueue = new PlayQueue();
+ playQueue.addFiles(true, file);
+ player.setPlayQueue(playQueue);
+
+ if (!file.isVideo()) {
+ response.setIntHeader("ETag", file.getId());
+ response.setHeader("Accept-Ranges", "bytes");
+ }
+
+ TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, videoTranscodingSettings);
+ long fileLength = getFileLength(parameters);
+ boolean isConversion = parameters.isDownsample() || parameters.isTranscode();
+ boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request, "estimateContentLength", false);
+
+ range = getRange(request, file);
+ if (range != null) {
+ LOG.info("Got range: " + range);
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ Util.setContentLength(response, fileLength - range.getMinimumLong());
+ long firstBytePos = range.getMinimumLong();
+ long lastBytePos = fileLength - 1;
+ response.setHeader("Content-Range", "bytes " + firstBytePos + "-" + lastBytePos + "/" + fileLength);
+ } else if (!isConversion || estimateContentLength) {
+ Util.setContentLength(response, fileLength);
+ }
+
+ String transcodedSuffix = transcodingService.getSuffix(player, file, preferredTargetFormat);
+ response.setContentType(StringUtil.getMimeType(transcodedSuffix));
+
+ if (file.isVideo()) {
+ videoTranscodingSettings = createVideoTranscodingSettings(file, request);
+ }
+ }
+
+ if (request.getMethod().equals("HEAD")) {
+ return null;
+ }
+
+ // Terminate any other streams to this player.
+ if (!isPodcast && !isSingleFile) {
+ for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) {
+ if (streamStatus.isActive()) {
+ streamStatus.terminate();
+ }
+ }
+ }
+
+ status = statusService.createStreamStatus(player);
+
+ in = new PlayQueueInputStream(player, status, maxBitRate, preferredTargetFormat, videoTranscodingSettings, transcodingService,
+ audioScrobblerService, mediaFileService, searchService);
+ OutputStream out = RangeOutputStream.wrap(response.getOutputStream(), range);
+
+ // Enabled SHOUTcast, if requested.
+ boolean isShoutCastRequested = "1".equals(request.getHeader("icy-metadata"));
+ if (isShoutCastRequested && !isSingleFile) {
+ response.setHeader("icy-metaint", "" + ShoutCastOutputStream.META_DATA_INTERVAL);
+ response.setHeader("icy-notice1", "This stream is served using Subsonic");
+ response.setHeader("icy-notice2", "Subsonic - Free media streamer - subsonic.org");
+ response.setHeader("icy-name", "Subsonic");
+ response.setHeader("icy-genre", "Mixed");
+ response.setHeader("icy-url", "http://subsonic.org/");
+ out = new ShoutCastOutputStream(out, player.getPlayQueue(), settingsService);
+ }
+
+ final int BUFFER_SIZE = 2048;
+ byte[] buf = new byte[BUFFER_SIZE];
+
+ while (true) {
+
+ // Check if stream has been terminated.
+ if (status.terminated()) {
+ return null;
+ }
+
+ if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
+ if (isPodcast || isSingleFile) {
+ break;
+ } else {
+ sendDummy(buf, out);
+ }
+ } else {
+
+ int n = in.read(buf);
+ if (n == -1) {
+ if (isPodcast || isSingleFile) {
+ break;
+ } else {
+ sendDummy(buf, out);
+ }
+ } else {
+ out.write(buf, 0, n);
+ }
+ }
+ }
+
+ } finally {
+ if (status != null) {
+ securityService.updateUserByteCounts(user, status.getBytesTransfered(), 0L, 0L);
+ statusService.removeStreamStatus(status);
+ }
+ IOUtils.closeQuietly(in);
+ }
+ return null;
+ }
+
+ private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException {
+ String path = request.getParameter("path");
+ if (path != null) {
+ return mediaFileService.getMediaFile(path);
+ }
+ Integer id = ServletRequestUtils.getIntParameter(request, "id");
+ if (id != null) {
+ return mediaFileService.getMediaFile(id);
+ }
+ return null;
+ }
+
+ private long getFileLength(TranscodingService.Parameters parameters) {
+ MediaFile file = parameters.getMediaFile();
+
+ if (!parameters.isDownsample() && !parameters.isTranscode()) {
+ return file.getFileSize();
+ }
+ Integer duration = file.getDurationSeconds();
+ Integer maxBitRate = parameters.getMaxBitRate();
+
+ if (duration == null) {
+ LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size.");
+ return file.getFileSize();
+ }
+
+ if (maxBitRate == null) {
+ LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size.");
+ return file.getFileSize();
+ }
+
+ return duration * maxBitRate * 1000L / 8L;
+ }
+
+ private LongRange getRange(HttpServletRequest request, MediaFile file) {
+
+ // First, look for "Range" HTTP header.
+ LongRange range = StringUtil.parseRange(request.getHeader("Range"));
+ if (range != null) {
+ return range;
+ }
+
+ // Second, look for "offsetSeconds" request parameter.
+ String offsetSeconds = request.getParameter("offsetSeconds");
+ range = parseAndConvertOffsetSeconds(offsetSeconds, file);
+ if (range != null) {
+ return range;
+ }
+
+ return null;
+ }
+
+ private LongRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) {
+ if (offsetSeconds == null) {
+ return null;
+ }
+
+ try {
+ Integer duration = file.getDurationSeconds();
+ Long fileSize = file.getFileSize();
+ if (duration == null || fileSize == null) {
+ return null;
+ }
+ float offset = Float.parseFloat(offsetSeconds);
+
+ // Convert from time offset to byte offset.
+ long byteOffset = (long) (fileSize * (offset / duration));
+ return new LongRange(byteOffset, Long.MAX_VALUE);
+
+ } catch (Exception x) {
+ LOG.error("Failed to parse and convert time offset: " + offsetSeconds, x);
+ return null;
+ }
+ }
+
+ private VideoTranscodingSettings createVideoTranscodingSettings(MediaFile file, HttpServletRequest request) throws ServletRequestBindingException {
+ Integer existingWidth = file.getWidth();
+ Integer existingHeight = file.getHeight();
+ Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate");
+ int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0);
+
+ Dimension dim = getRequestedVideoSize(request.getParameter("size"));
+ if (dim == null) {
+ dim = getSuitableVideoSize(existingWidth, existingHeight, maxBitRate);
+ }
+
+ return new VideoTranscodingSettings(dim.width, dim.height, timeOffset);
+ }
+
+ protected Dimension getRequestedVideoSize(String sizeSpec) {
+ if (sizeSpec == null) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile("^(\\d+)x(\\d+)$");
+ Matcher matcher = pattern.matcher(sizeSpec);
+ if (matcher.find()) {
+ int w = Integer.parseInt(matcher.group(1));
+ int h = Integer.parseInt(matcher.group(2));
+ if (w >= 0 && h >= 0 && w <= 2000 && h <= 2000) {
+ return new Dimension(w, h);
+ }
+ }
+ return null;
+ }
+
+ protected Dimension getSuitableVideoSize(Integer existingWidth, Integer existingHeight, Integer maxBitRate) {
+ if (maxBitRate == null) {
+ return new Dimension(320, 240);
+ }
+
+ int w, h;
+ if (maxBitRate <= 600) {
+ w = 320; h = 240;
+ } else if (maxBitRate <= 1000) {
+ w = 480; h = 360;
+ } else {
+ w = 640; h = 480;
+ }
+
+ if (existingWidth == null || existingHeight == null) {
+ return new Dimension(w, h);
+ }
+
+ if (existingWidth < w || existingHeight < h) {
+ return new Dimension(even(existingWidth), even(existingHeight));
+ }
+
+ double aspectRate = existingWidth.doubleValue() / existingHeight.doubleValue();
+ w = (int) Math.round(h * aspectRate);
+
+ return new Dimension(even(w), even(h));
+ }
+
+ // Make sure width and height are multiples of two, as some versions of ffmpeg require it.
+ private int even(int size) {
+ return size + (size % 2);
+ }
+
+ /**
+ * Feed the other end with some dummy data to keep it from reconnecting.
+ */
+ private void sendDummy(byte[] buf, OutputStream out) throws IOException {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException x) {
+ LOG.warn("Interrupted in sleep.", x);
+ }
+ Arrays.fill(buf, (byte) 0xFF);
+ out.write(buf);
+ out.flush();
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+
+ public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
+ this.audioScrobblerService = audioScrobblerService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java
new file mode 100644
index 00000000..800aef0e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java
@@ -0,0 +1,84 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.service.VersionService;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for the top frame.
+ *
+ * @author Sindre Mehus
+ */
+public class TopController extends ParameterizableViewController {
+
+ private SettingsService settingsService;
+ private VersionService versionService;
+ private SecurityService securityService;
+
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders();
+ User user = securityService.getCurrentUser(request);
+
+ map.put("user", user);
+ map.put("musicFoldersExist", !allMusicFolders.isEmpty());
+ map.put("brand", settingsService.getBrand());
+ map.put("licensed", settingsService.isLicenseValid());
+
+ UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
+ if (userSettings.isFinalVersionNotificationEnabled() && versionService.isNewFinalVersionAvailable()) {
+ map.put("newVersionAvailable", true);
+ map.put("latestVersion", versionService.getLatestFinalVersion());
+
+ } else if (userSettings.isBetaVersionNotificationEnabled() && versionService.isNewBetaVersionAvailable()) {
+ map.put("newVersionAvailable", true);
+ map.put("latestVersion", versionService.getLatestBetaVersion());
+ }
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setVersionService(VersionService versionService) {
+ this.versionService = versionService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java
new file mode 100644
index 00000000..8bd87408
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java
@@ -0,0 +1,139 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.Transcoding;
+import net.sourceforge.subsonic.service.TranscodingService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Controller for the page used to administrate the set of transcoding configurations.
+ *
+ * @author Sindre Mehus
+ */
+public class TranscodingSettingsController extends ParameterizableViewController {
+
+ private TranscodingService transcodingService;
+ private SettingsService settingsService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ if (isFormSubmission(request)) {
+ handleParameters(request, map);
+ }
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ map.put("transcodings", transcodingService.getAllTranscodings());
+ map.put("transcodeDirectory", transcodingService.getTranscodeDirectory());
+ map.put("brand", settingsService.getBrand());
+
+ result.addObject("model", map);
+ return result;
+ }
+
+ /**
+ * Determine if the given request represents a form submission.
+ *
+ * @param request current HTTP request
+ * @return if the request represents a form submission
+ */
+ private boolean isFormSubmission(HttpServletRequest request) {
+ return "POST".equals(request.getMethod());
+ }
+
+ private void handleParameters(HttpServletRequest request, Map<String, Object> map) {
+
+ for (Transcoding transcoding : transcodingService.getAllTranscodings()) {
+ Integer id = transcoding.getId();
+ String name = getParameter(request, "name", id);
+ String sourceFormats = getParameter(request, "sourceFormats", id);
+ String targetFormat = getParameter(request, "targetFormat", id);
+ String step1 = getParameter(request, "step1", id);
+ String step2 = getParameter(request, "step2", id);
+ boolean delete = getParameter(request, "delete", id) != null;
+
+ if (delete) {
+ transcodingService.deleteTranscoding(id);
+ } else if (name == null) {
+ map.put("error", "transcodingsettings.noname");
+ } else if (sourceFormats == null) {
+ map.put("error", "transcodingsettings.nosourceformat");
+ } else if (targetFormat == null) {
+ map.put("error", "transcodingsettings.notargetformat");
+ } else if (step1 == null) {
+ map.put("error", "transcodingsettings.nostep1");
+ } else {
+ transcoding.setName(name);
+ transcoding.setSourceFormats(sourceFormats);
+ transcoding.setTargetFormat(targetFormat);
+ transcoding.setStep1(step1);
+ transcoding.setStep2(step2);
+ transcodingService.updateTranscoding(transcoding);
+ }
+ }
+
+ String name = StringUtils.trimToNull(request.getParameter("name"));
+ String sourceFormats = StringUtils.trimToNull(request.getParameter("sourceFormats"));
+ String targetFormat = StringUtils.trimToNull(request.getParameter("targetFormat"));
+ String step1 = StringUtils.trimToNull(request.getParameter("step1"));
+ String step2 = StringUtils.trimToNull(request.getParameter("step2"));
+ boolean defaultActive = request.getParameter("defaultActive") != null;
+
+ if (name != null || sourceFormats != null || targetFormat != null || step1 != null || step2 != null) {
+ Transcoding transcoding = new Transcoding(null, name, sourceFormats, targetFormat, step1, step2, null, defaultActive);
+ if (name == null) {
+ map.put("error", "transcodingsettings.noname");
+ } else if (sourceFormats == null) {
+ map.put("error", "transcodingsettings.nosourceformat");
+ } else if (targetFormat == null) {
+ map.put("error", "transcodingsettings.notargetformat");
+ } else if (step1 == null) {
+ map.put("error", "transcodingsettings.nostep1");
+ } else {
+ transcodingService.createTranscoding(transcoding);
+ }
+ if (map.containsKey("error")) {
+ map.put("newTranscoding", transcoding);
+ }
+ }
+ }
+
+ private String getParameter(HttpServletRequest request, String name, Integer id) {
+ return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]"));
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java
new file mode 100644
index 00000000..de7bf8dd
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java
@@ -0,0 +1,260 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.*;
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.upload.*;
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.util.*;
+import org.apache.commons.fileupload.*;
+import org.apache.commons.fileupload.servlet.*;
+import org.apache.commons.io.*;
+import org.apache.tools.zip.*;
+import org.springframework.web.servlet.*;
+import org.springframework.web.servlet.mvc.*;
+
+import javax.servlet.http.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * Controller which receives uploaded files.
+ *
+ * @author Sindre Mehus
+ */
+public class UploadController extends ParameterizableViewController {
+
+ private static final Logger LOG = Logger.getLogger(UploadController.class);
+
+ private SecurityService securityService;
+ private PlayerService playerService;
+ private StatusService statusService;
+ private SettingsService settingsService;
+ public static final String UPLOAD_STATUS = "uploadStatus";
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ List<File> uploadedFiles = new ArrayList<File>();
+ List<File> unzippedFiles = new ArrayList<File>();
+ TransferStatus status = null;
+
+ try {
+
+ status = statusService.createUploadStatus(playerService.getPlayer(request, response, false, false));
+ status.setBytesTotal(request.getContentLength());
+
+ request.getSession().setAttribute(UPLOAD_STATUS, status);
+
+ // Check that we have a file upload request
+ if (!ServletFileUpload.isMultipartContent(request)) {
+ throw new Exception("Illegal request.");
+ }
+
+ File dir = null;
+ boolean unzip = false;
+
+ UploadListener listener = new UploadListenerImpl(status);
+
+ FileItemFactory factory = new MonitoredDiskFileItemFactory(listener);
+ ServletFileUpload upload = new ServletFileUpload(factory);
+
+ List<?> items = upload.parseRequest(request);
+
+ // First, look for "dir" and "unzip" parameters.
+ for (Object o : items) {
+ FileItem item = (FileItem) o;
+
+ if (item.isFormField() && "dir".equals(item.getFieldName())) {
+ dir = new File(item.getString());
+ } else if (item.isFormField() && "unzip".equals(item.getFieldName())) {
+ unzip = true;
+ }
+ }
+
+ if (dir == null) {
+ throw new Exception("Missing 'dir' parameter.");
+ }
+
+ // Look for file items.
+ for (Object o : items) {
+ FileItem item = (FileItem) o;
+
+ if (!item.isFormField()) {
+ String fileName = item.getName();
+ if (fileName.trim().length() > 0) {
+
+ File targetFile = new File(dir, new File(fileName).getName());
+
+ if (!securityService.isUploadAllowed(targetFile)) {
+ throw new Exception("Permission denied: " + StringUtil.toHtml(targetFile.getPath()));
+ }
+
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ item.write(targetFile);
+ uploadedFiles.add(targetFile);
+ LOG.info("Uploaded " + targetFile);
+
+ if (unzip && targetFile.getName().toLowerCase().endsWith(".zip")) {
+ unzip(targetFile, unzippedFiles);
+ }
+ }
+ }
+ }
+
+ } catch (Exception x) {
+ LOG.warn("Uploading failed.", x);
+ map.put("exception", x);
+ } finally {
+ if (status != null) {
+ statusService.removeUploadStatus(status);
+ request.getSession().removeAttribute(UPLOAD_STATUS);
+ User user = securityService.getCurrentUser(request);
+ securityService.updateUserByteCounts(user, 0L, 0L, status.getBytesTransfered());
+ }
+ }
+
+ map.put("uploadedFiles", uploadedFiles);
+ map.put("unzippedFiles", unzippedFiles);
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ private void unzip(File file, List<File> unzippedFiles) throws Exception {
+ LOG.info("Unzipping " + file);
+
+ ZipFile zipFile = new ZipFile(file);
+
+ try {
+
+ Enumeration<?> entries = zipFile.getEntries();
+
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = (ZipEntry) entries.nextElement();
+ File entryFile = new File(file.getParentFile(), entry.getName());
+
+ if (!entry.isDirectory()) {
+
+ if (!securityService.isUploadAllowed(entryFile)) {
+ throw new Exception("Permission denied: " + StringUtil.toHtml(entryFile.getPath()));
+ }
+
+ entryFile.getParentFile().mkdirs();
+ InputStream inputStream = null;
+ OutputStream outputStream = null;
+ try {
+ inputStream = zipFile.getInputStream(entry);
+ outputStream = new FileOutputStream(entryFile);
+
+ byte[] buf = new byte[8192];
+ while (true) {
+ int n = inputStream.read(buf);
+ if (n == -1) {
+ break;
+ }
+ outputStream.write(buf, 0, n);
+ }
+
+ LOG.info("Unzipped " + entryFile);
+ unzippedFiles.add(entryFile);
+ } finally {
+ IOUtils.closeQuietly(inputStream);
+ IOUtils.closeQuietly(outputStream);
+ }
+ }
+ }
+
+ zipFile.close();
+ file.delete();
+
+ } finally {
+ zipFile.close();
+ }
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ /**
+ * Receives callbacks as the file upload progresses.
+ */
+ private class UploadListenerImpl implements UploadListener {
+ private TransferStatus status;
+ private long start;
+
+ private UploadListenerImpl(TransferStatus status) {
+ this.status = status;
+ start = System.currentTimeMillis();
+ }
+
+ public void start(String fileName) {
+ status.setFile(new File(fileName));
+ }
+
+ public void bytesRead(long bytesRead) {
+
+ // Throttle bitrate.
+
+ long byteCount = status.getBytesTransfered() + bytesRead;
+ long bitCount = byteCount * 8L;
+
+ float elapsedMillis = Math.max(1, System.currentTimeMillis() - start);
+ float elapsedSeconds = elapsedMillis / 1000.0F;
+ long maxBitsPerSecond = getBitrateLimit();
+
+ status.setBytesTransfered(byteCount);
+
+ if (maxBitsPerSecond > 0) {
+ float sleepMillis = 1000.0F * (bitCount / maxBitsPerSecond - elapsedSeconds);
+ if (sleepMillis > 0) {
+ try {
+ Thread.sleep((long) sleepMillis);
+ } catch (InterruptedException x) {
+ LOG.warn("Failed to sleep.", x);
+ }
+ }
+ }
+ }
+
+ private long getBitrateLimit() {
+ return 1024L * settingsService.getUploadBitrateLimit() / Math.max(1, statusService.getAllUploadStatuses().size());
+ }
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java
new file mode 100644
index 00000000..0428eff8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java
@@ -0,0 +1,145 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.awt.Color;
+import java.awt.GradientPaint;
+import java.awt.Paint;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartUtilities;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.CategoryLabelPositions;
+import org.jfree.chart.axis.LogarithmicAxis;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.BarRenderer;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.springframework.web.servlet.ModelAndView;
+
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.SecurityService;
+
+/**
+ * Controller for generating a chart showing bitrate vs time.
+ *
+ * @author Sindre Mehus
+ */
+public class UserChartController extends AbstractChartController {
+
+ private SecurityService securityService;
+
+ public static final int IMAGE_WIDTH = 400;
+ public static final int IMAGE_MIN_HEIGHT = 200;
+ private static final long BYTES_PER_MB = 1024L * 1024L;
+
+ public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String type = request.getParameter("type");
+ CategoryDataset dataset = createDataset(type);
+ JFreeChart chart = createChart(dataset, request);
+
+ int imageHeight = Math.max(IMAGE_MIN_HEIGHT, 15 * dataset.getColumnCount());
+
+ ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, IMAGE_WIDTH, imageHeight);
+ return null;
+ }
+
+ private CategoryDataset createDataset(String type) {
+ DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+ List<User> users = securityService.getAllUsers();
+ for (User user : users) {
+ double value;
+ if ("stream".equals(type)) {
+ value = user.getBytesStreamed();
+ } else if ("download".equals(type)) {
+ value = user.getBytesDownloaded();
+ } else if ("upload".equals(type)) {
+ value = user.getBytesUploaded();
+ } else if ("total".equals(type)) {
+ value = user.getBytesStreamed() + user.getBytesDownloaded() + user.getBytesUploaded();
+ } else {
+ throw new RuntimeException("Illegal chart type: " + type);
+ }
+
+ value /= BYTES_PER_MB;
+ dataset.addValue(value, "Series", user.getUsername());
+ }
+
+ return dataset;
+ }
+
+ private JFreeChart createChart(CategoryDataset dataset, HttpServletRequest request) {
+ JFreeChart chart = ChartFactory.createBarChart(null, null, null, dataset, PlotOrientation.HORIZONTAL, false, false, false);
+
+ CategoryPlot plot = chart.getCategoryPlot();
+ Paint background = new GradientPaint(0, 0, Color.lightGray, 0, IMAGE_MIN_HEIGHT, Color.white);
+ plot.setBackgroundPaint(background);
+ plot.setDomainGridlinePaint(Color.white);
+ plot.setDomainGridlinesVisible(true);
+ plot.setRangeGridlinePaint(Color.white);
+ plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
+
+ LogarithmicAxis rangeAxis = new LogarithmicAxis(null);
+ rangeAxis.setStrictValuesFlag(false);
+ rangeAxis.setAllowNegativesFlag(true);
+ plot.setRangeAxis(rangeAxis);
+
+ // Disable bar outlines.
+ BarRenderer renderer = (BarRenderer) plot.getRenderer();
+ renderer.setDrawBarOutline(false);
+
+ // Set up gradient paint for series.
+ GradientPaint gp0 = new GradientPaint(
+ 0.0f, 0.0f, Color.blue,
+ 0.0f, 0.0f, new Color(0, 0, 64)
+ );
+ renderer.setSeriesPaint(0, gp0);
+
+ // Rotate labels.
+ CategoryAxis domainAxis = plot.getDomainAxis();
+ domainAxis.setCategoryLabelPositions(CategoryLabelPositions.createUpRotationLabelPositions(Math.PI / 6.0));
+
+ // Set theme-specific colors.
+ Color bgColor = getBackground(request);
+ Color fgColor = getForeground(request);
+
+ chart.setBackgroundPaint(bgColor);
+
+ domainAxis.setTickLabelPaint(fgColor);
+ domainAxis.setTickMarkPaint(fgColor);
+ domainAxis.setAxisLinePaint(fgColor);
+
+ rangeAxis.setTickLabelPaint(fgColor);
+ rangeAxis.setTickMarkPaint(fgColor);
+ rangeAxis.setAxisLinePaint(fgColor);
+
+ return chart;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java
new file mode 100644
index 00000000..58848840
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java
@@ -0,0 +1,159 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.util.List;
+import java.util.Date;
+
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.command.*;
+import org.springframework.web.servlet.mvc.*;
+import org.springframework.web.bind.*;
+import org.apache.commons.lang.StringUtils;
+
+import javax.servlet.http.*;
+
+/**
+ * Controller for the page used to administrate users.
+ *
+ * @author Sindre Mehus
+ */
+public class UserSettingsController extends SimpleFormController {
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private TranscodingService transcodingService;
+
+ @Override
+ protected Object formBackingObject(HttpServletRequest request) throws Exception {
+ UserSettingsCommand command = new UserSettingsCommand();
+
+ User user = getUser(request);
+ if (user != null) {
+ command.setUser(user);
+ command.setEmail(user.getEmail());
+ command.setAdmin(User.USERNAME_ADMIN.equals(user.getUsername()));
+ UserSettings userSettings = settingsService.getUserSettings(user.getUsername());
+ command.setTranscodeSchemeName(userSettings.getTranscodeScheme().name());
+
+ } else {
+ command.setNew(true);
+ command.setStreamRole(true);
+ command.setSettingsRole(true);
+ }
+
+ command.setUsers(securityService.getAllUsers());
+ command.setTranscodingSupported(transcodingService.isDownsamplingSupported(null));
+ command.setTranscodeDirectory(transcodingService.getTranscodeDirectory().getPath());
+ command.setTranscodeSchemes(TranscodeScheme.values());
+ command.setLdapEnabled(settingsService.isLdapEnabled());
+
+ return command;
+ }
+
+ private User getUser(HttpServletRequest request) throws ServletRequestBindingException {
+ Integer userIndex = ServletRequestUtils.getIntParameter(request, "userIndex");
+ if (userIndex != null) {
+ List<User> allUsers = securityService.getAllUsers();
+ if (userIndex >= 0 && userIndex < allUsers.size()) {
+ return allUsers.get(userIndex);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void doSubmitAction(Object comm) throws Exception {
+ UserSettingsCommand command = (UserSettingsCommand) comm;
+
+ if (command.isDelete()) {
+ deleteUser(command);
+ } else if (command.isNew()) {
+ createUser(command);
+ } else {
+ updateUser(command);
+ }
+ resetCommand(command);
+ }
+
+ private void deleteUser(UserSettingsCommand command) {
+ securityService.deleteUser(command.getUsername());
+ }
+
+ public void createUser(UserSettingsCommand command) {
+ User user = new User(command.getUsername(), command.getPassword(), StringUtils.trimToNull(command.getEmail()));
+ user.setLdapAuthenticated(command.isLdapAuthenticated());
+ securityService.createUser(user);
+ updateUser(command);
+ }
+
+ private void updateUser(UserSettingsCommand command) {
+ User user = securityService.getUserByName(command.getUsername());
+ user.setEmail(StringUtils.trimToNull(command.getEmail()));
+ user.setLdapAuthenticated(command.isLdapAuthenticated());
+ user.setAdminRole(command.isAdminRole());
+ user.setDownloadRole(command.isDownloadRole());
+ user.setUploadRole(command.isUploadRole());
+ user.setCoverArtRole(command.isCoverArtRole());
+ user.setCommentRole(command.isCommentRole());
+ user.setPodcastRole(command.isPodcastRole());
+ user.setStreamRole(command.isStreamRole());
+ user.setJukeboxRole(command.isJukeboxRole());
+ user.setSettingsRole(command.isSettingsRole());
+ user.setShareRole(command.isShareRole());
+
+ if (command.isPasswordChange()) {
+ user.setPassword(command.getPassword());
+ }
+
+ securityService.updateUser(user);
+
+ UserSettings userSettings = settingsService.getUserSettings(command.getUsername());
+ userSettings.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName()));
+ userSettings.setChanged(new Date());
+ settingsService.updateUserSettings(userSettings);
+ }
+
+ private void resetCommand(UserSettingsCommand command) {
+ command.setUser(null);
+ command.setUsers(securityService.getAllUsers());
+ command.setDelete(false);
+ command.setPasswordChange(false);
+ command.setNew(true);
+ command.setStreamRole(true);
+ command.setSettingsRole(true);
+ command.setPassword(null);
+ command.setConfirmPassword(null);
+ command.setEmail(null);
+ command.setTranscodeSchemeName(null);
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java
new file mode 100644
index 00000000..1d7686eb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java
@@ -0,0 +1,110 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.ParameterizableViewController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Controller for the page used to play videos.
+ *
+ * @author Sindre Mehus
+ */
+public class VideoPlayerController extends ParameterizableViewController {
+
+ public static final int DEFAULT_BIT_RATE = 1000;
+ public static final int[] BIT_RATES = {200, 300, 400, 500, 700, 1000, 1200, 1500, 2000, 3000, 5000};
+ private static final long TRIAL_DAYS = 30L;
+
+ private MediaFileService mediaFileService;
+ private SettingsService settingsService;
+ private PlayerService playerService;
+
+ @Override
+ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
+ MediaFile file = mediaFileService.getMediaFile(id);
+
+ int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0);
+ timeOffset = Math.max(0, timeOffset);
+ Integer duration = file.getDurationSeconds();
+ if (duration != null) {
+ map.put("skipOffsets", createSkipOffsets(duration));
+ timeOffset = Math.min(duration, timeOffset);
+ duration -= timeOffset;
+ }
+
+ map.put("video", file);
+ map.put("player", playerService.getPlayer(request, response).getId());
+ map.put("maxBitRate", ServletRequestUtils.getIntParameter(request, "maxBitRate", DEFAULT_BIT_RATE));
+ map.put("popout", ServletRequestUtils.getBooleanParameter(request, "popout", false));
+ map.put("duration", duration);
+ map.put("timeOffset", timeOffset);
+ map.put("bitRates", BIT_RATES);
+
+ if (!settingsService.isLicenseValid() && settingsService.getVideoTrialExpires() == null) {
+ Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L);
+ settingsService.setVideoTrialExpires(expiryDate);
+ settingsService.save();
+ }
+ Date trialExpires = settingsService.getVideoTrialExpires();
+ map.put("trialExpires", trialExpires);
+ map.put("trialExpired", trialExpires != null && trialExpires.before(new Date()));
+ map.put("trial", trialExpires != null && !settingsService.isLicenseValid());
+
+ ModelAndView result = super.handleRequestInternal(request, response);
+ result.addObject("model", map);
+ return result;
+ }
+
+ public static Map<String, Integer> createSkipOffsets(int durationSeconds) {
+ LinkedHashMap<String, Integer> result = new LinkedHashMap<String, Integer>();
+ for (int i = 0; i < durationSeconds; i += 60) {
+ result.put(StringUtil.formatDuration(i), i);
+ }
+ return result;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java
new file mode 100644
index 00000000..02509687
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java
@@ -0,0 +1,247 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.controller;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.web.bind.ServletRequestUtils;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.MusicIndex;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.RandomSearchCriteria;
+import net.sourceforge.subsonic.domain.SearchCriteria;
+import net.sourceforge.subsonic.domain.SearchResult;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.SearchService;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.MusicIndexService;
+import net.sourceforge.subsonic.service.PlayerService;
+import net.sourceforge.subsonic.service.PlaylistService;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+
+/**
+ * Multi-controller used for wap pages.
+ *
+ * @author Sindre Mehus
+ */
+public class WapController extends MultiActionController {
+
+ private SettingsService settingsService;
+ private PlayerService playerService;
+ private PlaylistService playlistService;
+ private SecurityService securityService;
+ private MusicIndexService musicIndexService;
+ private MediaFileService mediaFileService;
+ private SearchService searchService;
+
+ public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ return wap(request, response);
+ }
+
+ public ModelAndView wap(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+ List<MusicFolder> folders = settingsService.getAllMusicFolders();
+
+ if (folders.isEmpty()) {
+ map.put("noMusic", true);
+ } else {
+
+ SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> allArtists = musicIndexService.getIndexedArtists(folders);
+
+ // If an index is given as parameter, only show music files for this index.
+ String index = request.getParameter("index");
+ if (index != null) {
+ SortedSet<MusicIndex.Artist> artists = allArtists.get(new MusicIndex(index));
+ if (artists == null) {
+ map.put("noMusic", true);
+ } else {
+ map.put("artists", artists);
+ }
+ }
+
+ // Otherwise, list all indexes.
+ else {
+ map.put("indexes", allArtists.keySet());
+ }
+ }
+
+ return new ModelAndView("wap/index", "model", map);
+ }
+
+ public ModelAndView browse(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String path = request.getParameter("path");
+ MediaFile parent = mediaFileService.getMediaFile(path);
+
+ // Create array of file(s) to display.
+ List<MediaFile> children;
+ if (parent.isDirectory()) {
+ children = mediaFileService.getChildrenOf(parent, true, true, true);
+ } else {
+ children = new ArrayList<MediaFile>();
+ children.add(parent);
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("parent", parent);
+ map.put("children", children);
+ map.put("user", securityService.getCurrentUser(request));
+
+ return new ModelAndView("wap/browse", "model", map);
+ }
+
+ public ModelAndView playlist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ // Create array of players to control. If the "player" attribute is set for this session,
+ // only the player with this ID is controlled. Otherwise, all players are controlled.
+ List<Player> players = playerService.getAllPlayers();
+
+ String playerId = (String) request.getSession().getAttribute("player");
+ if (playerId != null) {
+ Player player = playerService.getPlayerById(playerId);
+ if (player != null) {
+ players = Arrays.asList(player);
+ }
+ }
+
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ for (Player player : players) {
+ PlayQueue playQueue = player.getPlayQueue();
+ map.put("playlist", playQueue);
+
+ if (request.getParameter("play") != null) {
+ MediaFile file = mediaFileService.getMediaFile(request.getParameter("play"));
+ playQueue.addFiles(false, file);
+ } else if (request.getParameter("add") != null) {
+ MediaFile file = mediaFileService.getMediaFile(request.getParameter("add"));
+ playQueue.addFiles(true, file);
+ } else if (request.getParameter("skip") != null) {
+ playQueue.setIndex(Integer.parseInt(request.getParameter("skip")));
+ } else if (request.getParameter("clear") != null) {
+ playQueue.clear();
+ } else if (request.getParameter("load") != null) {
+ List<MediaFile> songs = playlistService.getFilesInPlaylist(ServletRequestUtils.getIntParameter(request, "id"));
+ playQueue.addFiles(false, songs);
+ } else if (request.getParameter("random") != null) {
+ List<MediaFile> randomFiles = searchService.getRandomSongs(new RandomSearchCriteria(20, null, null, null, null));
+ playQueue.addFiles(false, randomFiles);
+ }
+ }
+
+ map.put("players", players);
+ return new ModelAndView("wap/playlist", "model", map);
+ }
+
+ public ModelAndView loadPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("playlists", playlistService.getReadablePlaylistsForUser(securityService.getCurrentUsername(request)));
+ return new ModelAndView("wap/loadPlaylist", "model", map);
+ }
+
+ public ModelAndView search(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ return new ModelAndView("wap/search");
+ }
+
+ public ModelAndView searchResult(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String query = request.getParameter("query");
+
+ Map<String, Object> map = new HashMap<String, Object>();
+ map.put("hits", search(query));
+
+ return new ModelAndView("wap/searchResult", "model", map);
+ }
+
+ public ModelAndView settings(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ String playerId = (String) request.getSession().getAttribute("player");
+
+ List<Player> allPlayers = playerService.getAllPlayers();
+ User user = securityService.getCurrentUser(request);
+ List<Player> players = new ArrayList<Player>();
+ Map<String, Object> map = new HashMap<String, Object>();
+
+ for (Player player : allPlayers) {
+ // Only display authorized players.
+ if (user.isAdminRole() || user.getUsername().equals(player.getUsername())) {
+ players.add(player);
+ }
+
+ }
+ map.put("playerId", playerId);
+ map.put("players", players);
+ return new ModelAndView("wap/settings", "model", map);
+ }
+
+ public ModelAndView selectPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ request.getSession().setAttribute("player", request.getParameter("playerId"));
+ return settings(request, response);
+ }
+
+ private List<MediaFile> search(String query) throws IOException {
+ SearchCriteria criteria = new SearchCriteria();
+ criteria.setQuery(query);
+ criteria.setOffset(0);
+ criteria.setCount(50);
+
+ SearchResult result = searchService.search(criteria, SearchService.IndexType.SONG);
+ return result.getMediaFiles();
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public void setPlaylistService(PlaylistService playlistService) {
+ this.playlistService = playlistService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMusicIndexService(MusicIndexService musicIndexService) {
+ this.musicIndexService = musicIndexService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java
new file mode 100644
index 00000000..de17f4d4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java
@@ -0,0 +1,127 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.util.Date;
+import java.util.List;
+
+import org.springframework.jdbc.core.*;
+
+import net.sourceforge.subsonic.Logger;
+
+/**
+ * Abstract superclass for all DAO's.
+ *
+ * @author Sindre Mehus
+ */
+public class AbstractDao {
+ private static final Logger LOG = Logger.getLogger(AbstractDao.class);
+
+ private DaoHelper daoHelper;
+
+ /**
+ * Returns a JDBC template for performing database operations.
+ * @return A JDBC template.
+ */
+ public JdbcTemplate getJdbcTemplate() {
+ return daoHelper.getJdbcTemplate();
+ }
+
+ protected String questionMarks(String columns) {
+ int count = columns.split(", ").length;
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ builder.append('?');
+ if (i < count - 1) {
+ builder.append(", ");
+ }
+ }
+ return builder.toString();
+ }
+
+ protected String prefix(String columns, String prefix) {
+ StringBuilder builder = new StringBuilder();
+ for (String s : columns.split(", ")) {
+ builder.append(prefix).append(".").append(s).append(",");
+ }
+ if (builder.length() > 0) {
+ builder.setLength(builder.length() - 1);
+ }
+ return builder.toString();
+ }
+
+ protected int update(String sql, Object... args) {
+ long t = System.nanoTime();
+ int result = getJdbcTemplate().update(sql, args);
+ log(sql, t);
+ return result;
+ }
+
+ private void log(String sql, long startTimeNano) {
+// long micros = (System.nanoTime() - startTimeNano) / 1000L;
+// LOG.debug(micros + " " + sql);
+ }
+
+ protected <T> List<T> query(String sql, RowMapper rowMapper, Object... args) {
+ long t = System.nanoTime();
+ List<T> result = getJdbcTemplate().query(sql, args, rowMapper);
+ log(sql, t);
+ return result;
+ }
+
+ protected List<String> queryForStrings(String sql, Object... args) {
+ long t = System.nanoTime();
+ List<String> result = getJdbcTemplate().queryForList(sql, args, String.class);
+ log(sql, t);
+ return result;
+ }
+
+ protected Integer queryForInt(String sql, Integer defaultValue, Object... args) {
+ long t = System.nanoTime();
+ List<Integer> list = getJdbcTemplate().queryForList(sql, args, Integer.class);
+ Integer result = list.isEmpty() ? defaultValue : list.get(0) == null ? defaultValue : list.get(0);
+ log(sql, t);
+ return result;
+ }
+
+ protected Date queryForDate(String sql, Date defaultValue, Object... args) {
+ long t = System.nanoTime();
+ List<Date> list = getJdbcTemplate().queryForList(sql, args, Date.class);
+ Date result = list.isEmpty() ? defaultValue : list.get(0) == null ? defaultValue : list.get(0);
+ log(sql, t);
+ return result;
+ }
+
+ protected Long queryForLong(String sql, Long defaultValue, Object... args) {
+ long t = System.nanoTime();
+ List<Long> list = getJdbcTemplate().queryForList(sql, args, Long.class);
+ Long result = list.isEmpty() ? defaultValue : list.get(0) == null ? defaultValue : list.get(0);
+ log(sql, t);
+ return result;
+ }
+
+ protected <T> T queryOne(String sql, RowMapper rowMapper, Object... args) {
+ List<T> list = query(sql, rowMapper, args);
+ return list.isEmpty() ? null : list.get(0);
+ }
+
+ public void setDaoHelper(DaoHelper daoHelper) {
+ this.daoHelper = daoHelper;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java
new file mode 100644
index 00000000..603f6dad
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java
@@ -0,0 +1,243 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Album;
+import net.sourceforge.subsonic.domain.MediaFile;
+import org.apache.commons.lang.ObjectUtils;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Provides database services for albums.
+ *
+ * @author Sindre Mehus
+ */
+public class AlbumDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(AlbumDao.class);
+ private static final String COLUMNS = "id, path, name, artist, song_count, duration_seconds, cover_art_path, " +
+ "play_count, last_played, comment, created, last_scanned, present";
+
+ private final RowMapper rowMapper = new AlbumMapper();
+
+ /**
+ * Returns the album with the given artist and album name.
+ *
+ * @param artistName The artist name.
+ * @param albumName The album name.
+ * @return The album or null.
+ */
+ public Album getAlbum(String artistName, String albumName) {
+ return queryOne("select " + COLUMNS + " from album where artist=? and name=?", rowMapper, artistName, albumName);
+ }
+
+ /**
+ * Returns the album that the given file (most likely) is part of.
+ *
+ * @param file The media file.
+ * @return The album or null.
+ */
+ public Album getAlbumForFile(MediaFile file) {
+
+ // First, get all albums with the correct album name (irrespective of artist).
+ List<Album> candidates = query("select " + COLUMNS + " from album where name=?", rowMapper, file.getAlbumName());
+ if (candidates.isEmpty()) {
+ return null;
+ }
+
+ // Look for album with the correct artist.
+ for (Album candidate : candidates) {
+ if (ObjectUtils.equals(candidate.getArtist(), file.getArtist())) {
+ return candidate;
+ }
+ }
+
+ // Look for album with the same path as the file.
+ for (Album candidate : candidates) {
+ if (ObjectUtils.equals(candidate.getPath(), file.getParentPath())) {
+ return candidate;
+ }
+ }
+
+ // No appropriate album found.
+ return null;
+ }
+
+ public Album getAlbum(int id) {
+ return queryOne("select " + COLUMNS + " from album where id=?", rowMapper, id);
+ }
+
+ public List<Album> getAlbumsForArtist(String artist) {
+ return query("select " + COLUMNS + " from album where artist=? and present order by name", rowMapper, artist);
+ }
+
+ /**
+ * Creates or updates an album.
+ *
+ * @param album The album to create/update.
+ */
+ public synchronized void createOrUpdateAlbum(Album album) {
+ String sql = "update album set " +
+ "song_count=?," +
+ "duration_seconds=?," +
+ "cover_art_path=?," +
+ "play_count=?," +
+ "last_played=?," +
+ "comment=?," +
+ "created=?," +
+ "last_scanned=?," +
+ "present=? " +
+ "where artist=? and name=?";
+
+ int n = update(sql, album.getSongCount(), album.getDurationSeconds(), album.getCoverArtPath(), album.getPlayCount(), album.getLastPlayed(),
+ album.getComment(), album.getCreated(), album.getLastScanned(), album.isPresent(), album.getArtist(), album.getName());
+
+ if (n == 0) {
+
+ update("insert into album (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null, album.getPath(), album.getName(), album.getArtist(),
+ album.getSongCount(), album.getDurationSeconds(), album.getCoverArtPath(), album.getPlayCount(), album.getLastPlayed(),
+ album.getComment(), album.getCreated(), album.getLastScanned(), album.isPresent());
+ }
+
+ int id = queryForInt("select id from album where artist=? and name=?", null, album.getArtist(), album.getName());
+ album.setId(id);
+ }
+
+ /**
+ * Returns albums in alphabetical order.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @param byArtist Whether to sort by artist name
+ * @return Albums in alphabetical order.
+ */
+ public List<Album> getAlphabetialAlbums(int offset, int count, boolean byArtist) {
+ String orderBy = byArtist ? "artist, name" : "name";
+ return query("select " + COLUMNS + " from album where present order by " + orderBy + " limit ? offset ?", rowMapper, count, offset);
+ }
+
+ /**
+ * Returns the most frequently played albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most frequently played albums.
+ */
+ public List<Album> getMostFrequentlyPlayedAlbums(int offset, int count) {
+ return query("select " + COLUMNS + " from album where play_count > 0 and present " +
+ "order by play_count desc limit ? offset ?", rowMapper, count, offset);
+ }
+
+ /**
+ * Returns the most recently played albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most recently played albums.
+ */
+ public List<Album> getMostRecentlyPlayedAlbums(int offset, int count) {
+ return query("select " + COLUMNS + " from album where last_played is not null and present " +
+ "order by last_played desc limit ? offset ?", rowMapper, count, offset);
+ }
+
+ /**
+ * Returns the most recently added albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most recently added albums.
+ */
+ public List<Album> getNewestAlbums(int offset, int count) {
+ return query("select " + COLUMNS + " from album where present order by created desc limit ? offset ?",
+ rowMapper, count, offset);
+ }
+
+ /**
+ * Returns the most recently starred albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @param username Returns albums starred by this user.
+ * @return The most recently starred albums for this user.
+ */
+ public List<Album> getStarredAlbums(int offset, int count, String username) {
+ return query("select " + prefix(COLUMNS, "album") + " from album, starred_album where album.id = starred_album.album_id and " +
+ "album.present and starred_album.username=? order by starred_album.created desc limit ? offset ?",
+ rowMapper, username, count, offset);
+ }
+
+ public void markNonPresent(Date lastScanned) {
+ int minId = queryForInt("select id from album where true limit 1", 0);
+ int maxId = queryForInt("select max(id) from album", 0);
+
+ final int batchSize = 1000;
+ for (int id = minId; id <= maxId; id += batchSize) {
+ update("update album set present=false where id between ? and ? and last_scanned != ? and present", id, id + batchSize, lastScanned);
+ }
+ }
+
+ public void expunge() {
+ int minId = queryForInt("select id from album where true limit 1", 0);
+ int maxId = queryForInt("select max(id) from album", 0);
+
+ final int batchSize = 1000;
+ for (int id = minId; id <= maxId; id += batchSize) {
+ update("delete from album where id between ? and ? and not present", id, id + batchSize);
+ }
+ }
+
+ public void starAlbum(int albumId, String username) {
+ unstarAlbum(albumId, username);
+ update("insert into starred_album(album_id, username, created) values (?,?,?)", albumId, username, new Date());
+ }
+
+ public void unstarAlbum(int albumId, String username) {
+ update("delete from starred_album where album_id=? and username=?", albumId, username);
+ }
+
+ public Date getAlbumStarredDate(int albumId, String username) {
+ return queryForDate("select created from starred_album where album_id=? and username=?", null, albumId, username);
+ }
+
+ private static class AlbumMapper implements ParameterizedRowMapper<Album> {
+ public Album mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Album(
+ rs.getInt(1),
+ rs.getString(2),
+ rs.getString(3),
+ rs.getString(4),
+ rs.getInt(5),
+ rs.getInt(6),
+ rs.getString(7),
+ rs.getInt(8),
+ rs.getTimestamp(9),
+ rs.getString(10),
+ rs.getTimestamp(11),
+ rs.getTimestamp(12),
+ rs.getBoolean(13));
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java
new file mode 100644
index 00000000..41d57c33
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java
@@ -0,0 +1,161 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Artist;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Provides database services for artists.
+ *
+ * @author Sindre Mehus
+ */
+public class ArtistDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(ArtistDao.class);
+ private static final String COLUMNS = "id, name, cover_art_path, album_count, last_scanned, present";
+
+ private final RowMapper rowMapper = new ArtistMapper();
+
+ /**
+ * Returns the artist with the given name.
+ *
+ * @param artistName The artist name.
+ * @return The artist or null.
+ */
+ public Artist getArtist(String artistName) {
+ return queryOne("select " + COLUMNS + " from artist where name=?", rowMapper, artistName);
+ }
+
+ /**
+ * Returns the artist with the given ID.
+ *
+ * @param id The artist ID.
+ * @return The artist or null.
+ */
+ public Artist getArtist(int id) {
+ return queryOne("select " + COLUMNS + " from artist where id=?", rowMapper, id);
+ }
+
+ /**
+ * Creates or updates an artist.
+ *
+ * @param artist The artist to create/update.
+ */
+ public synchronized void createOrUpdateArtist(Artist artist) {
+ String sql = "update artist set " +
+ "cover_art_path=?," +
+ "album_count=?," +
+ "last_scanned=?," +
+ "present=? " +
+ "where name=?";
+
+ int n = update(sql, artist.getCoverArtPath(), artist.getAlbumCount(), artist.getLastScanned(), artist.isPresent(), artist.getName());
+
+ if (n == 0) {
+
+ update("insert into artist (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null,
+ artist.getName(), artist.getCoverArtPath(), artist.getAlbumCount(), artist.getLastScanned(), artist.isPresent());
+ }
+
+ int id = queryForInt("select id from artist where name=?", null, artist.getName());
+ artist.setId(id);
+ }
+
+ /**
+ * Returns artists in alphabetical order.
+ *
+ * @param offset Number of artists to skip.
+ * @param count Maximum number of artists to return.
+ * @return Artists in alphabetical order.
+ */
+ public List<Artist> getAlphabetialArtists(int offset, int count) {
+ return query("select " + COLUMNS + " from artist where present order by name limit ? offset ?", rowMapper, count, offset);
+ }
+
+ /**
+ * Returns the most recently starred artists.
+ *
+ * @param offset Number of artists to skip.
+ * @param count Maximum number of artists to return.
+ * @param username Returns artists starred by this user.
+ * @return The most recently starred artists for this user.
+ */
+ public List<Artist> getStarredArtists(int offset, int count, String username) {
+ return query("select " + prefix(COLUMNS, "artist") + " from artist, starred_artist where artist.id = starred_artist.artist_id and " +
+ "artist.present and starred_artist.username=? order by starred_artist.created desc limit ? offset ?",
+ rowMapper, username, count, offset);
+ }
+
+ public void markPresent(String artistName, Date lastScanned) {
+ update("update artist set present=?, last_scanned=? where name=?", true, lastScanned, artistName);
+ }
+
+ public void markNonPresent(Date lastScanned) {
+ int minId = queryForInt("select id from artist where true limit 1", 0);
+ int maxId = queryForInt("select max(id) from artist", 0);
+
+ final int batchSize = 1000;
+ for (int id = minId; id <= maxId; id += batchSize) {
+ update("update artist set present=false where id between ? and ? and last_scanned != ? and present", id, id + batchSize, lastScanned);
+ }
+ }
+
+ public void expunge() {
+ int minId = queryForInt("select id from artist where true limit 1", 0);
+ int maxId = queryForInt("select max(id) from artist", 0);
+
+ final int batchSize = 1000;
+ for (int id = minId; id <= maxId; id += batchSize) {
+ update("delete from artist where id between ? and ? and not present", id, id + batchSize);
+ }
+ }
+
+ public void starArtist(int artistId, String username) {
+ unstarArtist(artistId, username);
+ update("insert into starred_artist(artist_id, username, created) values (?,?,?)", artistId, username, new Date());
+ }
+
+ public void unstarArtist(int artistId, String username) {
+ update("delete from starred_artist where artist_id=? and username=?", artistId, username);
+ }
+
+ public Date getArtistStarredDate(int artistId, String username) {
+ return queryForDate("select created from starred_artist where artist_id=? and username=?", null, artistId, username);
+ }
+
+ private static class ArtistMapper implements ParameterizedRowMapper<Artist> {
+ public Artist mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Artist(
+ rs.getInt(1),
+ rs.getString(2),
+ rs.getString(3),
+ rs.getInt(4),
+ rs.getTimestamp(5),
+ rs.getBoolean(6));
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java
new file mode 100644
index 00000000..abdc118d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java
@@ -0,0 +1,94 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.domain.Avatar;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Provides database services for avatars.
+ *
+ * @author Sindre Mehus
+ */
+public class AvatarDao extends AbstractDao {
+
+ private static final String COLUMNS = "id, name, created_date, mime_type, width, height, data";
+ private final AvatarRowMapper rowMapper = new AvatarRowMapper();
+
+ /**
+ * Returns all system avatars.
+ *
+ * @return All system avatars.
+ */
+ public List<Avatar> getAllSystemAvatars() {
+ String sql = "select " + COLUMNS + " from system_avatar";
+ return query(sql, rowMapper);
+ }
+
+ /**
+ * Returns the system avatar with the given ID.
+ *
+ * @param id The system avatar ID.
+ * @return The avatar or <code>null</code> if not found.
+ */
+ public Avatar getSystemAvatar(int id) {
+ String sql = "select " + COLUMNS + " from system_avatar where id=" + id;
+ return queryOne(sql, rowMapper);
+ }
+
+ /**
+ * Returns the custom avatar for the given user.
+ *
+ * @param username The username.
+ * @return The avatar or <code>null</code> if not found.
+ */
+ public Avatar getCustomAvatar(String username) {
+ String sql = "select " + COLUMNS + " from custom_avatar where username=?";
+ return queryOne(sql, rowMapper, username);
+ }
+
+ /**
+ * Sets the custom avatar for the given user.
+ *
+ * @param avatar The avatar, or <code>null</code> to remove the avatar.
+ * @param username The username.
+ */
+ public void setCustomAvatar(Avatar avatar, String username) {
+ String sql = "delete from custom_avatar where username=?";
+ update(sql, username);
+
+ if (avatar != null) {
+ update("insert into custom_avatar(" + COLUMNS + ", username) values(" + questionMarks(COLUMNS) + ", ?)",
+ null, avatar.getName(), avatar.getCreatedDate(), avatar.getMimeType(),
+ avatar.getWidth(), avatar.getHeight(), avatar.getData(), username);
+ }
+ }
+
+ private static class AvatarRowMapper implements ParameterizedRowMapper<Avatar> {
+ public Avatar mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Avatar(rs.getInt(1), rs.getString(2), rs.getTimestamp(3), rs.getString(4),
+ rs.getInt(5), rs.getInt(6), rs.getBytes(7));
+ }
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java
new file mode 100644
index 00000000..802a5b3d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java
@@ -0,0 +1,117 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.schema.Schema;
+import net.sourceforge.subsonic.dao.schema.Schema25;
+import net.sourceforge.subsonic.dao.schema.Schema26;
+import net.sourceforge.subsonic.dao.schema.Schema27;
+import net.sourceforge.subsonic.dao.schema.Schema28;
+import net.sourceforge.subsonic.dao.schema.Schema29;
+import net.sourceforge.subsonic.dao.schema.Schema30;
+import net.sourceforge.subsonic.dao.schema.Schema31;
+import net.sourceforge.subsonic.dao.schema.Schema32;
+import net.sourceforge.subsonic.dao.schema.Schema33;
+import net.sourceforge.subsonic.dao.schema.Schema34;
+import net.sourceforge.subsonic.dao.schema.Schema35;
+import net.sourceforge.subsonic.dao.schema.Schema36;
+import net.sourceforge.subsonic.dao.schema.Schema37;
+import net.sourceforge.subsonic.dao.schema.Schema38;
+import net.sourceforge.subsonic.dao.schema.Schema40;
+import net.sourceforge.subsonic.dao.schema.Schema43;
+import net.sourceforge.subsonic.dao.schema.Schema45;
+import net.sourceforge.subsonic.dao.schema.Schema46;
+import net.sourceforge.subsonic.dao.schema.Schema47;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+import javax.sql.DataSource;
+import java.io.File;
+
+/**
+ * DAO helper class which creates the data source, and updates the database schema.
+ *
+ * @author Sindre Mehus
+ */
+public class DaoHelper {
+
+ private static final Logger LOG = Logger.getLogger(DaoHelper.class);
+
+ private Schema[] schemas = {new Schema25(), new Schema26(), new Schema27(), new Schema28(), new Schema29(),
+ new Schema30(), new Schema31(), new Schema32(), new Schema33(), new Schema34(),
+ new Schema35(), new Schema36(), new Schema37(), new Schema38(), new Schema40(),
+ new Schema43(), new Schema45(), new Schema46(), new Schema47()};
+ private DataSource dataSource;
+ private static boolean shutdownHookAdded;
+
+ public DaoHelper() {
+ dataSource = createDataSource();
+ checkDatabase();
+ addShutdownHook();
+ }
+
+ private void addShutdownHook() {
+ if (shutdownHookAdded) {
+ return;
+ }
+ shutdownHookAdded = true;
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ System.err.println("Shutting down database.");
+ getJdbcTemplate().execute("shutdown");
+ System.err.println("Done.");
+ }
+ });
+ }
+
+ /**
+ * Returns a JDBC template for performing database operations.
+ *
+ * @return A JDBC template.
+ */
+ public JdbcTemplate getJdbcTemplate() {
+ return new JdbcTemplate(dataSource);
+ }
+
+ private DataSource createDataSource() {
+ File subsonicHome = SettingsService.getSubsonicHome();
+ DriverManagerDataSource ds = new DriverManagerDataSource();
+ ds.setDriverClassName("org.hsqldb.jdbcDriver");
+ ds.setUrl("jdbc:hsqldb:file:" + subsonicHome.getPath() + "/db/subsonic");
+ ds.setUsername("sa");
+ ds.setPassword("");
+
+ return ds;
+ }
+
+ private void checkDatabase() {
+ LOG.info("Checking database schema.");
+ try {
+ for (Schema schema : schemas) {
+ schema.execute(getJdbcTemplate());
+ }
+ LOG.info("Done checking database schema.");
+ } catch (Exception x) {
+ LOG.error("Failed to initialize database.", x);
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.java
new file mode 100644
index 00000000..c3c20a74
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.java
@@ -0,0 +1,89 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.InternetRadio;
+
+/**
+ * Provides database services for internet radio.
+ *
+ * @author Sindre Mehus
+ */
+public class InternetRadioDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(InternetRadioDao.class);
+ private static final String COLUMNS = "id, name, stream_url, homepage_url, enabled, changed";
+ private final InternetRadioRowMapper rowMapper = new InternetRadioRowMapper();
+
+ /**
+ * Returns all internet radio stations.
+ *
+ * @return Possibly empty list of all internet radio stations.
+ */
+ public List<InternetRadio> getAllInternetRadios() {
+ String sql = "select " + COLUMNS + " from internet_radio";
+ return query(sql, rowMapper);
+ }
+
+ /**
+ * Creates a new internet radio station.
+ *
+ * @param radio The internet radio station to create.
+ */
+ public void createInternetRadio(InternetRadio radio) {
+ String sql = "insert into internet_radio (" + COLUMNS + ") values (null, ?, ?, ?, ?, ?)";
+ update(sql, radio.getName(), radio.getStreamUrl(), radio.getHomepageUrl(), radio.isEnabled(), radio.getChanged());
+ LOG.info("Created internet radio station " + radio.getName());
+ }
+
+ /**
+ * Deletes the internet radio station with the given ID.
+ *
+ * @param id The internet radio station ID.
+ */
+ public void deleteInternetRadio(Integer id) {
+ String sql = "delete from internet_radio where id=?";
+ update(sql, id);
+ LOG.info("Deleted internet radio station with ID " + id);
+ }
+
+ /**
+ * Updates the given internet radio station.
+ *
+ * @param radio The internet radio station to update.
+ */
+ public void updateInternetRadio(InternetRadio radio) {
+ String sql = "update internet_radio set name=?, stream_url=?, homepage_url=?, enabled=?, changed=? where id=?";
+ update(sql, radio.getName(), radio.getStreamUrl(), radio.getHomepageUrl(), radio.isEnabled(), radio.getChanged(), radio.getId());
+ }
+
+ private static class InternetRadioRowMapper implements ParameterizedRowMapper<InternetRadio> {
+ public InternetRadio mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new InternetRadio(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getBoolean(5), rs.getTimestamp(6));
+ }
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java
new file mode 100644
index 00000000..e75bc7a6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java
@@ -0,0 +1,374 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MediaLibraryStatistics;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Date;
+import java.util.List;
+
+import static net.sourceforge.subsonic.domain.MediaFile.MediaType;
+import static net.sourceforge.subsonic.domain.MediaFile.MediaType.*;
+
+/**
+ * Provides database services for media files.
+ *
+ * @author Sindre Mehus
+ */
+public class MediaFileDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(MediaFileDao.class);
+ private static final String COLUMNS = "id, path, folder, type, format, title, album, artist, album_artist, disc_number, " +
+ "track_number, year, genre, bit_rate, variable_bit_rate, duration_seconds, file_size, width, height, cover_art_path, " +
+ "parent_path, play_count, last_played, comment, created, changed, last_scanned, children_last_updated, present, version";
+
+ private static final int VERSION = 1;
+
+ private final RowMapper rowMapper = new MediaFileMapper();
+ private final RowMapper musicFileInfoRowMapper = new MusicFileInfoMapper();
+
+ /**
+ * Returns the media file for the given path.
+ *
+ * @param path The path.
+ * @return The media file or null.
+ */
+ public MediaFile getMediaFile(String path) {
+ return queryOne("select " + COLUMNS + " from media_file where path=?", rowMapper, path);
+ }
+
+ /**
+ * Returns the media file for the given ID.
+ *
+ * @param id The ID.
+ * @return The media file or null.
+ */
+ public MediaFile getMediaFile(int id) {
+ return queryOne("select " + COLUMNS + " from media_file where id=?", rowMapper, id);
+ }
+
+ /**
+ * Returns the media file that are direct children of the given path.
+ *
+ * @param path The path.
+ * @return The list of children.
+ */
+ public List<MediaFile> getChildrenOf(String path) {
+ return query("select " + COLUMNS + " from media_file where parent_path=? and present", rowMapper, path);
+ }
+
+ public List<MediaFile> getFilesInPlaylist(int playlistId) {
+ return query("select " + prefix(COLUMNS, "media_file") + " from media_file, playlist_file where " +
+ "media_file.id = playlist_file.media_file_id and " +
+ "playlist_file.playlist_id = ? and " +
+ "media_file.present order by playlist_file.id", rowMapper, playlistId);
+ }
+
+ public List<MediaFile> getSongsForAlbum(String artist, String album) {
+ return query("select " + COLUMNS + " from media_file where album_artist=? and album=? and present and type in (?,?,?) order by track_number", rowMapper,
+ artist, album, MUSIC.name(), AUDIOBOOK.name(), PODCAST.name());
+ }
+
+ public List<MediaFile> getVideos(int size, int offset) {
+ return query("select " + COLUMNS + " from media_file where type=? and present order by title limit ? offset ?", rowMapper,
+ VIDEO.name(), size, offset);
+ }
+
+ /**
+ * Creates or updates a media file.
+ *
+ * @param file The media file to create/update.
+ */
+ public synchronized void createOrUpdateMediaFile(MediaFile file) {
+ String sql = "update media_file set " +
+ "folder=?," +
+ "type=?," +
+ "format=?," +
+ "title=?," +
+ "album=?," +
+ "artist=?," +
+ "album_artist=?," +
+ "disc_number=?," +
+ "track_number=?," +
+ "year=?," +
+ "genre=?," +
+ "bit_rate=?," +
+ "variable_bit_rate=?," +
+ "duration_seconds=?," +
+ "file_size=?," +
+ "width=?," +
+ "height=?," +
+ "cover_art_path=?," +
+ "parent_path=?," +
+ "play_count=?," +
+ "last_played=?," +
+ "comment=?," +
+ "changed=?," +
+ "last_scanned=?," +
+ "children_last_updated=?," +
+ "present=?, " +
+ "version=? " +
+ "where path=?";
+
+ int n = update(sql,
+ file.getFolder(), file.getMediaType().name(), file.getFormat(), file.getTitle(), file.getAlbumName(), file.getArtist(),
+ file.getAlbumArtist(), file.getDiscNumber(), file.getTrackNumber(), file.getYear(), file.getGenre(), file.getBitRate(),
+ file.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(),
+ file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(),
+ file.getChanged(), file.getLastScanned(), file.getChildrenLastUpdated(), file.isPresent(), VERSION, file.getPath());
+
+ if (n == 0) {
+
+ // Copy values from obsolete table music_file_info.
+ MediaFile musicFileInfo = getMusicFileInfo(file.getPath());
+ if (musicFileInfo != null) {
+ file.setComment(musicFileInfo.getComment());
+ file.setLastPlayed(musicFileInfo.getLastPlayed());
+ file.setPlayCount(musicFileInfo.getPlayCount());
+ }
+
+ update("insert into media_file (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null,
+ file.getPath(), file.getFolder(), file.getMediaType().name(), file.getFormat(), file.getTitle(), file.getAlbumName(), file.getArtist(),
+ file.getAlbumArtist(), file.getDiscNumber(), file.getTrackNumber(), file.getYear(), file.getGenre(), file.getBitRate(),
+ file.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(),
+ file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(),
+ file.getCreated(), file.getChanged(), file.getLastScanned(),
+ file.getChildrenLastUpdated(), file.isPresent(), VERSION);
+ }
+
+ int id = queryForInt("select id from media_file where path=?", null, file.getPath());
+ file.setId(id);
+ }
+
+ private MediaFile getMusicFileInfo(String path) {
+ return queryOne("select play_count, last_played, comment from music_file_info where path=?", musicFileInfoRowMapper, path);
+ }
+
+ @Deprecated
+ public List<String> getArtists() {
+ return queryForStrings("select distinct artist from media_file where artist is not null and present order by artist");
+ }
+
+ public void deleteMediaFile(String path) {
+ update("update media_file set present=false, children_last_updated=? where path=?", new Date(0L), path);
+ }
+
+ public List<String> getGenres() {
+ return queryForStrings("select distinct genre from media_file where genre is not null and present order by genre");
+ }
+
+ /**
+ * Returns the most frequently played albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most frequently played albums.
+ */
+ public List<MediaFile> getMostFrequentlyPlayedAlbums(int offset, int count) {
+ return query("select " + COLUMNS + " from media_file where type=? and play_count > 0 and present " +
+ "order by play_count desc limit ? offset ?", rowMapper, ALBUM.name(), count, offset);
+ }
+
+ /**
+ * Returns the most recently played albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most recently played albums.
+ */
+ public List<MediaFile> getMostRecentlyPlayedAlbums(int offset, int count) {
+ return query("select " + COLUMNS + " from media_file where type=? and last_played is not null and present " +
+ "order by last_played desc limit ? offset ?", rowMapper, ALBUM.name(), count, offset);
+ }
+
+ /**
+ * Returns the most recently added albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most recently added albums.
+ */
+ public List<MediaFile> getNewestAlbums(int offset, int count) {
+ return query("select " + COLUMNS + " from media_file where type=? and present order by created desc limit ? offset ?",
+ rowMapper, ALBUM.name(), count, offset);
+ }
+
+ /**
+ * Returns albums in alphabetical order.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @param byArtist Whether to sort by artist name
+ * @return Albums in alphabetical order.
+ */
+ public List<MediaFile> getAlphabetialAlbums(int offset, int count, boolean byArtist) {
+ String orderBy = byArtist ? "artist, album" : "album";
+ return query("select " + COLUMNS + " from media_file where type=? and artist != '' and present order by " + orderBy + " limit ? offset ?",
+ rowMapper, ALBUM.name(), count, offset);
+ }
+
+ /**
+ * Returns the most recently starred albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @param username Returns albums starred by this user.
+ * @return The most recently starred albums for this user.
+ */
+ public List<MediaFile> getStarredAlbums(int offset, int count, String username) {
+ return query("select " + prefix(COLUMNS, "media_file") + " from media_file, starred_media_file where media_file.id = starred_media_file.media_file_id and " +
+ "media_file.present and media_file.type=? and starred_media_file.username=? order by starred_media_file.created desc limit ? offset ?",
+ rowMapper, ALBUM.name(), username, count, offset);
+ }
+
+ /**
+ * Returns the most recently starred directories.
+ *
+ * @param offset Number of directories to skip.
+ * @param count Maximum number of directories to return.
+ * @param username Returns directories starred by this user.
+ * @return The most recently starred directories for this user.
+ */
+ public List<MediaFile> getStarredDirectories(int offset, int count, String username) {
+ return query("select " + prefix(COLUMNS, "media_file") + " from media_file, starred_media_file where media_file.id = starred_media_file.media_file_id and " +
+ "media_file.present and media_file.type=? and starred_media_file.username=? order by starred_media_file.created desc limit ? offset ?",
+ rowMapper, DIRECTORY.name(), username, count, offset);
+ }
+
+ /**
+ * Returns the most recently starred files.
+ *
+ * @param offset Number of files to skip.
+ * @param count Maximum number of files to return.
+ * @param username Returns files starred by this user.
+ * @return The most recently starred files for this user.
+ */
+ public List<MediaFile> getStarredFiles(int offset, int count, String username) {
+ return query("select " + prefix(COLUMNS, "media_file") + " from media_file, starred_media_file where media_file.id = starred_media_file.media_file_id and " +
+ "media_file.present and media_file.type in (?,?,?,?) and starred_media_file.username=? order by starred_media_file.created desc limit ? offset ?",
+ rowMapper, MUSIC.name(), PODCAST.name(), AUDIOBOOK.name(), VIDEO.name(), username, count, offset);
+ }
+
+ public void starMediaFile(int id, String username) {
+ unstarMediaFile(id, username);
+ update("insert into starred_media_file(media_file_id, username, created) values (?,?,?)", id, username, new Date());
+ }
+
+ public void unstarMediaFile(int id, String username) {
+ update("delete from starred_media_file where media_file_id=? and username=?", id, username);
+ }
+
+ public Date getMediaFileStarredDate(int id, String username) {
+ return queryForDate("select created from starred_media_file where media_file_id=? and username=?", null, id, username);
+ }
+
+ /**
+ * Returns media library statistics, including the number of artists, albums and songs.
+ *
+ * @return Media library statistics.
+ */
+ public MediaLibraryStatistics getStatistics() {
+ int artistCount = queryForInt("select count(1) from artist where present", 0);
+ int albumCount = queryForInt("select count(1) from album where present", 0);
+ int songCount = queryForInt("select count(1) from media_file where type in (?, ?, ?, ?) and present", 0, VIDEO.name(), MUSIC.name(), AUDIOBOOK.name(), PODCAST.name());
+ long totalLengthInBytes = queryForLong("select sum(file_size) from media_file where present", 0L);
+ long totalDurationInSeconds = queryForLong("select sum(duration_seconds) from media_file where present", 0L);
+
+ return new MediaLibraryStatistics(artistCount, albumCount, songCount, totalLengthInBytes, totalDurationInSeconds);
+ }
+
+ public void markPresent(String path, Date lastScanned) {
+ update("update media_file set present=?, last_scanned=? where path=?", true, lastScanned, path);
+ }
+
+ public void markNonPresent(Date lastScanned) {
+ int minId = queryForInt("select id from media_file where true limit 1", 0);
+ int maxId = queryForInt("select max(id) from media_file", 0);
+
+ final int batchSize = 1000;
+ Date childrenLastUpdated = new Date(0L); // Used to force a children rescan if file is later resurrected.
+ for (int id = minId; id <= maxId; id += batchSize) {
+ update("update media_file set present=false, children_last_updated=? where id between ? and ? and last_scanned != ? and present",
+ childrenLastUpdated, id, id + batchSize, lastScanned);
+ }
+ }
+
+ public void expunge() {
+ int minId = queryForInt("select id from media_file where true limit 1", 0);
+ int maxId = queryForInt("select max(id) from media_file", 0);
+
+ final int batchSize = 1000;
+ for (int id = minId; id <= maxId; id += batchSize) {
+ update("delete from media_file where id between ? and ? and not present", id, id + batchSize);
+ }
+ update("checkpoint");
+ }
+
+ private static class MediaFileMapper implements ParameterizedRowMapper<MediaFile> {
+ public MediaFile mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new MediaFile(
+ rs.getInt(1),
+ rs.getString(2),
+ rs.getString(3),
+ MediaType.valueOf(rs.getString(4)),
+ rs.getString(5),
+ rs.getString(6),
+ rs.getString(7),
+ rs.getString(8),
+ rs.getString(9),
+ rs.getInt(10) == 0 ? null : rs.getInt(10),
+ rs.getInt(11) == 0 ? null : rs.getInt(11),
+ rs.getInt(12) == 0 ? null : rs.getInt(12),
+ rs.getString(13),
+ rs.getInt(14) == 0 ? null : rs.getInt(14),
+ rs.getBoolean(15),
+ rs.getInt(16) == 0 ? null : rs.getInt(16),
+ rs.getLong(17) == 0 ? null : rs.getLong(17),
+ rs.getInt(18) == 0 ? null : rs.getInt(18),
+ rs.getInt(19) == 0 ? null : rs.getInt(19),
+ rs.getString(20),
+ rs.getString(21),
+ rs.getInt(22),
+ rs.getTimestamp(23),
+ rs.getString(24),
+ rs.getTimestamp(25),
+ rs.getTimestamp(26),
+ rs.getTimestamp(27),
+ rs.getTimestamp(28),
+ rs.getBoolean(29));
+ }
+ }
+
+ private static class MusicFileInfoMapper implements ParameterizedRowMapper<MediaFile> {
+ public MediaFile mapRow(ResultSet rs, int rowNum) throws SQLException {
+ MediaFile file = new MediaFile();
+ file.setPlayCount(rs.getInt(1));
+ file.setLastPlayed(rs.getTimestamp(2));
+ file.setComment(rs.getString(3));
+ return file;
+ }
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.java
new file mode 100644
index 00000000..a5205d71
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.java
@@ -0,0 +1,91 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.io.File;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MusicFolder;
+
+/**
+ * Provides database services for music folders.
+ *
+ * @author Sindre Mehus
+ */
+public class MusicFolderDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(MusicFolderDao.class);
+ private static final String COLUMNS = "id, path, name, enabled, changed";
+ private final MusicFolderRowMapper rowMapper = new MusicFolderRowMapper();
+
+ /**
+ * Returns all music folders.
+ *
+ * @return Possibly empty list of all music folders.
+ */
+ public List<MusicFolder> getAllMusicFolders() {
+ String sql = "select " + COLUMNS + " from music_folder";
+ return query(sql, rowMapper);
+ }
+
+ /**
+ * Creates a new music folder.
+ *
+ * @param musicFolder The music folder to create.
+ */
+ public void createMusicFolder(MusicFolder musicFolder) {
+ String sql = "insert into music_folder (" + COLUMNS + ") values (null, ?, ?, ?, ?)";
+ update(sql, musicFolder.getPath(), musicFolder.getName(), musicFolder.isEnabled(), musicFolder.getChanged());
+ LOG.info("Created music folder " + musicFolder.getPath());
+ }
+
+ /**
+ * Deletes the music folder with the given ID.
+ *
+ * @param id The music folder ID.
+ */
+ public void deleteMusicFolder(Integer id) {
+ String sql = "delete from music_folder where id=?";
+ update(sql, id);
+ LOG.info("Deleted music folder with ID " + id);
+ }
+
+ /**
+ * Updates the given music folder.
+ *
+ * @param musicFolder The music folder to update.
+ */
+ public void updateMusicFolder(MusicFolder musicFolder) {
+ String sql = "update music_folder set path=?, name=?, enabled=?, changed=? where id=?";
+ update(sql, musicFolder.getPath().getPath(), musicFolder.getName(),
+ musicFolder.isEnabled(), musicFolder.getChanged(), musicFolder.getId());
+ }
+
+ private static class MusicFolderRowMapper implements ParameterizedRowMapper<MusicFolder> {
+ public MusicFolder mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new MusicFolder(rs.getInt(1), new File(rs.getString(2)), rs.getString(3), rs.getBoolean(4), rs.getTimestamp(5));
+ }
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java
new file mode 100644
index 00000000..f129fa37
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java
@@ -0,0 +1,194 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.CoverArtScheme;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayerTechnology;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+
+/**
+ * Provides player-related database services.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayerDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(PlayerDao.class);
+ private static final String COLUMNS = "id, name, type, username, ip_address, auto_control_enabled, " +
+ "last_seen, cover_art_scheme, transcode_scheme, dynamic_ip, technology, client_id";
+
+ private PlayerRowMapper rowMapper = new PlayerRowMapper();
+ private Map<String, PlayQueue> playlists = Collections.synchronizedMap(new HashMap<String, PlayQueue>());
+
+ /**
+ * Returns all players.
+ *
+ * @return Possibly empty list of all users.
+ */
+ public List<Player> getAllPlayers() {
+ String sql = "select " + COLUMNS + " from player";
+ return query(sql, rowMapper);
+ }
+
+ /**
+ * Returns all players owned by the given username and client ID.
+ *
+ * @param username The name of the user.
+ * @param clientId The third-party client ID (used if this player is managed over the
+ * Subsonic REST API). May be <code>null</code>.
+ * @return All relevant players.
+ */
+ public List<Player> getPlayersForUserAndClientId(String username, String clientId) {
+ if (clientId != null) {
+ String sql = "select " + COLUMNS + " from player where username=? and client_id=?";
+ return query(sql, rowMapper, username, clientId);
+ } else {
+ String sql = "select " + COLUMNS + " from player where username=? and client_id is null";
+ return query(sql, rowMapper, username);
+ }
+ }
+
+ /**
+ * Returns the player with the given ID.
+ *
+ * @param id The unique player ID.
+ * @return The player with the given ID, or <code>null</code> if no such player exists.
+ */
+ public Player getPlayerById(String id) {
+ String sql = "select " + COLUMNS + " from player where id=?";
+ return queryOne(sql, rowMapper, id);
+ }
+
+ /**
+ * Creates a new player.
+ *
+ * @param player The player to create.
+ */
+ public synchronized void createPlayer(Player player) {
+ int id = getJdbcTemplate().queryForInt("select max(id) from player") + 1;
+ player.setId(String.valueOf(id));
+ String sql = "insert into player (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")";
+ update(sql, player.getId(), player.getName(), player.getType(), player.getUsername(),
+ player.getIpAddress(), player.isAutoControlEnabled(),
+ player.getLastSeen(), player.getCoverArtScheme().name(),
+ player.getTranscodeScheme().name(), player.isDynamicIp(),
+ player.getTechnology().name(), player.getClientId());
+ addPlaylist(player);
+
+ LOG.info("Created player " + id + '.');
+ }
+
+ /**
+ * Deletes the player with the given ID.
+ *
+ * @param id The player ID.
+ */
+ public void deletePlayer(String id) {
+ String sql = "delete from player where id=?";
+ update(sql, id);
+ playlists.remove(id);
+ }
+
+
+ /**
+ * Delete players that haven't been used for the given number of days, and which is not given a name
+ * or is used by a REST client.
+ *
+ * @param days Number of days.
+ */
+ public void deleteOldPlayers(int days) {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DATE, -days);
+ String sql = "delete from player where name is null and client_id is null and (last_seen is null or last_seen < ?)";
+ int n = update(sql, cal.getTime());
+ if (n > 0) {
+ LOG.info("Deleted " + n + " player(s) that haven't been used after " + cal.getTime());
+ }
+ }
+
+ /**
+ * Updates the given player.
+ *
+ * @param player The player to update.
+ */
+ public void updatePlayer(Player player) {
+ String sql = "update player set " +
+ "name = ?," +
+ "type = ?," +
+ "username = ?," +
+ "ip_address = ?," +
+ "auto_control_enabled = ?," +
+ "last_seen = ?," +
+ "cover_art_scheme = ?," +
+ "transcode_scheme = ?, " +
+ "dynamic_ip = ?, " +
+ "technology = ?, " +
+ "client_id = ? " +
+ "where id = ?";
+ update(sql, player.getName(), player.getType(), player.getUsername(),
+ player.getIpAddress(), player.isAutoControlEnabled(),
+ player.getLastSeen(), player.getCoverArtScheme().name(),
+ player.getTranscodeScheme().name(), player.isDynamicIp(),
+ player.getTechnology(), player.getClientId(), player.getId());
+ }
+
+ private void addPlaylist(Player player) {
+ PlayQueue playQueue = playlists.get(player.getId());
+ if (playQueue == null) {
+ playQueue = new PlayQueue();
+ playlists.put(player.getId(), playQueue);
+ }
+ player.setPlayQueue(playQueue);
+ }
+
+ private class PlayerRowMapper implements ParameterizedRowMapper<Player> {
+ public Player mapRow(ResultSet rs, int rowNum) throws SQLException {
+ Player player = new Player();
+ int col = 1;
+ player.setId(rs.getString(col++));
+ player.setName(rs.getString(col++));
+ player.setType(rs.getString(col++));
+ player.setUsername(rs.getString(col++));
+ player.setIpAddress(rs.getString(col++));
+ player.setAutoControlEnabled(rs.getBoolean(col++));
+ player.setLastSeen(rs.getTimestamp(col++));
+ player.setCoverArtScheme(CoverArtScheme.valueOf(rs.getString(col++)));
+ player.setTranscodeScheme(TranscodeScheme.valueOf(rs.getString(col++)));
+ player.setDynamicIp(rs.getBoolean(col++));
+ player.setTechnology(PlayerTechnology.valueOf(rs.getString(col++)));
+ player.setClientId(rs.getString(col++));
+
+ addPlaylist(player);
+ return player;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java
new file mode 100644
index 00000000..54cbaded
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java
@@ -0,0 +1,142 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Playlist;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Provides database services for playlists.
+ *
+ * @author Sindre Mehus
+ */
+public class PlaylistDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(PlaylistDao.class);
+ private static final String COLUMNS = "id, username, is_public, name, comment, file_count, duration_seconds, " +
+ "created, changed, imported_from";
+ private final RowMapper rowMapper = new PlaylistMapper();
+
+ public List<Playlist> getReadablePlaylistsForUser(String username) {
+
+ List<Playlist> result1 = getWritablePlaylistsForUser(username);
+ List<Playlist> result2 = query("select " + COLUMNS + " from playlist where is_public", rowMapper);
+ List<Playlist> result3 = query("select " + prefix(COLUMNS, "playlist") + " from playlist, playlist_user where " +
+ "playlist.id = playlist_user.playlist_id and " +
+ "playlist.username != ? and " +
+ "playlist_user.username = ?", rowMapper, username, username);
+
+ // Put in sorted map to avoid duplicates.
+ SortedMap<Integer, Playlist> map = new TreeMap<Integer, Playlist>();
+ for (Playlist playlist : result1) {
+ map.put(playlist.getId(), playlist);
+ }
+ for (Playlist playlist : result2) {
+ map.put(playlist.getId(), playlist);
+ }
+ for (Playlist playlist : result3) {
+ map.put(playlist.getId(), playlist);
+ }
+ return new ArrayList<Playlist>(map.values());
+ }
+
+ public List<Playlist> getWritablePlaylistsForUser(String username) {
+ return query("select " + COLUMNS + " from playlist where username=?", rowMapper, username);
+ }
+
+ public Playlist getPlaylist(int id) {
+ return queryOne("select " + COLUMNS + " from playlist where id=?", rowMapper, id);
+ }
+
+ public List<Playlist> getAllPlaylists() {
+ return query("select " + COLUMNS + " from playlist", rowMapper);
+ }
+
+ public synchronized void createPlaylist(Playlist playlist) {
+ update("insert into playlist(" + COLUMNS + ") values(" + questionMarks(COLUMNS) + ")",
+ null, playlist.getUsername(), playlist.isPublic(), playlist.getName(), playlist.getComment(),
+ 0, 0, playlist.getCreated(), playlist.getChanged(), playlist.getImportedFrom());
+
+ int id = queryForInt("select max(id) from playlist", 0);
+ playlist.setId(id);
+ }
+
+ public void setFilesInPlaylist(int id, List<MediaFile> files) {
+ update("delete from playlist_file where playlist_id=?", id);
+ int duration = 0;
+ for (MediaFile file : files) {
+ update("insert into playlist_file (playlist_id, media_file_id) values (?, ?)", id, file.getId());
+ if (file.getDurationSeconds() != null) {
+ duration += file.getDurationSeconds();
+ }
+ }
+ update("update playlist set file_count=?, duration_seconds=?, changed=? where id=?", files.size(), duration, new Date(), id);
+ }
+
+ public List<String> getPlaylistUsers(int playlistId) {
+ return queryForStrings("select username from playlist_user where playlist_id=?", playlistId);
+ }
+
+ public void addPlaylistUser(int playlistId, String username) {
+ if (!getPlaylistUsers(playlistId).contains(username)) {
+ update("insert into playlist_user(playlist_id,username) values (?,?)", playlistId, username);
+ }
+ }
+
+ public void deletePlaylistUser(int playlistId, String username) {
+ update("delete from playlist_user where playlist_id=? and username=?", playlistId, username);
+ }
+
+ public synchronized void deletePlaylist(int id) {
+ update("delete from playlist where id=?", id);
+ }
+
+ public void updatePlaylist(Playlist playlist) {
+ update("update playlist set username=?, is_public=?, name=?, comment=?, changed=?, imported_from=? where id=?",
+ playlist.getUsername(), playlist.isPublic(), playlist.getName(), playlist.getComment(),
+ new Date(), playlist.getImportedFrom(), playlist.getId());
+ }
+
+ private static class PlaylistMapper implements ParameterizedRowMapper<Playlist> {
+ public Playlist mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Playlist(
+ rs.getInt(1),
+ rs.getString(2),
+ rs.getBoolean(3),
+ rs.getString(4),
+ rs.getString(5),
+ rs.getInt(6),
+ rs.getInt(7),
+ rs.getTimestamp(8),
+ rs.getTimestamp(9),
+ rs.getString(10));
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java
new file mode 100644
index 00000000..3f274ec6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java
@@ -0,0 +1,165 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.domain.PodcastChannel;
+import net.sourceforge.subsonic.domain.PodcastEpisode;
+import net.sourceforge.subsonic.domain.PodcastStatus;
+
+/**
+ * Provides database services for Podcast channels and episodes.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastDao extends AbstractDao {
+
+ private static final String CHANNEL_COLUMNS = "id, url, title, description, status, error_message";
+ private static final String EPISODE_COLUMNS = "id, channel_id, url, path, title, description, publish_date, " +
+ "duration, bytes_total, bytes_downloaded, status, error_message";
+
+ private PodcastChannelRowMapper channelRowMapper = new PodcastChannelRowMapper();
+ private PodcastEpisodeRowMapper episodeRowMapper = new PodcastEpisodeRowMapper();
+
+ /**
+ * Creates a new Podcast channel.
+ *
+ * @param channel The Podcast channel to create.
+ * @return The ID of the newly created channel.
+ */
+ public synchronized int createChannel(PodcastChannel channel) {
+ String sql = "insert into podcast_channel (" + CHANNEL_COLUMNS + ") values (" + questionMarks(CHANNEL_COLUMNS) + ")";
+ update(sql, null, channel.getUrl(), channel.getTitle(), channel.getDescription(),
+ channel.getStatus().name(), channel.getErrorMessage());
+
+ return getJdbcTemplate().queryForInt("select max(id) from podcast_channel");
+ }
+
+ /**
+ * Returns all Podcast channels.
+ *
+ * @return Possibly empty list of all Podcast channels.
+ */
+ public List<PodcastChannel> getAllChannels() {
+ String sql = "select " + CHANNEL_COLUMNS + " from podcast_channel";
+ return query(sql, channelRowMapper);
+ }
+
+ /**
+ * Updates the given Podcast channel.
+ *
+ * @param channel The Podcast channel to update.
+ */
+ public void updateChannel(PodcastChannel channel) {
+ String sql = "update podcast_channel set url=?, title=?, description=?, status=?, error_message=? where id=?";
+ update(sql, channel.getUrl(), channel.getTitle(), channel.getDescription(),
+ channel.getStatus().name(), channel.getErrorMessage(), channel.getId());
+ }
+
+ /**
+ * Deletes the Podcast channel with the given ID.
+ *
+ * @param id The Podcast channel ID.
+ */
+ public void deleteChannel(int id) {
+ String sql = "delete from podcast_channel where id=?";
+ update(sql, id);
+ }
+
+ /**
+ * Creates a new Podcast episode.
+ *
+ * @param episode The Podcast episode to create.
+ */
+ public void createEpisode(PodcastEpisode episode) {
+ String sql = "insert into podcast_episode (" + EPISODE_COLUMNS + ") values (" + questionMarks(EPISODE_COLUMNS) + ")";
+ update(sql, null, episode.getChannelId(), episode.getUrl(), episode.getPath(),
+ episode.getTitle(), episode.getDescription(), episode.getPublishDate(),
+ episode.getDuration(), episode.getBytesTotal(), episode.getBytesDownloaded(),
+ episode.getStatus().name(), episode.getErrorMessage());
+ }
+
+ /**
+ * Returns all Podcast episodes for a given channel.
+ *
+ * @return Possibly empty list of all Podcast episodes for the given channel, sorted in
+ * reverse chronological order (newest episode first).
+ */
+ public List<PodcastEpisode> getEpisodes(int channelId) {
+ String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where channel_id=? order by publish_date desc";
+ return query(sql, episodeRowMapper, channelId);
+ }
+
+ /**
+ * Returns the Podcast episode with the given ID.
+ *
+ * @param episodeId The Podcast episode ID.
+ * @return The episode or <code>null</code> if not found.
+ */
+ public PodcastEpisode getEpisode(int episodeId) {
+ String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where id=?";
+ return queryOne(sql, episodeRowMapper, episodeId);
+ }
+
+ /**
+ * Updates the given Podcast episode.
+ *
+ * @param episode The Podcast episode to update.
+ * @return The number of episodes updated (zero or one).
+ */
+ public int updateEpisode(PodcastEpisode episode) {
+ String sql = "update podcast_episode set url=?, path=?, title=?, description=?, publish_date=?, duration=?, " +
+ "bytes_total=?, bytes_downloaded=?, status=?, error_message=? where id=?";
+ return update(sql, episode.getUrl(), episode.getPath(), episode.getTitle(),
+ episode.getDescription(), episode.getPublishDate(), episode.getDuration(),
+ episode.getBytesTotal(), episode.getBytesDownloaded(), episode.getStatus().name(),
+ episode.getErrorMessage(), episode.getId());
+ }
+
+ /**
+ * Deletes the Podcast episode with the given ID.
+ *
+ * @param id The Podcast episode ID.
+ */
+ public void deleteEpisode(int id) {
+ String sql = "delete from podcast_episode where id=?";
+ update(sql, id);
+ }
+
+ private static class PodcastChannelRowMapper implements RowMapper {
+ public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new PodcastChannel(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4),
+ PodcastStatus.valueOf(rs.getString(5)), rs.getString(6));
+ }
+ }
+
+ private static class PodcastEpisodeRowMapper implements ParameterizedRowMapper<PodcastEpisode> {
+ public PodcastEpisode mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new PodcastEpisode(rs.getInt(1), rs.getInt(2), rs.getString(3), rs.getString(4), rs.getString(5),
+ rs.getString(6), rs.getTimestamp(7), rs.getString(8), (Long) rs.getObject(9),
+ (Long) rs.getObject(10), PodcastStatus.valueOf(rs.getString(11)), rs.getString(12));
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java
new file mode 100644
index 00000000..221fe889
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java
@@ -0,0 +1,99 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides database services for ratings.
+ *
+ * @author Sindre Mehus
+ */
+public class RatingDao extends AbstractDao {
+
+ /**
+ * Returns paths for the highest rated music files.
+ *
+ * @param offset Number of files to skip.
+ * @param count Maximum number of files to return.
+ * @return Paths for the highest rated music files.
+ */
+ public List<String> getHighestRated(int offset, int count) {
+ if (count < 1) {
+ return new ArrayList<String>();
+ }
+
+ String sql = "select user_rating.path from user_rating, media_file " +
+ "where user_rating.path=media_file.path and media_file.present " +
+ "group by path " +
+ "order by avg(rating) desc limit " + count + " offset " + offset;
+ return queryForStrings(sql);
+ }
+
+ /**
+ * Sets the rating for a media file and a given user.
+ *
+ * @param username The user name.
+ * @param mediaFile The media file.
+ * @param rating The rating between 1 and 5, or <code>null</code> to remove the rating.
+ */
+ public void setRatingForUser(String username, MediaFile mediaFile, Integer rating) {
+ if (rating != null && (rating < 1 || rating > 5)) {
+ return;
+ }
+
+ update("delete from user_rating where username=? and path=?", username, mediaFile.getPath());
+ if (rating != null) {
+ update("insert into user_rating values(?, ?, ?)", username, mediaFile.getPath(), rating);
+ }
+ }
+
+ /**
+ * Returns the average rating for the given media file.
+ *
+ * @param mediaFile The media file.
+ * @return The average rating, or <code>null</code> if no ratings are set.
+ */
+ public Double getAverageRating(MediaFile mediaFile) {
+ try {
+ return (Double) getJdbcTemplate().queryForObject("select avg(rating) from user_rating where path=?", new Object[]{mediaFile.getPath()}, Double.class);
+ } catch (EmptyResultDataAccessException x) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the rating for the given user and media file.
+ *
+ * @param username The user name.
+ * @param mediaFile The media file.
+ * @return The rating, or <code>null</code> if no rating is set.
+ */
+ public Integer getRatingForUser(String username, MediaFile mediaFile) {
+ try {
+ return getJdbcTemplate().queryForInt("select rating from user_rating where username=? and path=?", new Object[]{username, mediaFile.getPath()});
+ } catch (EmptyResultDataAccessException x) {
+ return null;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java
new file mode 100644
index 00000000..17d4cd73
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java
@@ -0,0 +1,131 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.domain.Share;
+
+/**
+ * Provides database services for shared media.
+ *
+ * @author Sindre Mehus
+ */
+public class ShareDao extends AbstractDao {
+
+ private static final String COLUMNS = "id, name, description, username, created, expires, last_visited, visit_count";
+
+ private ShareRowMapper shareRowMapper = new ShareRowMapper();
+ private ShareFileRowMapper shareFileRowMapper = new ShareFileRowMapper();
+
+ /**
+ * Creates a new share.
+ *
+ * @param share The share to create. The ID of the share will be set by this method.
+ */
+ public synchronized void createShare(Share share) {
+ String sql = "insert into share (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")";
+ update(sql, null, share.getName(), share.getDescription(), share.getUsername(), share.getCreated(),
+ share.getExpires(), share.getLastVisited(), share.getVisitCount());
+
+ int id = getJdbcTemplate().queryForInt("select max(id) from share");
+ share.setId(id);
+ }
+
+ /**
+ * Returns all shares.
+ *
+ * @return Possibly empty list of all shares.
+ */
+ public List<Share> getAllShares() {
+ String sql = "select " + COLUMNS + " from share";
+ return query(sql, shareRowMapper);
+ }
+
+ public Share getShareByName(String shareName) {
+ String sql = "select " + COLUMNS + " from share where name=?";
+ return queryOne(sql, shareRowMapper, shareName);
+ }
+
+ public Share getShareById(int id) {
+ String sql = "select " + COLUMNS + " from share where id=?";
+ return queryOne(sql, shareRowMapper, id);
+ }
+
+ /**
+ * Updates the given share.
+ *
+ * @param share The share to update.
+ */
+ public void updateShare(Share share) {
+ String sql = "update share set name=?, description=?, username=?, created=?, expires=?, last_visited=?, visit_count=? where id=?";
+ update(sql, share.getName(), share.getDescription(), share.getUsername(), share.getCreated(), share.getExpires(),
+ share.getLastVisited(), share.getVisitCount(), share.getId());
+ }
+
+ /**
+ * Creates shared files.
+ *
+ * @param shareId The share ID.
+ * @param paths Paths of the files to share.
+ */
+ public void createSharedFiles(int shareId, String... paths) {
+ String sql = "insert into share_file (share_id, path) values (?, ?)";
+ for (String path : paths) {
+ update(sql, shareId, path);
+ }
+ }
+
+ /**
+ * Returns files for a share.
+ *
+ * @param shareId The ID of the share.
+ * @return The paths of the shared files.
+ */
+ public List<String> getSharedFiles(int shareId) {
+ return query("select path from share_file where share_id=?", shareFileRowMapper, shareId);
+ }
+
+ /**
+ * Deletes the share with the given ID.
+ *
+ * @param id The ID of the share to delete.
+ */
+ public void deleteShare(Integer id) {
+ update("delete from share where id=?", id);
+ }
+
+ private static class ShareRowMapper implements ParameterizedRowMapper<Share> {
+ public Share mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Share(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getTimestamp(5),
+ rs.getTimestamp(6), rs.getTimestamp(7), rs.getInt(8));
+ }
+ }
+
+ private static class ShareFileRowMapper implements ParameterizedRowMapper<String> {
+ public String mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return rs.getString(1);
+ }
+
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.java
new file mode 100644
index 00000000..22b8ae20
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.java
@@ -0,0 +1,123 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Transcoding;
+
+/**
+ * Provides database services for transcoding configurations.
+ *
+ * @author Sindre Mehus
+ */
+public class TranscodingDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(TranscodingDao.class);
+ private static final String COLUMNS = "id, name, source_formats, target_format, step1, step2, step3, default_active";
+ private TranscodingRowMapper rowMapper = new TranscodingRowMapper();
+
+ /**
+ * Returns all transcodings.
+ *
+ * @return Possibly empty list of all transcodings.
+ */
+ public List<Transcoding> getAllTranscodings() {
+ String sql = "select " + COLUMNS + " from transcoding2";
+ return query(sql, rowMapper);
+ }
+
+ /**
+ * Returns all active transcodings for the given player.
+ *
+ * @param playerId The player ID.
+ * @return All active transcodings for the player.
+ */
+ public List<Transcoding> getTranscodingsForPlayer(String playerId) {
+ String sql = "select " + COLUMNS + " from transcoding2, player_transcoding2 " +
+ "where player_transcoding2.player_id = ? " +
+ "and player_transcoding2.transcoding_id = transcoding2.id";
+ return query(sql, rowMapper, playerId);
+ }
+
+ /**
+ * Sets the list of active transcodings for the given player.
+ *
+ * @param playerId The player ID.
+ * @param transcodingIds ID's of the active transcodings.
+ */
+ public void setTranscodingsForPlayer(String playerId, int[] transcodingIds) {
+ update("delete from player_transcoding2 where player_id = ?", playerId);
+ String sql = "insert into player_transcoding2(player_id, transcoding_id) values (?, ?)";
+ for (int transcodingId : transcodingIds) {
+ update(sql, playerId, transcodingId);
+ }
+ }
+
+ /**
+ * Creates a new transcoding.
+ *
+ * @param transcoding The transcoding to create.
+ */
+ public synchronized void createTranscoding(Transcoding transcoding) {
+ int id = getJdbcTemplate().queryForInt("select max(id) + 1 from transcoding2");
+ transcoding.setId(id);
+ String sql = "insert into transcoding2 (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")";
+ update(sql, transcoding.getId(), transcoding.getName(), transcoding.getSourceFormats(),
+ transcoding.getTargetFormat(), transcoding.getStep1(),
+ transcoding.getStep2(), transcoding.getStep3(), transcoding.isDefaultActive());
+ LOG.info("Created transcoding " + transcoding.getName());
+ }
+
+ /**
+ * Deletes the transcoding with the given ID.
+ *
+ * @param id The transcoding ID.
+ */
+ public void deleteTranscoding(Integer id) {
+ String sql = "delete from transcoding2 where id=?";
+ update(sql, id);
+ LOG.info("Deleted transcoding with ID " + id);
+ }
+
+ /**
+ * Updates the given transcoding.
+ *
+ * @param transcoding The transcoding to update.
+ */
+ public void updateTranscoding(Transcoding transcoding) {
+ String sql = "update transcoding2 set name=?, source_formats=?, target_format=?, " +
+ "step1=?, step2=?, step3=?, default_active=? where id=?";
+ update(sql, transcoding.getName(), transcoding.getSourceFormats(),
+ transcoding.getTargetFormat(), transcoding.getStep1(), transcoding.getStep2(),
+ transcoding.getStep3(), transcoding.isDefaultActive(), transcoding.getId());
+ }
+
+ private static class TranscodingRowMapper implements ParameterizedRowMapper<Transcoding> {
+ public Transcoding mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Transcoding(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5),
+ rs.getString(6), rs.getString(7), rs.getBoolean(8));
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java
new file mode 100644
index 00000000..e7807765
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java
@@ -0,0 +1,352 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.AvatarScheme;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Provides user-related database services.
+ *
+ * @author Sindre Mehus
+ */
+public class UserDao extends AbstractDao {
+
+ private static final Logger LOG = Logger.getLogger(UserDao.class);
+ private static final String USER_COLUMNS = "username, password, email, ldap_authenticated, bytes_streamed, bytes_downloaded, bytes_uploaded";
+ private static final String USER_SETTINGS_COLUMNS = "username, locale, theme_id, final_version_notification, beta_version_notification, " +
+ "main_caption_cutoff, main_track_number, main_artist, main_album, main_genre, " +
+ "main_year, main_bit_rate, main_duration, main_format, main_file_size, " +
+ "playlist_caption_cutoff, playlist_track_number, playlist_artist, playlist_album, playlist_genre, " +
+ "playlist_year, playlist_bit_rate, playlist_duration, playlist_format, playlist_file_size, " +
+ "last_fm_enabled, last_fm_username, last_fm_password, transcode_scheme, show_now_playing, selected_music_folder_id, " +
+ "party_mode_enabled, now_playing_allowed, avatar_scheme, system_avatar_id, changed, show_chat";
+
+ private static final Integer ROLE_ID_ADMIN = 1;
+ private static final Integer ROLE_ID_DOWNLOAD = 2;
+ private static final Integer ROLE_ID_UPLOAD = 3;
+ private static final Integer ROLE_ID_PLAYLIST = 4;
+ private static final Integer ROLE_ID_COVER_ART = 5;
+ private static final Integer ROLE_ID_COMMENT = 6;
+ private static final Integer ROLE_ID_PODCAST = 7;
+ private static final Integer ROLE_ID_STREAM = 8;
+ private static final Integer ROLE_ID_SETTINGS = 9;
+ private static final Integer ROLE_ID_JUKEBOX = 10;
+ private static final Integer ROLE_ID_SHARE = 11;
+
+ private UserRowMapper userRowMapper = new UserRowMapper();
+ private UserSettingsRowMapper userSettingsRowMapper = new UserSettingsRowMapper();
+
+ /**
+ * Returns the user with the given username.
+ *
+ * @param username The username used when logging in.
+ * @return The user, or <code>null</code> if not found.
+ */
+ public User getUserByName(String username) {
+ String sql = "select " + USER_COLUMNS + " from user where username=?";
+ return queryOne(sql, userRowMapper, username);
+ }
+
+ /**
+ * Returns the user with the given email address.
+ *
+ * @param email The email address.
+ * @return The user, or <code>null</code> if not found.
+ */
+ public User getUserByEmail(String email) {
+ String sql = "select " + USER_COLUMNS + " from user where email=?";
+ return queryOne(sql, userRowMapper, email);
+ }
+
+ /**
+ * Returns all users.
+ *
+ * @return Possibly empty array of all users.
+ */
+ public List<User> getAllUsers() {
+ String sql = "select " + USER_COLUMNS + " from user";
+ return query(sql, userRowMapper);
+ }
+
+ /**
+ * Creates a new user.
+ *
+ * @param user The user to create.
+ */
+ public void createUser(User user) {
+ String sql = "insert into user (" + USER_COLUMNS + ") values (" + questionMarks(USER_COLUMNS) + ')';
+ update(sql, user.getUsername(), encrypt(user.getPassword()), user.getEmail(), user.isLdapAuthenticated(),
+ user.getBytesStreamed(), user.getBytesDownloaded(), user.getBytesUploaded());
+ writeRoles(user);
+ }
+
+ /**
+ * Deletes the user with the given username.
+ *
+ * @param username The username.
+ */
+ public void deleteUser(String username) {
+ if (User.USERNAME_ADMIN.equals(username)) {
+ throw new IllegalArgumentException("Can't delete admin user.");
+ }
+
+ String sql = "delete from user_role where username=?";
+ update(sql, username);
+
+ sql = "delete from user where username=?";
+ update(sql, username);
+ }
+
+ /**
+ * Updates the given user.
+ *
+ * @param user The user to update.
+ */
+ public void updateUser(User user) {
+ String sql = "update user set password=?, email=?, ldap_authenticated=?, bytes_streamed=?, bytes_downloaded=?, bytes_uploaded=? " +
+ "where username=?";
+ getJdbcTemplate().update(sql, new Object[]{encrypt(user.getPassword()), user.getEmail(), user.isLdapAuthenticated(),
+ user.getBytesStreamed(), user.getBytesDownloaded(), user.getBytesUploaded(),
+ user.getUsername()});
+ writeRoles(user);
+ }
+
+ /**
+ * Returns the name of the roles for the given user.
+ *
+ * @param username The user name.
+ * @return Roles the user is granted.
+ */
+ public String[] getRolesForUser(String username) {
+ String sql = "select r.name from role r, user_role ur " +
+ "where ur.username=? and ur.role_id=r.id";
+ List<?> roles = getJdbcTemplate().queryForList(sql, new Object[]{username}, String.class);
+ String[] result = new String[roles.size()];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = (String) roles.get(i);
+ }
+ return result;
+ }
+
+ /**
+ * Returns settings for the given user.
+ *
+ * @param username The username.
+ * @return User-specific settings, or <code>null</code> if no such settings exist.
+ */
+ public UserSettings getUserSettings(String username) {
+ String sql = "select " + USER_SETTINGS_COLUMNS + " from user_settings where username=?";
+ return queryOne(sql, userSettingsRowMapper, username);
+ }
+
+ /**
+ * Updates settings for the given username, creating it if necessary.
+ *
+ * @param settings The user-specific settings.
+ */
+ public void updateUserSettings(UserSettings settings) {
+ getJdbcTemplate().update("delete from user_settings where username=?", new Object[]{settings.getUsername()});
+
+ String sql = "insert into user_settings (" + USER_SETTINGS_COLUMNS + ") values (" + questionMarks(USER_SETTINGS_COLUMNS) + ')';
+ String locale = settings.getLocale() == null ? null : settings.getLocale().toString();
+ UserSettings.Visibility main = settings.getMainVisibility();
+ UserSettings.Visibility playlist = settings.getPlaylistVisibility();
+ getJdbcTemplate().update(sql, new Object[]{settings.getUsername(), locale, settings.getThemeId(),
+ settings.isFinalVersionNotificationEnabled(), settings.isBetaVersionNotificationEnabled(),
+ main.getCaptionCutoff(), main.isTrackNumberVisible(), main.isArtistVisible(), main.isAlbumVisible(),
+ main.isGenreVisible(), main.isYearVisible(), main.isBitRateVisible(), main.isDurationVisible(),
+ main.isFormatVisible(), main.isFileSizeVisible(),
+ playlist.getCaptionCutoff(), playlist.isTrackNumberVisible(), playlist.isArtistVisible(), playlist.isAlbumVisible(),
+ playlist.isGenreVisible(), playlist.isYearVisible(), playlist.isBitRateVisible(), playlist.isDurationVisible(),
+ playlist.isFormatVisible(), playlist.isFileSizeVisible(),
+ settings.isLastFmEnabled(), settings.getLastFmUsername(), encrypt(settings.getLastFmPassword()),
+ settings.getTranscodeScheme().name(), settings.isShowNowPlayingEnabled(),
+ settings.getSelectedMusicFolderId(), settings.isPartyModeEnabled(), settings.isNowPlayingAllowed(),
+ settings.getAvatarScheme().name(), settings.getSystemAvatarId(), settings.getChanged(), settings.isShowChatEnabled()});
+ }
+
+ private static String encrypt(String s) {
+ if (s == null) {
+ return null;
+ }
+ try {
+ return "enc:" + StringUtil.utf8HexEncode(s);
+ } catch (Exception e) {
+ return s;
+ }
+ }
+
+ private static String decrypt(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (!s.startsWith("enc:")) {
+ return s;
+ }
+ try {
+ return StringUtil.utf8HexDecode(s.substring(4));
+ } catch (Exception e) {
+ return s;
+ }
+ }
+
+ private void readRoles(User user) {
+ synchronized (user.getUsername().intern()) {
+ String sql = "select role_id from user_role where username=?";
+ List<?> roles = getJdbcTemplate().queryForList(sql, new Object[]{user.getUsername()}, Integer.class);
+ for (Object role : roles) {
+ if (ROLE_ID_ADMIN.equals(role)) {
+ user.setAdminRole(true);
+ } else if (ROLE_ID_DOWNLOAD.equals(role)) {
+ user.setDownloadRole(true);
+ } else if (ROLE_ID_UPLOAD.equals(role)) {
+ user.setUploadRole(true);
+ } else if (ROLE_ID_PLAYLIST.equals(role)) {
+ user.setPlaylistRole(true);
+ } else if (ROLE_ID_COVER_ART.equals(role)) {
+ user.setCoverArtRole(true);
+ } else if (ROLE_ID_COMMENT.equals(role)) {
+ user.setCommentRole(true);
+ } else if (ROLE_ID_PODCAST.equals(role)) {
+ user.setPodcastRole(true);
+ } else if (ROLE_ID_STREAM.equals(role)) {
+ user.setStreamRole(true);
+ } else if (ROLE_ID_SETTINGS.equals(role)) {
+ user.setSettingsRole(true);
+ } else if (ROLE_ID_JUKEBOX.equals(role)) {
+ user.setJukeboxRole(true);
+ } else if (ROLE_ID_SHARE.equals(role)) {
+ user.setShareRole(true);
+ } else {
+ LOG.warn("Unknown role: '" + role + '\'');
+ }
+ }
+ }
+ }
+
+ private void writeRoles(User user) {
+ synchronized (user.getUsername().intern()) {
+ String sql = "delete from user_role where username=?";
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername()});
+ sql = "insert into user_role (username, role_id) values(?, ?)";
+ if (user.isAdminRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_ADMIN});
+ }
+ if (user.isDownloadRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_DOWNLOAD});
+ }
+ if (user.isUploadRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_UPLOAD});
+ }
+ if (user.isPlaylistRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_PLAYLIST});
+ }
+ if (user.isCoverArtRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_COVER_ART});
+ }
+ if (user.isCommentRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_COMMENT});
+ }
+ if (user.isPodcastRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_PODCAST});
+ }
+ if (user.isStreamRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_STREAM});
+ }
+ if (user.isJukeboxRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_JUKEBOX});
+ }
+ if (user.isSettingsRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_SETTINGS});
+ }
+ if (user.isShareRole()) {
+ getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_SHARE});
+ }
+ }
+ }
+
+ private class UserRowMapper implements ParameterizedRowMapper<User> {
+ public User mapRow(ResultSet rs, int rowNum) throws SQLException {
+ User user = new User(rs.getString(1), decrypt(rs.getString(2)), rs.getString(3), rs.getBoolean(4),
+ rs.getLong(5), rs.getLong(6), rs.getLong(7));
+ readRoles(user);
+ return user;
+ }
+ }
+
+ private static class UserSettingsRowMapper implements ParameterizedRowMapper<UserSettings> {
+ public UserSettings mapRow(ResultSet rs, int rowNum) throws SQLException {
+ int col = 1;
+ UserSettings settings = new UserSettings(rs.getString(col++));
+ settings.setLocale(StringUtil.parseLocale(rs.getString(col++)));
+ settings.setThemeId(rs.getString(col++));
+ settings.setFinalVersionNotificationEnabled(rs.getBoolean(col++));
+ settings.setBetaVersionNotificationEnabled(rs.getBoolean(col++));
+
+ settings.getMainVisibility().setCaptionCutoff(rs.getInt(col++));
+ settings.getMainVisibility().setTrackNumberVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setArtistVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setAlbumVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setGenreVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setYearVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setBitRateVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setDurationVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setFormatVisible(rs.getBoolean(col++));
+ settings.getMainVisibility().setFileSizeVisible(rs.getBoolean(col++));
+
+ settings.getPlaylistVisibility().setCaptionCutoff(rs.getInt(col++));
+ settings.getPlaylistVisibility().setTrackNumberVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setArtistVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setAlbumVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setGenreVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setYearVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setBitRateVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setDurationVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setFormatVisible(rs.getBoolean(col++));
+ settings.getPlaylistVisibility().setFileSizeVisible(rs.getBoolean(col++));
+
+ settings.setLastFmEnabled(rs.getBoolean(col++));
+ settings.setLastFmUsername(rs.getString(col++));
+ settings.setLastFmPassword(decrypt(rs.getString(col++)));
+
+ settings.setTranscodeScheme(TranscodeScheme.valueOf(rs.getString(col++)));
+ settings.setShowNowPlayingEnabled(rs.getBoolean(col++));
+ settings.setSelectedMusicFolderId(rs.getInt(col++));
+ settings.setPartyModeEnabled(rs.getBoolean(col++));
+ settings.setNowPlayingAllowed(rs.getBoolean(col++));
+ settings.setAvatarScheme(AvatarScheme.valueOf(rs.getString(col++)));
+ settings.setSystemAvatarId((Integer) rs.getObject(col++));
+ settings.setChanged(rs.getTimestamp(col++));
+ settings.setShowChatEnabled(rs.getBoolean(col++));
+
+ return settings;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.java
new file mode 100644
index 00000000..674f85ca
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.java
@@ -0,0 +1,66 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import org.springframework.jdbc.core.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ *
+ * @author Sindre Mehus
+ */
+public abstract class Schema {
+
+ /**
+ * Executes this schema.
+ * @param template The JDBC template to use.
+ */
+ public abstract void execute(JdbcTemplate template);
+
+ /**
+ * Returns whether the given table exists.
+ * @param template The JDBC template to use.
+ * @param table The table in question.
+ * @return Whether the table exists.
+ */
+ protected boolean tableExists(JdbcTemplate template, String table) {
+ try {
+ template.execute("select 1 from " + table);
+ } catch (Exception x) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether the given column in the given table exists.
+ * @param template The JDBC template to use.
+ * @param column The column in question.
+ * @param table The table in question.
+ * @return Whether the column exists.
+ */
+ protected boolean columnExists(JdbcTemplate template, String column, String table) {
+ try {
+ template.execute("select " + column + " from " + table + " where 1 = 0");
+ } catch (Exception x) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java
new file mode 100644
index 00000000..33cc2525
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java
@@ -0,0 +1,81 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import org.springframework.jdbc.core.*;
+import net.sourceforge.subsonic.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 2.5.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema25 extends Schema{
+ private static final Logger LOG = Logger.getLogger(Schema25.class);
+
+ public void execute(JdbcTemplate template) {
+ if (!tableExists(template, "version")) {
+ LOG.info("Database table 'version' not found. Creating it.");
+ template.execute("create table version (version int not null)");
+ template.execute("insert into version values (1)");
+ LOG.info("Database table 'version' was created successfully.");
+ }
+
+ if (!tableExists(template, "role")) {
+ LOG.info("Database table 'role' not found. Creating it.");
+ template.execute("create table role (" +
+ "id int not null," +
+ "name varchar not null," +
+ "primary key (id))");
+ template.execute("insert into role values (1, 'admin')");
+ template.execute("insert into role values (2, 'download')");
+ template.execute("insert into role values (3, 'upload')");
+ template.execute("insert into role values (4, 'playlist')");
+ template.execute("insert into role values (5, 'coverart')");
+ LOG.info("Database table 'role' was created successfully.");
+ }
+
+ if (!tableExists(template, "user")) {
+ LOG.info("Database table 'user' not found. Creating it.");
+ template.execute("create table user (" +
+ "username varchar not null," +
+ "password varchar not null," +
+ "primary key (username))");
+ template.execute("insert into user values ('admin', 'admin')");
+ LOG.info("Database table 'user' was created successfully.");
+ }
+
+ if (!tableExists(template, "user_role")) {
+ LOG.info("Database table 'user_role' not found. Creating it.");
+ template.execute("create table user_role (" +
+ "username varchar not null," +
+ "role_id int not null," +
+ "primary key (username, role_id)," +
+ "foreign key (username) references user(username)," +
+ "foreign key (role_id) references role(id))");
+ template.execute("insert into user_role values ('admin', 1)");
+ template.execute("insert into user_role values ('admin', 2)");
+ template.execute("insert into user_role values ('admin', 3)");
+ template.execute("insert into user_role values ('admin', 4)");
+ template.execute("insert into user_role values ('admin', 5)");
+ LOG.info("Database table 'user_role' was created successfully.");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.java
new file mode 100644
index 00000000..6d60b29b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.java
@@ -0,0 +1,110 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.*;
+import net.sourceforge.subsonic.util.Util;
+import org.springframework.jdbc.core.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 2.6.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema26 extends Schema{
+ private static final Logger LOG = Logger.getLogger(Schema26.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 2") == 0) {
+ LOG.info("Updating database schema to version 2.");
+ template.execute("insert into version values (2)");
+ }
+
+ if (!tableExists(template, "music_folder")) {
+ LOG.info("Database table 'music_folder' not found. Creating it.");
+ template.execute("create table music_folder (" +
+ "id identity," +
+ "path varchar not null," +
+ "name varchar not null," +
+ "enabled boolean not null)");
+ template.execute("insert into music_folder values (null, '" + Util.getDefaultMusicFolder() + "', 'Music', true)");
+ LOG.info("Database table 'music_folder' was created successfully.");
+ }
+
+ if (!tableExists(template, "music_file_info")) {
+ LOG.info("Database table 'music_file_info' not found. Creating it.");
+ template.execute("create cached table music_file_info (" +
+ "id identity," +
+ "path varchar not null," +
+ "rating int," +
+ "comment varchar," +
+ "play_count int," +
+ "last_played datetime)");
+ template.execute("create index idx_music_file_info_path on music_file_info(path)");
+ LOG.info("Database table 'music_file_info' was created successfully.");
+ }
+
+ if (!tableExists(template, "internet_radio")) {
+ LOG.info("Database table 'internet_radio' not found. Creating it.");
+ template.execute("create table internet_radio (" +
+ "id identity," +
+ "name varchar not null," +
+ "stream_url varchar not null," +
+ "homepage_url varchar," +
+ "enabled boolean not null)");
+ LOG.info("Database table 'internet_radio' was created successfully.");
+ }
+
+ if (!tableExists(template, "player")) {
+ LOG.info("Database table 'player' not found. Creating it.");
+ template.execute("create table player (" +
+ "id int not null," +
+ "name varchar," +
+ "type varchar," +
+ "username varchar," +
+ "ip_address varchar," +
+ "auto_control_enabled boolean not null," +
+ "last_seen datetime," +
+ "cover_art_scheme varchar not null," +
+ "transcode_scheme varchar not null," +
+ "primary key (id))");
+ LOG.info("Database table 'player' was created successfully.");
+ }
+
+ // 'dynamic_ip' was added in 2.6.beta2
+ if (!columnExists(template, "dynamic_ip", "player")) {
+ LOG.info("Database column 'player.dynamic_ip' not found. Creating it.");
+ template.execute("alter table player " +
+ "add dynamic_ip boolean default true not null");
+ LOG.info("Database column 'player.dynamic_ip' was added successfully.");
+ }
+
+ if (template.queryForInt("select count(*) from role where id = 6") == 0) {
+ LOG.info("Role 'comment' not found in database. Creating it.");
+ template.execute("insert into role values (6, 'comment')");
+ template.execute("insert into user_role " +
+ "select distinct u.username, 6 from user u, user_role ur " +
+ "where u.username = ur.username and ur.role_id in (1, 5)");
+ LOG.info("Role 'comment' was created successfully.");
+ }
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java
new file mode 100644
index 00000000..4057622e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.*;
+import org.springframework.jdbc.core.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 2.7.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema27 extends Schema{
+ private static final Logger LOG = Logger.getLogger(Schema27.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 3") == 0) {
+ LOG.info("Updating database schema to version 3.");
+ template.execute("insert into version values (3)");
+
+ LOG.info("Converting database column 'music_file_info.path' to varchar_ignorecase.");
+ template.execute("drop index idx_music_file_info_path");
+ template.execute("alter table music_file_info alter column path varchar_ignorecase not null");
+ template.execute("create index idx_music_file_info_path on music_file_info(path)");
+ LOG.info("Database column 'music_file_info.path' was converted successfully.");
+ }
+
+ if (!columnExists(template, "bytes_streamed", "user")) {
+ LOG.info("Database columns 'user.bytes_streamed/downloaded/uploaded' not found. Creating them.");
+ template.execute("alter table user add bytes_streamed bigint default 0 not null");
+ template.execute("alter table user add bytes_downloaded bigint default 0 not null");
+ template.execute("alter table user add bytes_uploaded bigint default 0 not null");
+ LOG.info("Database columns 'user.bytes_streamed/downloaded/uploaded' were added successfully.");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.java
new file mode 100644
index 00000000..dbee6730
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.java
@@ -0,0 +1,110 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.*;
+import org.springframework.jdbc.core.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 2.8.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema28 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema28.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 4") == 0) {
+ LOG.info("Updating database schema to version 4.");
+ template.execute("insert into version values (4)");
+ }
+
+ if (!tableExists(template, "user_settings")) {
+ LOG.info("Database table 'user_settings' not found. Creating it.");
+ template.execute("create table user_settings (" +
+ "username varchar not null," +
+ "locale varchar," +
+ "theme_id varchar," +
+ "final_version_notification boolean default true not null," +
+ "beta_version_notification boolean default false not null," +
+ "main_caption_cutoff int default 35 not null," +
+ "main_track_number boolean default true not null," +
+ "main_artist boolean default true not null," +
+ "main_album boolean default false not null," +
+ "main_genre boolean default false not null," +
+ "main_year boolean default false not null," +
+ "main_bit_rate boolean default false not null," +
+ "main_duration boolean default true not null," +
+ "main_format boolean default false not null," +
+ "main_file_size boolean default false not null," +
+ "playlist_caption_cutoff int default 35 not null," +
+ "playlist_track_number boolean default false not null," +
+ "playlist_artist boolean default true not null," +
+ "playlist_album boolean default true not null," +
+ "playlist_genre boolean default false not null," +
+ "playlist_year boolean default true not null," +
+ "playlist_bit_rate boolean default false not null," +
+ "playlist_duration boolean default true not null," +
+ "playlist_format boolean default true not null," +
+ "playlist_file_size boolean default true not null," +
+ "primary key (username)," +
+ "foreign key (username) references user(username) on delete cascade)");
+ LOG.info("Database table 'user_settings' was created successfully.");
+ }
+
+ if (!tableExists(template, "transcoding")) {
+ LOG.info("Database table 'transcoding' not found. Creating it.");
+ template.execute("create table transcoding (" +
+ "id identity," +
+ "name varchar not null," +
+ "source_format varchar not null," +
+ "target_format varchar not null," +
+ "step1 varchar not null," +
+ "step2 varchar," +
+ "step3 varchar," +
+ "enabled boolean not null)");
+
+ template.execute("insert into transcoding values(null,'wav > mp3', 'wav', 'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'flac > mp3','flac','mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'ogg > mp3' ,'ogg' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'wma > mp3' ,'wma' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'m4a > mp3' ,'m4a' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,false)");
+ template.execute("insert into transcoding values(null,'aac > mp3' ,'aac' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,false)");
+ template.execute("insert into transcoding values(null,'ape > mp3' ,'ape' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'mpc > mp3' ,'mpc' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'mv > mp3' ,'mv' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+ template.execute("insert into transcoding values(null,'shn > mp3' ,'shn' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)");
+
+ LOG.info("Database table 'transcoding' was created successfully.");
+ }
+
+ if (!tableExists(template, "player_transcoding")) {
+ LOG.info("Database table 'player_transcoding' not found. Creating it.");
+ template.execute("create table player_transcoding (" +
+ "player_id int not null," +
+ "transcoding_id int not null," +
+ "primary key (player_id, transcoding_id)," +
+ "foreign key (player_id) references player(id) on delete cascade," +
+ "foreign key (transcoding_id) references transcoding(id) on delete cascade)");
+ LOG.info("Database table 'player_transcoding' was created successfully.");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java
new file mode 100644
index 00000000..dd4748d1
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java
@@ -0,0 +1,55 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.*;
+import org.springframework.jdbc.core.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 2.9.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema29 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema29.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 5") == 0) {
+ LOG.info("Updating database schema to version 5.");
+ template.execute("insert into version values (5)");
+ }
+
+ if (!tableExists(template, "user_rating")) {
+ LOG.info("Database table 'user_rating' not found. Creating it.");
+ template.execute("create table user_rating (" +
+ "username varchar not null," +
+ "path varchar not null," +
+ "rating double not null," +
+ "primary key (username, path)," +
+ "foreign key (username) references user(username) on delete cascade)");
+ LOG.info("Database table 'user_rating' was created successfully.");
+
+ template.execute("insert into user_rating select 'admin', path, rating from music_file_info " +
+ "where rating is not null and rating > 0");
+ LOG.info("Migrated data from 'music_file_info' to 'user_rating'.");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java
new file mode 100644
index 00000000..cdea199b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java
@@ -0,0 +1,56 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.*;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import org.springframework.jdbc.core.*;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.0.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema30 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema30.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 6") == 0) {
+ LOG.info("Updating database schema to version 6.");
+ template.execute("insert into version values (6)");
+ }
+
+ if (!columnExists(template, "last_fm_enabled", "user_settings")) {
+ LOG.info("Database columns 'user_settings.last_fm_*' not found. Creating them.");
+ template.execute("alter table user_settings add last_fm_enabled boolean default false not null");
+ template.execute("alter table user_settings add last_fm_username varchar null");
+ template.execute("alter table user_settings add last_fm_password varchar null");
+ LOG.info("Database columns 'user_settings.last_fm_*' were added successfully.");
+ }
+
+ if (!columnExists(template, "transcode_scheme", "user_settings")) {
+ LOG.info("Database column 'user_settings.transcode_scheme' not found. Creating it.");
+ template.execute("alter table user_settings add transcode_scheme varchar default '" +
+ TranscodeScheme.OFF.name() + "' not null");
+ LOG.info("Database column 'user_settings.transcode_scheme' was added successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.java
new file mode 100644
index 00000000..00fb0c87
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.java
@@ -0,0 +1,52 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.1.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema31 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema31.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 7") == 0) {
+ LOG.info("Updating database schema to version 7.");
+ template.execute("insert into version values (7)");
+ }
+
+ if (!columnExists(template, "enabled", "music_file_info")) {
+ LOG.info("Database column 'music_file_info.enabled' not found. Creating it.");
+ template.execute("alter table music_file_info add enabled boolean default true not null");
+ LOG.info("Database column 'music_file_info.enabled' was added successfully.");
+ }
+
+ if (!columnExists(template, "default_active", "transcoding")) {
+ LOG.info("Database column 'transcoding.default_active' not found. Creating it.");
+ template.execute("alter table transcoding add default_active boolean default true not null");
+ LOG.info("Database column 'transcoding.default_active' was added successfully.");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.java
new file mode 100644
index 00000000..a1439bb0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.java
@@ -0,0 +1,93 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.2.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema32 extends Schema {
+ private static final Logger LOG = Logger.getLogger(Schema32.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 8") == 0) {
+ LOG.info("Updating database schema to version 8.");
+ template.execute("insert into version values (8)");
+ }
+
+ if (!columnExists(template, "show_now_playing", "user_settings")) {
+ LOG.info("Database column 'user_settings.show_now_playing' not found. Creating it.");
+ template.execute("alter table user_settings add show_now_playing boolean default true not null");
+ LOG.info("Database column 'user_settings.show_now_playing' was added successfully.");
+ }
+
+ if (!columnExists(template, "selected_music_folder_id", "user_settings")) {
+ LOG.info("Database column 'user_settings.selected_music_folder_id' not found. Creating it.");
+ template.execute("alter table user_settings add selected_music_folder_id int default -1 not null");
+ LOG.info("Database column 'user_settings.selected_music_folder_id' was added successfully.");
+ }
+
+ if (!tableExists(template, "podcast_channel")) {
+ LOG.info("Database table 'podcast_channel' not found. Creating it.");
+ template.execute("create table podcast_channel (" +
+ "id identity," +
+ "url varchar not null," +
+ "title varchar," +
+ "description varchar," +
+ "status varchar not null," +
+ "error_message varchar)");
+ LOG.info("Database table 'podcast_channel' was created successfully.");
+ }
+
+ if (!tableExists(template, "podcast_episode")) {
+ LOG.info("Database table 'podcast_episode' not found. Creating it.");
+ template.execute("create table podcast_episode (" +
+ "id identity," +
+ "channel_id int not null," +
+ "url varchar not null," +
+ "path varchar," +
+ "title varchar," +
+ "description varchar," +
+ "publish_date datetime," +
+ "duration varchar," +
+ "bytes_total bigint," +
+ "bytes_downloaded bigint," +
+ "status varchar not null," +
+ "error_message varchar," +
+ "foreign key (channel_id) references podcast_channel(id) on delete cascade)");
+ LOG.info("Database table 'podcast_episode' was created successfully.");
+ }
+
+ if (template.queryForInt("select count(*) from role where id = 7") == 0) {
+ LOG.info("Role 'podcast' not found in database. Creating it.");
+ template.execute("insert into role values (7, 'podcast')");
+ template.execute("insert into user_role " +
+ "select distinct u.username, 7 from user u, user_role ur " +
+ "where u.username = ur.username and ur.role_id = 1");
+ LOG.info("Role 'podcast' was created successfully.");
+ }
+
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java
new file mode 100644
index 00000000..6f754306
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java
@@ -0,0 +1,47 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.3.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema33 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema33.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 9") == 0) {
+ LOG.info("Updating database schema to version 9.");
+ template.execute("insert into version values (9)");
+ }
+
+ if (!columnExists(template, "client_side_playlist", "player")) {
+ LOG.info("Database column 'player.client_side_playlist' not found. Creating it.");
+ template.execute("alter table player add client_side_playlist boolean default false not null");
+ LOG.info("Database column 'player.client_side_playlist' was added successfully.");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java
new file mode 100644
index 00000000..daaf98ca
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java
@@ -0,0 +1,53 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.4.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema34 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema34.class);
+
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 10") == 0) {
+ LOG.info("Updating database schema to version 10.");
+ template.execute("insert into version values (10)");
+ }
+
+ if (!columnExists(template, "ldap_authenticated", "user")) {
+ LOG.info("Database column 'user.ldap_authenticated' not found. Creating it.");
+ template.execute("alter table user add ldap_authenticated boolean default false not null");
+ LOG.info("Database column 'user.ldap_authenticated' was added successfully.");
+ }
+
+ if (!columnExists(template, "party_mode_enabled", "user_settings")) {
+ LOG.info("Database column 'user_settings.party_mode_enabled' not found. Creating it.");
+ template.execute("alter table user_settings add party_mode_enabled boolean default false not null");
+ LOG.info("Database column 'user_settings.party_mode_enabled' was added successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java
new file mode 100644
index 00000000..56b5073d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java
@@ -0,0 +1,151 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.apache.commons.io.IOUtils;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.5.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema35 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema35.class);
+
+ private static final String[] AVATARS = {
+ "Formal", "Engineer", "Footballer", "Green-Boy",
+
+ "Linux-Zealot", "Mac-Zealot", "Windows-Zealot", "Army-Officer", "Beatnik",
+ "All-Caps", "Clown", "Commie-Pinko", "Forum-Flirt", "Gamer", "Hopelessly-Addicted",
+ "Jekyll-And-Hyde", "Joker", "Lurker", "Moderator", "Newbie", "No-Dissent",
+ "Performer", "Push-My-Button", "Ray-Of-Sunshine", "Red-Hot-Chili-Peppers-1",
+ "Red-Hot-Chili-Peppers-2", "Red-Hot-Chili-Peppers-3", "Red-Hot-Chili-Peppers-4",
+ "Ringmaster", "Rumor-Junkie", "Sozzled-Surfer", "Statistician", "Tech-Support",
+ "The-Guru", "The-Referee", "Troll", "Uptight",
+
+ "Fire-Guitar", "Drum", "Headphones", "Mic", "Turntable", "Vinyl",
+
+ "Cool", "Laugh", "Study"
+ };
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 11") == 0) {
+ LOG.info("Updating database schema to version 11.");
+ template.execute("insert into version values (11)");
+ }
+
+ if (!columnExists(template, "now_playing_allowed", "user_settings")) {
+ LOG.info("Database column 'user_settings.now_playing_allowed' not found. Creating it.");
+ template.execute("alter table user_settings add now_playing_allowed boolean default true not null");
+ LOG.info("Database column 'user_settings.now_playing_allowed' was added successfully.");
+ }
+
+ if (!columnExists(template, "web_player_default", "user_settings")) {
+ LOG.info("Database column 'user_settings.web_player_default' not found. Creating it.");
+ template.execute("alter table user_settings add web_player_default boolean default false not null");
+ LOG.info("Database column 'user_settings.web_player_default' was added successfully.");
+ }
+
+ if (template.queryForInt("select count(*) from role where id = 8") == 0) {
+ LOG.info("Role 'stream' not found in database. Creating it.");
+ template.execute("insert into role values (8, 'stream')");
+ template.execute("insert into user_role select distinct u.username, 8 from user u");
+ LOG.info("Role 'stream' was created successfully.");
+ }
+
+ if (!tableExists(template, "system_avatar")) {
+ LOG.info("Database table 'system_avatar' not found. Creating it.");
+ template.execute("create table system_avatar (" +
+ "id identity," +
+ "name varchar," +
+ "created_date datetime not null," +
+ "mime_type varchar not null," +
+ "width int not null," +
+ "height int not null," +
+ "data binary not null)");
+ LOG.info("Database table 'system_avatar' was created successfully.");
+ }
+
+ for (String avatar : AVATARS) {
+ createAvatar(template, avatar);
+ }
+
+ if (!tableExists(template, "custom_avatar")) {
+ LOG.info("Database table 'custom_avatar' not found. Creating it.");
+ template.execute("create table custom_avatar (" +
+ "id identity," +
+ "name varchar," +
+ "created_date datetime not null," +
+ "mime_type varchar not null," +
+ "width int not null," +
+ "height int not null," +
+ "data binary not null," +
+ "username varchar not null," +
+ "foreign key (username) references user(username) on delete cascade)");
+ LOG.info("Database table 'custom_avatar' was created successfully.");
+ }
+
+ if (!columnExists(template, "avatar_scheme", "user_settings")) {
+ LOG.info("Database column 'user_settings.avatar_scheme' not found. Creating it.");
+ template.execute("alter table user_settings add avatar_scheme varchar default 'NONE' not null");
+ LOG.info("Database column 'user_settings.avatar_scheme' was added successfully.");
+ }
+
+ if (!columnExists(template, "system_avatar_id", "user_settings")) {
+ LOG.info("Database column 'user_settings.system_avatar_id' not found. Creating it.");
+ template.execute("alter table user_settings add system_avatar_id int");
+ template.execute("alter table user_settings add foreign key (system_avatar_id) references system_avatar(id)");
+ LOG.info("Database column 'user_settings.system_avatar_id' was added successfully.");
+ }
+
+ if (!columnExists(template, "jukebox", "player")) {
+ LOG.info("Database column 'player.jukebox' not found. Creating it.");
+ template.execute("alter table player add jukebox boolean default false not null");
+ LOG.info("Database column 'player.jukebox' was added successfully.");
+ }
+ }
+
+ private void createAvatar(JdbcTemplate template, String avatar) {
+ if (template.queryForInt("select count(*) from system_avatar where name = ?", new Object[]{avatar}) == 0) {
+
+ InputStream in = null;
+ try {
+ in = getClass().getResourceAsStream(avatar + ".png");
+ byte[] imageData = IOUtils.toByteArray(in);
+ template.update("insert into system_avatar values (null, ?, ?, ?, ?, ?, ?)",
+ new Object[]{avatar, new Date(), "image/png", 48, 48, imageData});
+ LOG.info("Created avatar '" + avatar + "'.");
+ } catch (IOException x) {
+ LOG.error("Failed to create avatar '" + avatar + "'.", x);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java
new file mode 100644
index 00000000..caed6cdb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java
@@ -0,0 +1,48 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implementes the database schema for Subsonic version 3.6.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema36 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema36.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 12") == 0) {
+ LOG.info("Updating database schema to version 12.");
+ template.execute("insert into version values (12)");
+ }
+
+ if (!columnExists(template, "technology", "player")) {
+ LOG.info("Database column 'player.technology' not found. Creating it.");
+ template.execute("alter table player add technology varchar default 'WEB' not null");
+ LOG.info("Database column 'player.technology' was added successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.java
new file mode 100644
index 00000000..afb8fb6e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.java
@@ -0,0 +1,77 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 3.7.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema37 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema37.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 13") == 0) {
+ LOG.info("Updating database schema to version 13.");
+ template.execute("insert into version values (13)");
+ }
+
+ if (template.queryForInt("select count(*) from role where id = 9") == 0) {
+ LOG.info("Role 'settings' not found in database. Creating it.");
+ template.execute("insert into role values (9, 'settings')");
+ template.execute("insert into user_role select distinct u.username, 9 from user u");
+ LOG.info("Role 'settings' was created successfully.");
+ }
+
+ if (template.queryForInt("select count(*) from role where id = 10") == 0) {
+ LOG.info("Role 'jukebox' not found in database. Creating it.");
+ template.execute("insert into role values (10, 'jukebox')");
+ template.execute("insert into user_role " +
+ "select distinct u.username, 10 from user u, user_role ur " +
+ "where u.username = ur.username and ur.role_id = 1");
+ LOG.info("Role 'jukebox' was created successfully.");
+ }
+
+ if (!columnExists(template, "changed", "music_folder")) {
+ LOG.info("Database column 'music_folder.changed' not found. Creating it.");
+ template.execute("alter table music_folder add changed datetime default 0 not null");
+ LOG.info("Database column 'music_folder.changed' was added successfully.");
+ }
+
+ if (!columnExists(template, "changed", "internet_radio")) {
+ LOG.info("Database column 'internet_radio.changed' not found. Creating it.");
+ template.execute("alter table internet_radio add changed datetime default 0 not null");
+ LOG.info("Database column 'internet_radio.changed' was added successfully.");
+ }
+
+ if (!columnExists(template, "changed", "user_settings")) {
+ LOG.info("Database column 'user_settings.changed' not found. Creating it.");
+ template.execute("alter table user_settings add changed datetime default 0 not null");
+ LOG.info("Database column 'user_settings.changed' was added successfully.");
+ }
+
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java
new file mode 100644
index 00000000..fac49511
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 3.8.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema38 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema38.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 14") == 0) {
+ LOG.info("Updating database schema to version 14.");
+ template.execute("insert into version values (14)");
+ }
+
+ if (!columnExists(template, "client_id", "player")) {
+ LOG.info("Database column 'player.client_id' not found. Creating it.");
+ template.execute("alter table player add client_id varchar");
+ LOG.info("Database column 'player.client_id' was added successfully.");
+ }
+
+ if (!columnExists(template, "show_chat", "user_settings")) {
+ LOG.info("Database column 'user_settings.show_chat' not found. Creating it.");
+ template.execute("alter table user_settings add show_chat boolean default true not null");
+ LOG.info("Database column 'user_settings.show_chat' was added successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.java
new file mode 100644
index 00000000..e01d1ef0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 4.0.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema40 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema40.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 15") == 0) {
+ LOG.info("Updating database schema to version 15.");
+ template.execute("insert into version values (15)");
+
+ // Reset stream byte count since they have been wrong in earlier releases.
+ template.execute("update user set bytes_streamed = 0");
+ LOG.info("Reset stream byte count statistics.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java
new file mode 100644
index 00000000..cba1572c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java
@@ -0,0 +1,65 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import net.sourceforge.subsonic.Logger;
+
+import java.util.Arrays;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 4.3.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema43 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema43.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ // version 16 was used for 4.3.beta1
+ if (template.queryForInt("select count(*) from version where version = 16") == 0) {
+ LOG.info("Updating database schema to version 16.");
+ template.execute("insert into version values (16)");
+ }
+
+ if (template.queryForInt("select count(*) from version where version = 17") == 0) {
+ LOG.info("Updating database schema to version 17.");
+ template.execute("insert into version values (17)");
+
+ for (String format : Arrays.asList("avi", "mpg", "mpeg", "mp4", "m4v", "mkv", "mov", "wmv", "ogv")) {
+ template.update("delete from transcoding where source_format=? and target_format=?", new Object[] {format, "flv"});
+ template.execute("insert into transcoding values(null,'" + format + " > flv' ,'" + format + "' ,'flv','ffmpeg -ss %o -i %s -async 1 -b %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f flv -',null,null,true,true)");
+ template.execute("insert into player_transcoding select p.id as player_id, t.id as transaction_id from player p, transcoding t where t.name = '" + format + " > flv'");
+ }
+ LOG.info("Created video transcoding configuration.");
+ }
+
+ if (!columnExists(template, "email", "user")) {
+ LOG.info("Database column 'user.email' not found. Creating it.");
+ template.execute("alter table user add email varchar");
+ LOG.info("Database column 'user.email' was added successfully.");
+ }
+
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java
new file mode 100644
index 00000000..d82f2a92
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java
@@ -0,0 +1,76 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 4.5.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema45 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema45.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 18") == 0) {
+ LOG.info("Updating database schema to version 18.");
+ template.execute("insert into version values (18)");
+ }
+
+ if (template.queryForInt("select count(*) from role where id = 11") == 0) {
+ LOG.info("Role 'share' not found in database. Creating it.");
+ template.execute("insert into role values (11, 'share')");
+ template.execute("insert into user_role " +
+ "select distinct u.username, 11 from user u, user_role ur " +
+ "where u.username = ur.username and ur.role_id = 1");
+ LOG.info("Role 'share' was created successfully.");
+ }
+
+ if (!tableExists(template, "share")) {
+ LOG.info("Table 'share' not found in database. Creating it.");
+ template.execute("create cached table share (" +
+ "id identity," +
+ "name varchar not null," +
+ "description varchar," +
+ "username varchar not null," +
+ "created datetime not null," +
+ "expires datetime," +
+ "last_visited datetime," +
+ "visit_count int default 0 not null," +
+ "unique (name)," +
+ "foreign key (username) references user(username) on delete cascade)");
+ template.execute("create index idx_share_name on share(name)");
+
+ LOG.info("Table 'share' was created successfully.");
+ LOG.info("Table 'share_file' not found in database. Creating it.");
+ template.execute("create cached table share_file (" +
+ "id identity," +
+ "share_id int not null," +
+ "path varchar not null," +
+ "foreign key (share_id) references share(id) on delete cascade)");
+ LOG.info("Table 'share_file' was created successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java
new file mode 100644
index 00000000..c1fcf357
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java
@@ -0,0 +1,87 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import net.sourceforge.subsonic.Logger;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 4.6.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema46 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema46.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 19") == 0) {
+ LOG.info("Updating database schema to version 19.");
+ template.execute("insert into version values (19)");
+ }
+
+ if (!tableExists(template, "transcoding2")) {
+ LOG.info("Database table 'transcoding2' not found. Creating it.");
+ template.execute("create table transcoding2 (" +
+ "id identity," +
+ "name varchar not null," +
+ "source_formats varchar not null," +
+ "target_format varchar not null," +
+ "step1 varchar not null," +
+ "step2 varchar," +
+ "step3 varchar)");
+
+ template.execute("insert into transcoding2 values(null,'mp3 audio'," +
+ "'ogg oga aac m4a flac wav wma aif aiff ape mpc shn', 'mp3', " +
+ "'ffmpeg -i %s -ab %bk -v 0 -f mp3 -', null, null)");
+
+ template.execute("insert into transcoding2 values(null,'flv/h264 video', " +
+ "'avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts', 'flv', " +
+ "'ffmpeg -ss %o -i %s -async 1 -b %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f flv -vcodec libx264 -preset superfast -threads 0 -', null, null)");
+
+ LOG.info("Database table 'transcoding2' was created successfully.");
+ }
+
+ if (!tableExists(template, "player_transcoding2")) {
+ LOG.info("Database table 'player_transcoding2' not found. Creating it.");
+ template.execute("create table player_transcoding2 (" +
+ "player_id int not null," +
+ "transcoding_id int not null," +
+ "primary key (player_id, transcoding_id)," +
+ "foreign key (player_id) references player(id) on delete cascade," +
+ "foreign key (transcoding_id) references transcoding2(id) on delete cascade)");
+
+ template.execute("insert into player_transcoding2(player_id, transcoding_id) " +
+ "select distinct p.id, t.id from player p, transcoding2 t");
+
+ LOG.info("Database table 'player_transcoding2' was created successfully.");
+ }
+
+ if (!columnExists(template, "default_active", "transcoding2")) {
+ LOG.info("Database column 'transcoding2.default_active' not found. Creating it.");
+ template.execute("alter table transcoding2 add default_active boolean default true not null");
+ LOG.info("Database column 'transcoding2.default_active' was added successfully.");
+ }
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java
new file mode 100644
index 00000000..8b290b47
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java
@@ -0,0 +1,234 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.dao.schema;
+
+import net.sourceforge.subsonic.Logger;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Used for creating and evolving the database schema.
+ * This class implements the database schema for Subsonic version 4.7.
+ *
+ * @author Sindre Mehus
+ */
+public class Schema47 extends Schema {
+
+ private static final Logger LOG = Logger.getLogger(Schema47.class);
+
+ @Override
+ public void execute(JdbcTemplate template) {
+
+ if (template.queryForInt("select count(*) from version where version = 20") == 0) {
+ LOG.info("Updating database schema to version 20.");
+ template.execute("insert into version values (20)");
+ }
+
+ if (!tableExists(template, "media_file")) {
+ LOG.info("Database table 'media_file' not found. Creating it.");
+ template.execute("create cached table media_file (" +
+ "id identity," +
+ "path varchar not null," +
+ "folder varchar," +
+ "type varchar not null," +
+ "format varchar," +
+ "title varchar," +
+ "album varchar," +
+ "artist varchar," +
+ "album_artist varchar," +
+ "disc_number int," +
+ "track_number int," +
+ "year int," +
+ "genre varchar," +
+ "bit_rate int," +
+ "variable_bit_rate boolean not null," +
+ "duration_seconds int," +
+ "file_size bigint," +
+ "width int," +
+ "height int," +
+ "cover_art_path varchar," +
+ "parent_path varchar," +
+ "play_count int not null," +
+ "last_played datetime," +
+ "comment varchar," +
+ "created datetime not null," +
+ "changed datetime not null," +
+ "last_scanned datetime not null," +
+ "children_last_updated datetime not null," +
+ "present boolean not null," +
+ "version int not null," +
+ "unique (path))");
+
+ template.execute("create index idx_media_file_path on media_file(path)");
+ template.execute("create index idx_media_file_parent_path on media_file(parent_path)");
+ template.execute("create index idx_media_file_type on media_file(type)");
+ template.execute("create index idx_media_file_album on media_file(album)");
+ template.execute("create index idx_media_file_artist on media_file(artist)");
+ template.execute("create index idx_media_file_album_artist on media_file(album_artist)");
+ template.execute("create index idx_media_file_present on media_file(present)");
+ template.execute("create index idx_media_file_genre on media_file(genre)");
+ template.execute("create index idx_media_file_play_count on media_file(play_count)");
+ template.execute("create index idx_media_file_created on media_file(created)");
+ template.execute("create index idx_media_file_last_played on media_file(last_played)");
+
+ LOG.info("Database table 'media_file' was created successfully.");
+ }
+
+ if (!tableExists(template, "artist")) {
+ LOG.info("Database table 'artist' not found. Creating it.");
+ template.execute("create cached table artist (" +
+ "id identity," +
+ "name varchar not null," +
+ "cover_art_path varchar," +
+ "album_count int default 0 not null," +
+ "last_scanned datetime not null," +
+ "present boolean not null," +
+ "unique (name))");
+
+ template.execute("create index idx_artist_name on artist(name)");
+ template.execute("create index idx_artist_present on artist(present)");
+
+ LOG.info("Database table 'artist' was created successfully.");
+ }
+
+ if (!tableExists(template, "album")) {
+ LOG.info("Database table 'album' not found. Creating it.");
+ template.execute("create cached table album (" +
+ "id identity," +
+ "path varchar not null," +
+ "name varchar not null," +
+ "artist varchar not null," +
+ "song_count int default 0 not null," +
+ "duration_seconds int default 0 not null," +
+ "cover_art_path varchar," +
+ "play_count int default 0 not null," +
+ "last_played datetime," +
+ "comment varchar," +
+ "created datetime not null," +
+ "last_scanned datetime not null," +
+ "present boolean not null," +
+ "unique (artist, name))");
+
+ template.execute("create index idx_album_artist_name on album(artist, name)");
+ template.execute("create index idx_album_play_count on album(play_count)");
+ template.execute("create index idx_album_last_played on album(last_played)");
+ template.execute("create index idx_album_present on album(present)");
+
+ LOG.info("Database table 'album' was created successfully.");
+ }
+
+ if (!tableExists(template, "starred_media_file")) {
+ LOG.info("Database table 'starred_media_file' not found. Creating it.");
+ template.execute("create table starred_media_file (" +
+ "id identity," +
+ "media_file_id int not null," +
+ "username varchar not null," +
+ "created datetime not null," +
+ "foreign key (media_file_id) references media_file(id) on delete cascade,"+
+ "foreign key (username) references user(username) on delete cascade," +
+ "unique (media_file_id, username))");
+
+ template.execute("create index idx_starred_media_file_media_file_id on starred_media_file(media_file_id)");
+ template.execute("create index idx_starred_media_file_username on starred_media_file(username)");
+
+ LOG.info("Database table 'starred_media_file' was created successfully.");
+ }
+
+ if (!tableExists(template, "starred_album")) {
+ LOG.info("Database table 'starred_album' not found. Creating it.");
+ template.execute("create table starred_album (" +
+ "id identity," +
+ "album_id int not null," +
+ "username varchar not null," +
+ "created datetime not null," +
+ "foreign key (album_id) references album(id) on delete cascade," +
+ "foreign key (username) references user(username) on delete cascade," +
+ "unique (album_id, username))");
+
+ template.execute("create index idx_starred_album_album_id on starred_album(album_id)");
+ template.execute("create index idx_starred_album_username on starred_album(username)");
+
+ LOG.info("Database table 'starred_album' was created successfully.");
+ }
+
+ if (!tableExists(template, "starred_artist")) {
+ LOG.info("Database table 'starred_artist' not found. Creating it.");
+ template.execute("create table starred_artist (" +
+ "id identity," +
+ "artist_id int not null," +
+ "username varchar not null," +
+ "created datetime not null," +
+ "foreign key (artist_id) references artist(id) on delete cascade,"+
+ "foreign key (username) references user(username) on delete cascade," +
+ "unique (artist_id, username))");
+
+ template.execute("create index idx_starred_artist_artist_id on starred_artist(artist_id)");
+ template.execute("create index idx_starred_artist_username on starred_artist(username)");
+
+ LOG.info("Database table 'starred_artist' was created successfully.");
+ }
+
+ if (!tableExists(template, "playlist")) {
+ LOG.info("Database table 'playlist' not found. Creating it.");
+ template.execute("create table playlist (" +
+ "id identity," +
+ "username varchar not null," +
+ "is_public boolean not null," +
+ "name varchar not null," +
+ "comment varchar," +
+ "file_count int default 0 not null," +
+ "duration_seconds int default 0 not null," +
+ "created datetime not null," +
+ "changed datetime not null," +
+ "foreign key (username) references user(username) on delete cascade)");
+
+ LOG.info("Database table 'playlist' was created successfully.");
+ }
+
+ if (!columnExists(template, "imported_from", "playlist")) {
+ LOG.info("Database column 'playlist.imported_from' not found. Creating it.");
+ template.execute("alter table playlist add imported_from varchar");
+ LOG.info("Database column 'playlist.imported_from' was added successfully.");
+ }
+
+ if (!tableExists(template, "playlist_file")) {
+ LOG.info("Database table 'playlist_file' not found. Creating it.");
+ template.execute("create cached table playlist_file (" +
+ "id identity," +
+ "playlist_id int not null," +
+ "media_file_id int not null," +
+ "foreign key (playlist_id) references playlist(id) on delete cascade," +
+ "foreign key (media_file_id) references media_file(id) on delete cascade)");
+
+ LOG.info("Database table 'playlist_file' was created successfully.");
+ }
+
+ if (!tableExists(template, "playlist_user")) {
+ LOG.info("Database table 'playlist_user' not found. Creating it.");
+ template.execute("create table playlist_user (" +
+ "id identity," +
+ "playlist_id int not null," +
+ "username varchar not null," +
+ "unique(playlist_id, username)," +
+ "foreign key (playlist_id) references playlist(id) on delete cascade," +
+ "foreign key (username) references user(username) on delete cascade)");
+
+ LOG.info("Database table 'playlist_user' was created successfully.");
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java
new file mode 100644
index 00000000..23e8afaf
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java
@@ -0,0 +1,166 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class Album {
+
+ private int id;
+ private String path;
+ private String name;
+ private String artist;
+ private int songCount;
+ private int durationSeconds;
+ private String coverArtPath;
+ private int playCount;
+ private Date lastPlayed;
+ private String comment;
+ private Date created;
+ private Date lastScanned;
+ private boolean present;
+
+ public Album() {
+ }
+
+ public Album(int id, String path, String name, String artist, int songCount, int durationSeconds, String coverArtPath,
+ int playCount, Date lastPlayed, String comment, Date created, Date lastScanned, boolean present) {
+ this.id = id;
+ this.path = path;
+ this.name = name;
+ this.artist = artist;
+ this.songCount = songCount;
+ this.durationSeconds = durationSeconds;
+ this.coverArtPath = coverArtPath;
+ this.playCount = playCount;
+ this.lastPlayed = lastPlayed;
+ this.comment = comment;
+ this.created = created;
+ this.lastScanned = lastScanned;
+ this.present = present;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public int getSongCount() {
+ return songCount;
+ }
+
+ public void setSongCount(int songCount) {
+ this.songCount = songCount;
+ }
+
+ public int getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public void setDurationSeconds(int durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ }
+
+ public String getCoverArtPath() {
+ return coverArtPath;
+ }
+
+ public void setCoverArtPath(String coverArtPath) {
+ this.coverArtPath = coverArtPath;
+ }
+
+ public int getPlayCount() {
+ return playCount;
+ }
+
+ public void setPlayCount(int playCount) {
+ this.playCount = playCount;
+ }
+
+ public Date getLastPlayed() {
+ return lastPlayed;
+ }
+
+ public void setLastPlayed(Date lastPlayed) {
+ this.lastPlayed = lastPlayed;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getLastScanned() {
+ return lastScanned;
+ }
+
+ public void setLastScanned(Date lastScanned) {
+ this.lastScanned = lastScanned;
+ }
+
+ public boolean isPresent() {
+ return present;
+ }
+
+ public void setPresent(boolean present) {
+ this.present = present;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java
new file mode 100644
index 00000000..e6141f78
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java
@@ -0,0 +1,95 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class Artist {
+
+ private int id;
+ private String name;
+ private String coverArtPath;
+ private int albumCount;
+ private Date lastScanned;
+ private boolean present;
+
+ public Artist() {
+ }
+
+ public Artist(int id, String name, String coverArtPath, int albumCount, Date lastScanned, boolean present) {
+ this.id = id;
+ this.name = name;
+ this.coverArtPath = coverArtPath;
+ this.albumCount = albumCount;
+ this.lastScanned = lastScanned;
+ this.present = present;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getCoverArtPath() {
+ return coverArtPath;
+ }
+
+ public void setCoverArtPath(String coverArtPath) {
+ this.coverArtPath = coverArtPath;
+ }
+
+ public int getAlbumCount() {
+ return albumCount;
+ }
+
+ public void setAlbumCount(int albumCount) {
+ this.albumCount = albumCount;
+ }
+
+ public Date getLastScanned() {
+ return lastScanned;
+ }
+
+ public void setLastScanned(Date lastScanned) {
+ this.lastScanned = lastScanned;
+ }
+
+ public boolean isPresent() {
+ return present;
+ }
+
+ public void setPresent(boolean present) {
+ this.present = present;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java
new file mode 100644
index 00000000..0089a8a3
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java
@@ -0,0 +1,75 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.Date;
+
+/**
+ * An icon representing a user.
+ *
+ * @author Sindre Mehus
+ */
+public class Avatar {
+
+ private int id;
+ private String name;
+ private Date createdDate;
+ private String mimeType;
+ private int width;
+ private int height;
+ private byte[] data;
+
+ public Avatar(int id, String name, Date createdDate, String mimeType, int width, int height, byte[] data) {
+ this.id = id;
+ this.name = name;
+ this.createdDate = createdDate;
+ this.mimeType = mimeType;
+ this.width = width;
+ this.height = height;
+ this.data = data;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Date getCreatedDate() {
+ return createdDate;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.java
new file mode 100644
index 00000000..024dcb24
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.java
@@ -0,0 +1,52 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Enumeration of avatar schemes.
+ *
+ * @author Sindre Mehus
+ */
+public enum AvatarScheme {
+
+ /**
+ * No avatar should be displayed.
+ */
+ NONE(-1),
+
+ /**
+ * One of the system avatars should be displayed.
+ */
+ SYSTEM(0),
+
+ /**
+ * The custom avatar should be displayed.
+ */
+ CUSTOM(-2);
+
+ private final int code;
+
+ AvatarScheme(int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java
new file mode 100644
index 00000000..bb52eff7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java
@@ -0,0 +1,65 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class CacheElement {
+
+ private final long id;
+ private final int type;
+ private final String key;
+ private final Object value;
+ private final long created;
+
+ public CacheElement(int type, String key, Object value, long created) {
+ this.type = type;
+ this.key = key;
+ this.value = value;
+ this.created = created;
+
+ id = createId(type, key);
+ }
+
+ public static long createId(int type, String key) {
+ return ((long) type << 32) | Math.abs(key.hashCode());
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public long getCreated() {
+ return created;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java
new file mode 100644
index 00000000..91293e9f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java
@@ -0,0 +1,48 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Enumeration of cover art schemes. Each value contains a size, which indicates how big the
+ * scaled covert art images should be.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.3 $ $Date: 2005/06/15 18:10:40 $
+ */
+public enum CoverArtScheme {
+
+ OFF(0),
+ SMALL(70),
+ MEDIUM(100),
+ LARGE(150);
+
+ private int size;
+
+ CoverArtScheme(int size) {
+ this.size = size;
+ }
+
+ /**
+ * Returns the covert art size for this scheme.
+ * @return the covert art size for this scheme.
+ */
+ public int getSize() {
+ return size;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java
new file mode 100644
index 00000000..ae0c1f67
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java
@@ -0,0 +1,168 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.Date;
+
+/**
+ * Represents an internet radio station.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.2 $ $Date: 2005/12/25 13:48:46 $
+ */
+public class InternetRadio {
+
+ private Integer id;
+ private String name;
+ private String streamUrl;
+ private String homepageUrl;
+ private boolean isEnabled;
+ private Date changed;
+
+ /**
+ * Creates a new internet radio station.
+ *
+ * @param id The system-generated ID.
+ * @param name The user-defined name.
+ * @param streamUrl The stream URL for the station.
+ * @param homepageUrl The home page URL for the station.
+ * @param isEnabled Whether the station is enabled.
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public InternetRadio(Integer id, String name, String streamUrl, String homepageUrl, boolean isEnabled, Date changed) {
+ this.id = id;
+ this.name = name;
+ this.streamUrl = streamUrl;
+ this.homepageUrl = homepageUrl;
+ this.isEnabled = isEnabled;
+ this.changed = changed;
+ }
+
+ /**
+ * Creates a new internet radio station.
+ *
+ * @param name The user-defined name.
+ * @param streamUrl The URL for the station.
+ * @param homepageUrl The home page URL for the station.
+ * @param isEnabled Whether the station is enabled.
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public InternetRadio(String name, String streamUrl, String homepageUrl, boolean isEnabled, Date changed) {
+ this(null, name, streamUrl, homepageUrl, isEnabled, changed);
+ }
+
+ /**
+ * Returns the system-generated ID.
+ *
+ * @return The system-generated ID.
+ */
+ public Integer getId() {
+ return id;
+ }
+
+ /**
+ * Returns the user-defined name.
+ *
+ * @return The user-defined name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user-defined name.
+ *
+ * @param name The user-defined name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the stream URL of the radio station.
+ *
+ * @return The stream URL of the radio station.
+ */
+ public String getStreamUrl() {
+ return streamUrl;
+ }
+
+ /**
+ * Sets the stream URL of the radio station.
+ *
+ * @param streamUrl The stream URL of the radio station.
+ */
+ public void setStreamUrl(String streamUrl) {
+ this.streamUrl = streamUrl;
+ }
+
+ /**
+ * Returns the homepage URL of the radio station.
+ *
+ * @return The homepage URL of the radio station.
+ */
+ public String getHomepageUrl() {
+ return homepageUrl;
+ }
+
+ /**
+ * Sets the home page URL of the radio station.
+ *
+ * @param homepageUrl The home page URL of the radio station.
+ */
+ public void setHomepageUrl(String homepageUrl) {
+ this.homepageUrl = homepageUrl;
+ }
+
+ /**
+ * Returns whether the radio station is enabled.
+ *
+ * @return Whether the radio station is enabled.
+ */
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ /**
+ * Sets whether the radio station is enabled.
+ *
+ * @param enabled Whether the radio station is enabled.
+ */
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ /**
+ * Returns when the corresponding database entry was last changed.
+ *
+ * @return When the corresponding database entry was last changed.
+ */
+ public Date getChanged() {
+ return changed;
+ }
+
+ /**
+ * Sets when the corresponding database entry was last changed.
+ *
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public void setChanged(Date changed) {
+ this.changed = changed;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java
new file mode 100644
index 00000000..4f315028
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java
@@ -0,0 +1,449 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sourceforge.subsonic.util.FileUtil;
+import org.apache.commons.io.FilenameUtils;
+
+import java.io.File;
+import java.util.Date;
+
+/**
+ * A media file (audio, video or directory) with an assortment of its meta data.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class MediaFile {
+
+ private int id;
+ private String path;
+ private String folder;
+ private MediaType mediaType;
+ private String format;
+ private String title;
+ private String albumName;
+ private String artist;
+ private String albumArtist;
+ private Integer discNumber;
+ private Integer trackNumber;
+ private Integer year;
+ private String genre;
+ private Integer bitRate;
+ private boolean variableBitRate;
+ private Integer durationSeconds;
+ private Long fileSize;
+ private Integer width;
+ private Integer height;
+ private String coverArtPath;
+ private String parentPath;
+ private int playCount;
+ private Date lastPlayed;
+ private String comment;
+ private Date created;
+ private Date changed;
+ private Date lastScanned;
+ private Date starredDate;
+ private Date childrenLastUpdated;
+ private boolean present;
+
+ public MediaFile(int id, String path, String folder, MediaType mediaType, String format, String title,
+ String albumName, String artist, String albumArtist, Integer discNumber, Integer trackNumber, Integer year, String genre, Integer bitRate,
+ boolean variableBitRate, Integer durationSeconds, Long fileSize, Integer width, Integer height, String coverArtPath,
+ String parentPath, int playCount, Date lastPlayed, String comment, Date created, Date changed, Date lastScanned,
+ Date childrenLastUpdated, boolean present) {
+ this.id = id;
+ this.path = path;
+ this.folder = folder;
+ this.mediaType = mediaType;
+ this.format = format;
+ this.title = title;
+ this.albumName = albumName;
+ this.artist = artist;
+ this.albumArtist = albumArtist;
+ this.discNumber = discNumber;
+ this.trackNumber = trackNumber;
+ this.year = year;
+ this.genre = genre;
+ this.bitRate = bitRate;
+ this.variableBitRate = variableBitRate;
+ this.durationSeconds = durationSeconds;
+ this.fileSize = fileSize;
+ this.width = width;
+ this.height = height;
+ this.coverArtPath = coverArtPath;
+ this.parentPath = parentPath;
+ this.playCount = playCount;
+ this.lastPlayed = lastPlayed;
+ this.comment = comment;
+ this.created = created;
+ this.changed = changed;
+ this.lastScanned = lastScanned;
+ this.childrenLastUpdated = childrenLastUpdated;
+ this.present = present;
+ }
+
+ public MediaFile() {
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getFolder() {
+ return folder;
+ }
+
+ public void setFolder(String folder) {
+ this.folder = folder;
+ }
+
+ public File getFile() {
+ // TODO: Optimize
+ return new File(path);
+ }
+
+ public boolean exists() {
+ return FileUtil.exists(getFile());
+ }
+
+ public MediaType getMediaType() {
+ return mediaType;
+ }
+
+ public void setMediaType(MediaType mediaType) {
+ this.mediaType = mediaType;
+ }
+
+ public boolean isVideo() {
+ return mediaType == MediaType.VIDEO;
+ }
+
+ public boolean isAudio() {
+ return mediaType == MediaType.MUSIC || mediaType == MediaType.AUDIOBOOK || mediaType == MediaType.PODCAST;
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ public void setFormat(String format) {
+ this.format = format;
+ }
+
+ public boolean isDirectory() {
+ return !isFile();
+ }
+
+ public boolean isFile() {
+ return mediaType != MediaType.DIRECTORY && mediaType != MediaType.ALBUM;
+ }
+
+ public boolean isAlbum() {
+ return mediaType == MediaType.ALBUM;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getAlbumName() {
+ return albumName;
+ }
+
+ public void setAlbumName(String album) {
+ this.albumName = album;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getAlbumArtist() {
+ return albumArtist;
+ }
+
+ public void setAlbumArtist(String albumArtist) {
+ this.albumArtist = albumArtist;
+ }
+
+ public String getName() {
+ if (isFile()) {
+ return title != null ? title : FilenameUtils.getBaseName(path);
+ }
+
+ return FilenameUtils.getName(path);
+ }
+
+ public Integer getDiscNumber() {
+ return discNumber;
+ }
+
+ public void setDiscNumber(Integer discNumber) {
+ this.discNumber = discNumber;
+ }
+
+ public Integer getTrackNumber() {
+ return trackNumber;
+ }
+
+ public void setTrackNumber(Integer trackNumber) {
+ this.trackNumber = trackNumber;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public boolean isVariableBitRate() {
+ return variableBitRate;
+ }
+
+ public void setVariableBitRate(boolean variableBitRate) {
+ this.variableBitRate = variableBitRate;
+ }
+
+ public Integer getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public void setDurationSeconds(Integer durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ }
+
+ public String getDurationString() {
+ if (durationSeconds == null) {
+ return null;
+ }
+
+ StringBuilder result = new StringBuilder(8);
+
+ int seconds = durationSeconds;
+
+ int hours = seconds / 3600;
+ seconds -= hours * 3600;
+
+ int minutes = seconds / 60;
+ seconds -= minutes * 60;
+
+ if (hours > 0) {
+ result.append(hours).append(':');
+ if (minutes < 10) {
+ result.append('0');
+ }
+ }
+
+ result.append(minutes).append(':');
+ if (seconds < 10) {
+ result.append('0');
+ }
+ result.append(seconds);
+
+ return result.toString();
+ }
+
+ public Long getFileSize() {
+ return fileSize;
+ }
+
+ public void setFileSize(Long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ public Integer getWidth() {
+ return width;
+ }
+
+ public void setWidth(Integer width) {
+ this.width = width;
+ }
+
+ public Integer getHeight() {
+ return height;
+ }
+
+ public void setHeight(Integer height) {
+ this.height = height;
+ }
+
+ public String getCoverArtPath() {
+ return coverArtPath;
+ }
+
+ public void setCoverArtPath(String coverArtPath) {
+ this.coverArtPath = coverArtPath;
+ }
+
+
+ public String getParentPath() {
+ return parentPath;
+ }
+
+ public void setParentPath(String parentPath) {
+ this.parentPath = parentPath;
+ }
+
+ public File getParentFile() {
+ return getFile().getParentFile();
+ }
+
+ public int getPlayCount() {
+ return playCount;
+ }
+
+ public void setPlayCount(int playCount) {
+ this.playCount = playCount;
+ }
+
+ public Date getLastPlayed() {
+ return lastPlayed;
+ }
+
+ public void setLastPlayed(Date lastPlayed) {
+ this.lastPlayed = lastPlayed;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getChanged() {
+ return changed;
+ }
+
+ public void setChanged(Date changed) {
+ this.changed = changed;
+ }
+
+ public Date getLastScanned() {
+ return lastScanned;
+ }
+
+ public void setLastScanned(Date lastScanned) {
+ this.lastScanned = lastScanned;
+ }
+
+ public Date getStarredDate() {
+ return starredDate;
+ }
+
+ public void setStarredDate(Date starredDate) {
+ this.starredDate = starredDate;
+ }
+
+ /**
+ * Returns when the children was last updated in the database.
+ */
+ public Date getChildrenLastUpdated() {
+ return childrenLastUpdated;
+ }
+
+ public void setChildrenLastUpdated(Date childrenLastUpdated) {
+ this.childrenLastUpdated = childrenLastUpdated;
+ }
+
+ public boolean isPresent() {
+ return present;
+ }
+
+ public void setPresent(boolean present) {
+ this.present = present;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof MediaFile && ((MediaFile) o).path.equals(path);
+ }
+
+ @Override
+ public int hashCode() {
+ return path.hashCode();
+ }
+
+ public File getCoverArtFile() {
+ // TODO: Optimize
+ return coverArtPath == null ? null : new File(coverArtPath);
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ public static enum MediaType {
+ MUSIC,
+ PODCAST,
+ AUDIOBOOK,
+ VIDEO,
+ DIRECTORY,
+ ALBUM
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java
new file mode 100644
index 00000000..13b5bbbd
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java
@@ -0,0 +1,99 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.Comparator;
+
+import static net.sourceforge.subsonic.domain.MediaFile.MediaType.DIRECTORY;
+
+/**
+ * Comparator for sorting media files.
+ */
+public class MediaFileComparator implements Comparator<MediaFile> {
+
+ private final boolean sortAlbumsByYear;
+
+ public MediaFileComparator(boolean sortAlbumsByYear) {
+ this.sortAlbumsByYear = sortAlbumsByYear;
+ }
+
+ public int compare(MediaFile a, MediaFile b) {
+
+ // Directories before files.
+ if (a.isFile() && b.isDirectory()) {
+ return 1;
+ }
+ if (a.isDirectory() && b.isFile()) {
+ return -1;
+ }
+
+ // Non-album directories before album directories.
+ if (a.isAlbum() && b.getMediaType() == DIRECTORY) {
+ return 1;
+ }
+ if (a.getMediaType() == DIRECTORY && b.isAlbum()) {
+ return -1;
+ }
+
+ // Sort albums by year
+ if (sortAlbumsByYear && a.isAlbum() && b.isAlbum()) {
+ int i = nullSafeCompare(a.getYear(), b.getYear(), false);
+ if (i != 0) {
+ return i;
+ }
+ }
+
+ if (a.isDirectory() && b.isDirectory()) {
+ return a.getName().compareToIgnoreCase(b.getName());
+ }
+
+ // Compare by disc and track numbers, if present.
+ Integer trackA = getSortableDiscAndTrackNumber(a);
+ Integer trackB = getSortableDiscAndTrackNumber(b);
+ int i = nullSafeCompare(trackA, trackB, false);
+ if (i != 0) {
+ return i;
+ }
+
+ return a.getName().compareToIgnoreCase(b.getName());
+ }
+
+ private <T extends Comparable<T>> int nullSafeCompare(T a, T b, boolean nullIsSmaller) {
+ if (a == null && b == null) {
+ return 0;
+ }
+ if (a == null) {
+ return nullIsSmaller ? -1 : 1;
+ }
+ if (b == null) {
+ return nullIsSmaller ? 1 : -1;
+ }
+ return a.compareTo(b);
+ }
+
+ private Integer getSortableDiscAndTrackNumber(MediaFile file) {
+ if (file.getTrackNumber() == null) {
+ return null;
+ }
+
+ int discNumber = file.getDiscNumber() == null ? 1 : file.getDiscNumber();
+ return discNumber * 1000 + file.getTrackNumber();
+ }
+}
+
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java
new file mode 100644
index 00000000..c8b0cdd9
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java
@@ -0,0 +1,62 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Contains media libaray statistics, including the number of artists, albums and songs.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.1 $ $Date: 2005/11/17 18:29:03 $
+ */
+public class MediaLibraryStatistics {
+
+ private int artistCount;
+ private int albumCount;
+ private int songCount;
+ private long totalLengthInBytes;
+ private long totalDurationInSeconds;
+
+ public MediaLibraryStatistics(int artistCount, int albumCount, int songCount, long totalLengthInBytes, long totalDurationInSeconds) {
+ this.artistCount = artistCount;
+ this.albumCount = albumCount;
+ this.songCount = songCount;
+ this.totalLengthInBytes = totalLengthInBytes;
+ this.totalDurationInSeconds = totalDurationInSeconds;
+ }
+
+ public int getArtistCount() {
+ return artistCount;
+ }
+
+ public int getAlbumCount() {
+ return albumCount;
+ }
+
+ public int getSongCount() {
+ return songCount;
+ }
+
+ public long getTotalLengthInBytes() {
+ return totalLengthInBytes;
+ }
+
+ public long getTotalDurationInSeconds() {
+ return totalDurationInSeconds;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java
new file mode 100644
index 00000000..613b0a7f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java
@@ -0,0 +1,148 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * Represents a top level directory in which music or other media is stored.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.1 $ $Date: 2005/11/27 14:32:05 $
+ */
+public class MusicFolder implements Serializable {
+
+ private Integer id;
+ private File path;
+ private String name;
+ private boolean isEnabled;
+ private Date changed;
+
+ /**
+ * Creates a new music folder.
+ *
+ * @param id The system-generated ID.
+ * @param path The path of the music folder.
+ * @param name The user-defined name.
+ * @param enabled Whether the folder is enabled.
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public MusicFolder(Integer id, File path, String name, boolean enabled, Date changed) {
+ this.id = id;
+ this.path = path;
+ this.name = name;
+ isEnabled = enabled;
+ this.changed = changed;
+ }
+
+ /**
+ * Creates a new music folder.
+ *
+ * @param path The path of the music folder.
+ * @param name The user-defined name.
+ * @param enabled Whether the folder is enabled.
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public MusicFolder(File path, String name, boolean enabled, Date changed) {
+ this(null, path, name, enabled, changed);
+ }
+
+ /**
+ * Returns the system-generated ID.
+ *
+ * @return The system-generated ID.
+ */
+ public Integer getId() {
+ return id;
+ }
+
+ /**
+ * Returns the path of the music folder.
+ *
+ * @return The path of the music folder.
+ */
+ public File getPath() {
+ return path;
+ }
+
+ /**
+ * Sets the path of the music folder.
+ *
+ * @param path The path of the music folder.
+ */
+ public void setPath(File path) {
+ this.path = path;
+ }
+
+ /**
+ * Returns the user-defined name.
+ *
+ * @return The user-defined name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user-defined name.
+ *
+ * @param name The user-defined name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns whether the folder is enabled.
+ *
+ * @return Whether the folder is enabled.
+ */
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ /**
+ * Sets whether the folder is enabled.
+ *
+ * @param enabled Whether the folder is enabled.
+ */
+ public void setEnabled(boolean enabled) {
+ isEnabled = enabled;
+ }
+
+ /**
+ * Returns when the corresponding database entry was last changed.
+ *
+ * @return When the corresponding database entry was last changed.
+ */
+ public Date getChanged() {
+ return changed;
+ }
+
+ /**
+ * Sets when the corresponding database entry was last changed.
+ *
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public void setChanged(Date changed) {
+ this.changed = changed;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java
new file mode 100644
index 00000000..5753fa4d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java
@@ -0,0 +1,167 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.io.Serializable;
+
+/**
+ * A music index is a mapping from an index string to a list of prefixes. A complete index consists of a list of
+ * <code>MusicIndex</code> instances.<p/>
+ * <p/>
+ * For a normal alphabetical index, such a mapping would typically be <em>"A" -&gt; ["A"]</em>. The index can also be used
+ * to group less frequently used letters, such as <em>"X-&Aring;" -&gt; ["X", "Y", "Z", "&AElig;", "&Oslash;", "&Aring;"]</em>, or to make multiple
+ * indexes for frequently used letters, such as <em>"SA" -&gt; ["SA"]</em> and <em>"SO" -&gt; ["SO"]</em><p/>
+ * <p/>
+ * Clicking on an index in the user interface will typically bring up a list of all music files that are categorized
+ * under that index.
+ *
+ * @author Sindre Mehus
+ */
+public class MusicIndex implements Serializable {
+
+ public static final MusicIndex OTHER = new MusicIndex("#");
+
+ private final String index;
+ private final List<String> prefixes = new ArrayList<String>();
+
+ /**
+ * Creates a new index with the given index string.
+ *
+ * @param index The index string, e.g., "A" or "The".
+ */
+ public MusicIndex(String index) {
+ this.index = index;
+ }
+
+ /**
+ * Adds a prefix to this index. Music files that starts with this prefix will be categorized under this index entry.
+ *
+ * @param prefix The prefix.
+ */
+ public void addPrefix(String prefix) {
+ prefixes.add(prefix);
+ }
+
+ /**
+ * Returns the index name.
+ *
+ * @return The index name.
+ */
+ public String getIndex() {
+ return index;
+ }
+
+ /**
+ * Returns the list of prefixes.
+ *
+ * @return The list of prefixes.
+ */
+ public List<String> getPrefixes() {
+ return prefixes;
+ }
+
+ /**
+ * Returns whether this object is equal to another one.
+ *
+ * @param o Object to compare to.
+ * @return <code>true</code> if, and only if, the other object is a <code>MusicIndex</code> with the same
+ * index name as this one.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof MusicIndex)) {
+ return false;
+ }
+
+ final MusicIndex musicIndex = (MusicIndex) o;
+
+ if (index != null ? !index.equals(musicIndex.index) : musicIndex.index != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a hash code for this object.
+ *
+ * @return A hash code for this object.
+ */
+ @Override
+ public int hashCode() {
+ return (index != null ? index.hashCode() : 0);
+ }
+
+ /**
+ * An artist in an index.
+ */
+ public static class Artist implements Comparable<Artist>, Serializable {
+
+ private final String name;
+ private final String sortableName;
+ private final List<MediaFile> mediaFiles = new ArrayList<MediaFile>();
+
+ public Artist(String name, String sortableName) {
+ this.name = name;
+ this.sortableName = sortableName;
+ }
+
+ public void addMediaFile(MediaFile mediaFile) {
+ mediaFiles.add(mediaFile);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getSortableName() {
+ return sortableName;
+ }
+
+ public List<MediaFile> getMediaFiles() {
+ return mediaFiles;
+ }
+
+ public int compareTo(Artist artist) {
+ return sortableName.compareToIgnoreCase(artist.sortableName);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Artist artist = (Artist) o;
+ return sortableName.equalsIgnoreCase(artist.sortableName);
+ }
+
+ @Override
+ public int hashCode() {
+ return sortableName.hashCode();
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java
new file mode 100644
index 00000000..ef3821a1
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java
@@ -0,0 +1,61 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import com.hoodcomputing.natpmp.MapRequestMessage;
+import com.hoodcomputing.natpmp.NatPmpDevice;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class NATPMPRouter implements Router {
+
+ private final NatPmpDevice device;
+
+ private NATPMPRouter(NatPmpDevice device) {
+ this.device = device;
+ }
+
+ public static NATPMPRouter findRouter() {
+ try {
+ return new NATPMPRouter(new NatPmpDevice(false));
+ } catch (Exception x) {
+ return null;
+ }
+ }
+
+ public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception {
+
+ // Use one week if lease duration is "forever".
+ if (leaseDuration == 0) {
+ leaseDuration = 7 * 24 * 3600;
+ }
+
+ MapRequestMessage map = new MapRequestMessage(true, internalPort, externalPort, leaseDuration, null);
+ device.enqueueMessage(map);
+ device.waitUntilQueueEmpty();
+ }
+
+ public void deletePortMapping(int externalPort, int internalPort) throws Exception {
+ MapRequestMessage map = new MapRequestMessage(true, internalPort, externalPort, 0, null);
+ device.enqueueMessage(map);
+ device.waitUntilQueueEmpty();
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java
new file mode 100644
index 00000000..97748f72
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java
@@ -0,0 +1,417 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sourceforge.subsonic.util.FileUtil;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A playlist is a list of music files that are associated to a remote player.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayQueue {
+
+ private List<MediaFile> files = new ArrayList<MediaFile>();
+ private boolean repeatEnabled;
+ private String name = "(unnamed)";
+ private Status status = Status.PLAYING;
+ private RandomSearchCriteria randomSearchCriteria;
+
+ /**
+ * The index of the current song, or -1 is the end of the playlist is reached.
+ * Note that both the index and the playlist size can be zero.
+ */
+ private int index = 0;
+
+ /**
+ * Used for undo functionality.
+ */
+ private List<MediaFile> filesBackup = new ArrayList<MediaFile>();
+ private int indexBackup = 0;
+
+ /**
+ * Returns the user-defined name of the playlist.
+ *
+ * @return The name of the playlist, or <code>null</code> if no name has been assigned.
+ */
+ public synchronized String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user-defined name of the playlist.
+ *
+ * @param name The name of the playlist.
+ */
+ public synchronized void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the current song in the playlist.
+ *
+ * @return The current song in the playlist, or <code>null</code> if no current song exists.
+ */
+ public synchronized MediaFile getCurrentFile() {
+ if (index == -1 || index == 0 && size() == 0) {
+ setStatus(Status.STOPPED);
+ return null;
+ } else {
+ MediaFile file = files.get(index);
+
+ // Remove file from playlist if it doesn't exist.
+ if (!file.exists()) {
+ files.remove(index);
+ index = Math.max(0, Math.min(index, size() - 1));
+ return getCurrentFile();
+ }
+
+ return file;
+ }
+ }
+
+ /**
+ * Returns all music files in the playlist.
+ *
+ * @return All music files in the playlist.
+ */
+ public synchronized List<MediaFile> getFiles() {
+ return files;
+ }
+
+ /**
+ * Returns the music file at the given index.
+ *
+ * @param index The index.
+ * @return The music file at the given index.
+ * @throws IndexOutOfBoundsException If the index is out of range.
+ */
+ public synchronized MediaFile getFile(int index) {
+ return files.get(index);
+ }
+
+ /**
+ * Skip to the next song in the playlist.
+ */
+ public synchronized void next() {
+ index++;
+
+ // Reached the end?
+ if (index >= size()) {
+ index = isRepeatEnabled() ? 0 : -1;
+ }
+ }
+
+ /**
+ * Returns the number of songs in the playlists.
+ *
+ * @return The number of songs in the playlists.
+ */
+ public synchronized int size() {
+ return files.size();
+ }
+
+ /**
+ * Returns whether the playlist is empty.
+ *
+ * @return Whether the playlist is empty.
+ */
+ public synchronized boolean isEmpty() {
+ return files.isEmpty();
+ }
+
+ /**
+ * Returns the index of the current song.
+ *
+ * @return The index of the current song, or -1 if the end of the playlist is reached.
+ */
+ public synchronized int getIndex() {
+ return index;
+ }
+
+ /**
+ * Sets the index of the current song.
+ *
+ * @param index The index of the current song.
+ */
+ public synchronized void setIndex(int index) {
+ makeBackup();
+ this.index = Math.max(0, Math.min(index, size() - 1));
+ setStatus(Status.PLAYING);
+ }
+
+ /**
+ * Adds one or more music file to the playlist.
+ *
+ * @param append Whether existing songs in the playlist should be kept.
+ * @param mediaFiles The music files to add.
+ * @throws IOException If an I/O error occurs.
+ */
+ public synchronized void addFiles(boolean append, Iterable<MediaFile> mediaFiles) throws IOException {
+ makeBackup();
+ if (!append) {
+ index = 0;
+ files.clear();
+ }
+ for (MediaFile mediaFile : mediaFiles) {
+ files.add(mediaFile);
+ }
+ setStatus(Status.PLAYING);
+ }
+
+ /**
+ * Convenience method, equivalent to {@link #addFiles(boolean, Iterable)}.
+ */
+ public synchronized void addFiles(boolean append, MediaFile... mediaFiles) throws IOException {
+ addFiles(append, Arrays.asList(mediaFiles));
+ }
+
+ /**
+ * Removes the music file at the given index.
+ *
+ * @param index The playlist index.
+ */
+ public synchronized void removeFileAt(int index) {
+ makeBackup();
+ index = Math.max(0, Math.min(index, size() - 1));
+ if (this.index > index) {
+ this.index--;
+ }
+ files.remove(index);
+
+ if (index != -1) {
+ this.index = Math.max(0, Math.min(this.index, size() - 1));
+ }
+ }
+
+ /**
+ * Clears the playlist.
+ */
+ public synchronized void clear() {
+ makeBackup();
+ files.clear();
+ index = 0;
+ }
+
+ /**
+ * Shuffles the playlist.
+ */
+ public synchronized void shuffle() {
+ makeBackup();
+ MediaFile currentFile = getCurrentFile();
+ Collections.shuffle(files);
+ if (currentFile != null) {
+ index = files.indexOf(currentFile);
+ }
+ }
+
+ /**
+ * Sorts the playlist according to the given sort order.
+ */
+ public synchronized void sort(final SortOrder sortOrder) {
+ makeBackup();
+ MediaFile currentFile = getCurrentFile();
+
+ Comparator<MediaFile> comparator = new Comparator<MediaFile>() {
+ public int compare(MediaFile a, MediaFile b) {
+ switch (sortOrder) {
+ case TRACK:
+ Integer trackA = a.getTrackNumber();
+ Integer trackB = b.getTrackNumber();
+ if (trackA == null) {
+ trackA = 0;
+ }
+ if (trackB == null) {
+ trackB = 0;
+ }
+ return trackA.compareTo(trackB);
+
+ case ARTIST:
+ String artistA = StringUtils.trimToEmpty(a.getArtist());
+ String artistB = StringUtils.trimToEmpty(b.getArtist());
+ return artistA.compareTo(artistB);
+
+ case ALBUM:
+ String albumA = StringUtils.trimToEmpty(a.getAlbumName());
+ String albumB = StringUtils.trimToEmpty(b.getAlbumName());
+ return albumA.compareTo(albumB);
+ default:
+ return 0;
+ }
+ }
+ };
+
+ Collections.sort(files, comparator);
+ if (currentFile != null) {
+ index = files.indexOf(currentFile);
+ }
+ }
+
+ /**
+ * Moves the song at the given index one step up.
+ *
+ * @param index The playlist index.
+ */
+ public synchronized void moveUp(int index) {
+ makeBackup();
+ if (index <= 0 || index >= size()) {
+ return;
+ }
+ Collections.swap(files, index, index - 1);
+
+ if (this.index == index) {
+ this.index--;
+ } else if (this.index == index - 1) {
+ this.index++;
+ }
+ }
+
+ /**
+ * Moves the song at the given index one step down.
+ *
+ * @param index The playlist index.
+ */
+ public synchronized void moveDown(int index) {
+ makeBackup();
+ if (index < 0 || index >= size() - 1) {
+ return;
+ }
+ Collections.swap(files, index, index + 1);
+
+ if (this.index == index) {
+ this.index++;
+ } else if (this.index == index + 1) {
+ this.index--;
+ }
+ }
+
+ /**
+ * Returns whether the playlist is repeating.
+ *
+ * @return Whether the playlist is repeating.
+ */
+ public synchronized boolean isRepeatEnabled() {
+ return repeatEnabled;
+ }
+
+ /**
+ * Sets whether the playlist is repeating.
+ *
+ * @param repeatEnabled Whether the playlist is repeating.
+ */
+ public synchronized void setRepeatEnabled(boolean repeatEnabled) {
+ this.repeatEnabled = repeatEnabled;
+ }
+
+ /**
+ * Revert the last operation.
+ */
+ public synchronized void undo() {
+ List<MediaFile> filesTmp = new ArrayList<MediaFile>(files);
+ int indexTmp = index;
+
+ index = indexBackup;
+ files = filesBackup;
+
+ indexBackup = indexTmp;
+ filesBackup = filesTmp;
+ }
+
+ /**
+ * Returns the playlist status.
+ *
+ * @return The playlist status.
+ */
+ public synchronized Status getStatus() {
+ return status;
+ }
+
+ /**
+ * Sets the playlist status.
+ *
+ * @param status The playlist status.
+ */
+ public synchronized void setStatus(Status status) {
+ this.status = status;
+ if (index == -1) {
+ index = Math.max(0, Math.min(index, size() - 1));
+ }
+ }
+
+ /**
+ * Returns the criteria used to generate this random playlist.
+ *
+ * @return The search criteria, or <code>null</code> if this is not a random playlist.
+ */
+ public synchronized RandomSearchCriteria getRandomSearchCriteria() {
+ return randomSearchCriteria;
+ }
+
+ /**
+ * Sets the criteria used to generate this random playlist.
+ *
+ * @param randomSearchCriteria The search criteria, or <code>null</code> if this is not a random playlist.
+ */
+ public synchronized void setRandomSearchCriteria(RandomSearchCriteria randomSearchCriteria) {
+ this.randomSearchCriteria = randomSearchCriteria;
+ }
+
+ /**
+ * Returns the total length in bytes.
+ *
+ * @return The total length in bytes.
+ */
+ public synchronized long length() {
+ long length = 0;
+ for (MediaFile mediaFile : files) {
+ length += mediaFile.getFileSize();
+ }
+ return length;
+ }
+
+ private void makeBackup() {
+ filesBackup = new ArrayList<MediaFile>(files);
+ indexBackup = index;
+ }
+
+ /**
+ * Playlist status.
+ */
+ public enum Status {
+ PLAYING,
+ STOPPED
+ }
+
+ /**
+ * Playlist sort order.
+ */
+ public enum SortOrder {
+ TRACK,
+ ARTIST,
+ ALBUM
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java
new file mode 100644
index 00000000..e1780936
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java
@@ -0,0 +1,338 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Date;
+
+/**
+ * Represens a remote player. A player has a unique ID, a user-defined name, a logged-on user,
+ * miscellaneous identifiers, and an associated playlist.
+ *
+ * @author Sindre Mehus
+ */
+public class Player {
+
+ private String id;
+ private String name;
+ private PlayerTechnology technology = PlayerTechnology.WEB;
+ private String clientId;
+ private String type;
+ private String username;
+ private String ipAddress;
+ private boolean isDynamicIp = true;
+ private boolean isAutoControlEnabled = true;
+ private Date lastSeen;
+ private CoverArtScheme coverArtScheme = CoverArtScheme.MEDIUM;
+ private TranscodeScheme transcodeScheme = TranscodeScheme.OFF;
+ private PlayQueue playQueue;
+
+ /**
+ * Returns the player ID.
+ *
+ * @return The player ID.
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Sets the player ID.
+ *
+ * @param id The player ID.
+ */
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ * Returns the user-defined player name.
+ *
+ * @return The user-defined player name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user-defined player name.
+ *
+ * @param name The user-defined player name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the player "technology", e.g., web, external or jukebox.
+ *
+ * @return The player technology.
+ */
+ public PlayerTechnology getTechnology() {
+ return technology;
+ }
+
+ /**
+ * Returns the third-party client ID (used if this player is managed over the
+ * Subsonic REST API).
+ *
+ * @return The client ID.
+ */
+ public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * Sets the third-party client ID (used if this player is managed over the
+ * Subsonic REST API).
+ *
+ * @param clientId The client ID.
+ */
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ /**
+ * Sets the player "technology", e.g., web, external or jukebox.
+ *
+ * @param technology The player technology.
+ */
+ public void setTechnology(PlayerTechnology technology) {
+ this.technology = technology;
+ }
+
+ public boolean isJukebox() {
+ return technology == PlayerTechnology.JUKEBOX;
+ }
+
+ public boolean isExternal() {
+ return technology == PlayerTechnology.EXTERNAL;
+ }
+
+ public boolean isExternalWithPlaylist() {
+ return technology == PlayerTechnology.EXTERNAL_WITH_PLAYLIST;
+ }
+
+ public boolean isWeb() {
+ return technology == PlayerTechnology.WEB;
+ }
+
+ /**
+ * Returns the player type, e.g., WinAmp, iTunes.
+ *
+ * @return The player type.
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Sets the player type, e.g., WinAmp, iTunes.
+ *
+ * @param type The player type.
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Returns the logged-in user.
+ *
+ * @return The logged-in user.
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Sets the logged-in username.
+ *
+ * @param username The logged-in username.
+ */
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ /**
+ * Returns whether the player is automatically started.
+ *
+ * @return Whether the player is automatically started.
+ */
+ public boolean isAutoControlEnabled() {
+ return isAutoControlEnabled;
+ }
+
+ /**
+ * Sets whether the player is automatically started.
+ *
+ * @param isAutoControlEnabled Whether the player is automatically started.
+ */
+ public void setAutoControlEnabled(boolean isAutoControlEnabled) {
+ this.isAutoControlEnabled = isAutoControlEnabled;
+ }
+
+ /**
+ * Returns the time when the player was last seen.
+ *
+ * @return The time when the player was last seen.
+ */
+ public Date getLastSeen() {
+ return lastSeen;
+ }
+
+ /**
+ * Sets the time when the player was last seen.
+ *
+ * @param lastSeen The time when the player was last seen.
+ */
+ public void setLastSeen(Date lastSeen) {
+ this.lastSeen = lastSeen;
+ }
+
+ /**
+ * Returns the cover art scheme.
+ *
+ * @return The cover art scheme.
+ */
+ public CoverArtScheme getCoverArtScheme() {
+ return coverArtScheme;
+ }
+
+ /**
+ * Sets the cover art scheme.
+ *
+ * @param coverArtScheme The cover art scheme.
+ */
+ public void setCoverArtScheme(CoverArtScheme coverArtScheme) {
+ this.coverArtScheme = coverArtScheme;
+ }
+
+ /**
+ * Returns the transcode scheme.
+ *
+ * @return The transcode scheme.
+ */
+ public TranscodeScheme getTranscodeScheme() {
+ return transcodeScheme;
+ }
+
+ /**
+ * Sets the transcode scheme.
+ *
+ * @param transcodeScheme The transcode scheme.
+ */
+ public void setTranscodeScheme(TranscodeScheme transcodeScheme) {
+ this.transcodeScheme = transcodeScheme;
+ }
+
+ /**
+ * Returns the IP address of the player.
+ *
+ * @return The IP address of the player.
+ */
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ /**
+ * Sets the IP address of the player.
+ *
+ * @param ipAddress The IP address of the player.
+ */
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ /**
+ * Returns whether this player has a dynamic IP address.
+ *
+ * @return Whether this player has a dynamic IP address.
+ */
+ public boolean isDynamicIp() {
+ return isDynamicIp;
+ }
+
+ /**
+ * Sets whether this player has a dynamic IP address.
+ *
+ * @param dynamicIp Whether this player has a dynamic IP address.
+ */
+ public void setDynamicIp(boolean dynamicIp) {
+ isDynamicIp = dynamicIp;
+ }
+
+ /**
+ * Returns the player's playlist.
+ *
+ * @return The player's playlist
+ */
+ public PlayQueue getPlayQueue() {
+ return playQueue;
+ }
+
+ /**
+ * Sets the player's playlist.
+ *
+ * @param playQueue The player's playlist.
+ */
+ public void setPlayQueue(PlayQueue playQueue) {
+ this.playQueue = playQueue;
+ }
+
+ /**
+ * Returns a long description of the player, e.g., <code>Player 3 [admin]</code>
+ *
+ * @return A long description of the player.
+ */
+ public String getDescription() {
+ StringBuilder builder = new StringBuilder();
+ if (name != null) {
+ builder.append(name);
+ } else {
+ builder.append("Player ").append(id);
+ }
+
+ builder.append(" [").append(username).append(']');
+ return builder.toString();
+ }
+
+ /**
+ * Returns a short description of the player, e.g., <code>Player 3</code>
+ *
+ * @return A short description of the player.
+ */
+ public String getShortDescription() {
+ if (StringUtils.isNotBlank(name)) {
+ return name;
+ }
+ return "Player " + id;
+ }
+
+ /**
+ * Returns a string representation of the player.
+ *
+ * @return A string representation of the player.
+ * @see #getDescription()
+ */
+ @Override
+ public String toString() {
+ return getDescription();
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java
new file mode 100644
index 00000000..5ba3ff71
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java
@@ -0,0 +1,49 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Enumeration of player technologies.
+ *
+ * @author Sindre Mehus
+ */
+public enum PlayerTechnology {
+
+ /**
+ * Plays music directly in the web browser using the integrated Flash player.
+ */
+ WEB,
+
+ /**
+ * Plays music in an external player, such as WinAmp or Windows Media Player.
+ */
+ EXTERNAL,
+
+ /**
+ * Same as above, but the playlist is managed by the player, rather than the Subsonic server.
+ * In this mode, skipping within songs is possible.
+ */
+ EXTERNAL_WITH_PLAYLIST,
+
+ /**
+ * Plays music directly on the audio device of the Subsonic server.
+ */
+ JUKEBOX
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.java
new file mode 100644
index 00000000..80555ec7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.java
@@ -0,0 +1,141 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sourceforge.subsonic.util.StringUtil;
+
+import java.util.Date;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Playlist {
+
+ private int id;
+ private String username;
+ private boolean isPublic;
+ private String name;
+ private String comment;
+ private int fileCount;
+ private int durationSeconds;
+ private Date created;
+ private Date changed;
+ private String importedFrom;
+
+ public Playlist() {
+ }
+
+ public Playlist(int id, String username, boolean isPublic, String name, String comment, int fileCount,
+ int durationSeconds, Date created, Date changed, String importedFrom) {
+ this.id = id;
+ this.username = username;
+ this.isPublic = isPublic;
+ this.name = name;
+ this.comment = comment;
+ this.fileCount = fileCount;
+ this.durationSeconds = durationSeconds;
+ this.created = created;
+ this.changed = changed;
+ this.importedFrom = importedFrom;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public boolean isPublic() {
+ return isPublic;
+ }
+
+ public void setPublic(boolean isPublic) {
+ this.isPublic = isPublic;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ public int getFileCount() {
+ return fileCount;
+ }
+
+ public void setFileCount(int fileCount) {
+ this.fileCount = fileCount;
+ }
+
+ public int getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public void setDurationSeconds(int durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ }
+
+ public String getDurationAsString() {
+ return StringUtil.formatDuration(durationSeconds);
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getChanged() {
+ return changed;
+ }
+
+ public void setChanged(Date changed) {
+ this.changed = changed;
+ }
+
+ public String getImportedFrom() {
+ return importedFrom;
+ }
+
+ public void setImportedFrom(String importedFrom) {
+ this.importedFrom = importedFrom;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java
new file mode 100644
index 00000000..1127a5cb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java
@@ -0,0 +1,96 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * A Podcast channel. Each channel contain several episodes.
+ *
+ * @author Sindre Mehus
+ * @see PodcastEpisode
+ */
+public class PodcastChannel {
+
+ private Integer id;
+ private String url;
+ private String title;
+ private String description;
+ private PodcastStatus status;
+ private String errorMessage;
+
+ public PodcastChannel(Integer id, String url, String title, String description,
+ PodcastStatus status, String errorMessage) {
+ this.id = id;
+ this.url = url;
+ this.title = StringUtil.removeMarkup(title);
+ this.description = StringUtil.removeMarkup(description);
+ this.status = status;
+ this.errorMessage = errorMessage;
+ }
+
+ public PodcastChannel(String url) {
+ this.url = url;
+ status = PodcastStatus.NEW;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public PodcastStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(PodcastStatus status) {
+ this.status = status;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java
new file mode 100644
index 00000000..b5a835cb
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java
@@ -0,0 +1,172 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.Date;
+
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * A Podcast episode belonging to a channel.
+ *
+ * @author Sindre Mehus
+ * @see PodcastChannel
+ */
+public class PodcastEpisode {
+
+ private Integer id;
+ private Integer mediaFileId;
+ private Integer channelId;
+ private String url;
+ private String path;
+ private String title;
+ private String description;
+ private Date publishDate;
+ private String duration;
+ private Long bytesTotal;
+ private Long bytesDownloaded;
+ private PodcastStatus status;
+ private String errorMessage;
+
+ public PodcastEpisode(Integer id, Integer channelId, String url, String path, String title,
+ String description, Date publishDate, String duration, Long length, Long bytesDownloaded,
+ PodcastStatus status, String errorMessage) {
+ this.id = id;
+ this.channelId = channelId;
+ this.url = url;
+ this.path = path;
+ this.title = StringUtil.removeMarkup(title);
+ this.description = StringUtil.removeMarkup(description);
+ this.publishDate = publishDate;
+ this.duration = duration;
+ this.bytesTotal = length;
+ this.bytesDownloaded = bytesDownloaded;
+ this.status = status;
+ this.errorMessage = errorMessage;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public Integer getChannelId() {
+ return channelId;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Date getPublishDate() {
+ return publishDate;
+ }
+
+ public void setPublishDate(Date publishDate) {
+ this.publishDate = publishDate;
+ }
+
+ public String getDuration() {
+ return duration;
+ }
+
+ public void setDuration(String duration) {
+ this.duration = duration;
+ }
+
+ public Long getBytesTotal() {
+ return bytesTotal;
+ }
+
+ public void setBytesTotal(Long bytesTotal) {
+ this.bytesTotal = bytesTotal;
+ }
+
+ public Long getBytesDownloaded() {
+ return bytesDownloaded;
+ }
+
+ public Double getCompletionRate() {
+ if (bytesTotal == null || bytesTotal == 0) {
+ return null;
+ }
+ if (bytesDownloaded == null) {
+ return 0.0;
+ }
+
+ double d = bytesDownloaded;
+ double t = bytesTotal;
+ return d / t;
+ }
+
+ public void setBytesDownloaded(Long bytesDownloaded) {
+ this.bytesDownloaded = bytesDownloaded;
+ }
+
+ public PodcastStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(PodcastStatus status) {
+ this.status = status;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public Integer getMediaFileId() {
+ return mediaFileId;
+ }
+
+ public void setMediaFileId(Integer mediaFileId) {
+ this.mediaFileId = mediaFileId;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java
new file mode 100644
index 00000000..57cad155
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java
@@ -0,0 +1,29 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Enumeration of statuses for {@link PodcastChannel} and
+ * {@link PodcastEpisode}.
+ *
+ * @author Sindre Mehus
+ */
+public enum PodcastStatus {
+ NEW, DOWNLOADING, COMPLETED, ERROR, DELETED, SKIPPED
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java
new file mode 100644
index 00000000..d52cec39
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java
@@ -0,0 +1,70 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Defines criteria used when generating random playlists.
+ *
+ * @author Sindre Mehus
+ * @see net.sourceforge.subsonic.service.SearchService#getRandomSongs
+ */
+public class RandomSearchCriteria {
+ private final int count;
+ private final String genre;
+ private final Integer fromYear;
+ private final Integer toYear;
+ private final Integer musicFolderId;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param count Maximum number of songs to return.
+ * @param genre Only return songs of the given genre. May be <code>null</code>.
+ * @param fromYear Only return songs released after (or in) this year. May be <code>null</code>.
+ * @param toYear Only return songs released before (or in) this year. May be <code>null</code>.
+ * @param musicFolderId Only return songs from this music folder. May be <code>null</code>.
+ */
+ public RandomSearchCriteria(int count, String genre, Integer fromYear, Integer toYear, Integer musicFolderId) {
+ this.count = count;
+ this.genre = genre;
+ this.fromYear = fromYear;
+ this.toYear = toYear;
+ this.musicFolderId = musicFolderId;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public Integer getFromYear() {
+ return fromYear;
+ }
+
+ public Integer getToYear() {
+ return toYear;
+ }
+
+ public Integer getMusicFolderId() {
+ return musicFolderId;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java
new file mode 100644
index 00000000..ede9d19e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java
@@ -0,0 +1,43 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public interface Router {
+
+ /**
+ * Adds a NAT entry on the UPNP device.
+ *
+ * @param externalPort The external port to open on the UPNP device an map on the internal client.
+ * @param internalPort The internal client port where data should be redirected.
+ * @param leaseDuration Seconds the lease duration in seconds, or 0 for an infinite time.
+ */
+ void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception;
+
+ /**
+ * Deletes a NAT entry on the UPNP device.
+ *
+ * @param externalPort The external port of the NAT entry to delete.
+ * @param internalPort The internal port of the NAT entry to delete.
+ */
+ void deletePortMapping(int externalPort, int internalPort) throws Exception;
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java
new file mode 100644
index 00000000..a639e665
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java
@@ -0,0 +1,63 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sbbi.upnp.impls.InternetGatewayDevice;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+/**
+ * @author Sindre Mehus
+ */
+public class SBBIRouter implements Router {
+
+ // The timeout in milliseconds for finding a router device.
+ private static final int DISCOVERY_TIMEOUT = 3000;
+
+ private final InternetGatewayDevice device;
+
+ private SBBIRouter(InternetGatewayDevice device) {
+ this.device = device;
+ }
+
+ public static SBBIRouter findRouter() throws Exception {
+ InternetGatewayDevice[] devices;
+ try {
+ devices = InternetGatewayDevice.getDevices(DISCOVERY_TIMEOUT);
+ } catch (IOException e) {
+ throw new Exception("Could not find router", e);
+ }
+
+ if (devices == null || devices.length == 0) {
+ return null;
+ }
+
+ return new SBBIRouter(devices[0]);
+ }
+
+ public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception {
+ String localIp = InetAddress.getLocalHost().getHostAddress();
+ device.addPortMapping("Subsonic", null, internalPort, externalPort, localIp, leaseDuration, "TCP");
+ }
+
+ public void deletePortMapping(int externalPort, int internal) throws Exception {
+ device.deletePortMapping(null, externalPort, "TCP");
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java
new file mode 100644
index 00000000..f06a6512
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java
@@ -0,0 +1,59 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sourceforge.subsonic.service.MediaScannerService;
+import net.sourceforge.subsonic.service.SearchService;
+
+/**
+ * Defines criteria used when searching.
+ *
+ * @author Sindre Mehus
+ * @see SearchService#search
+ */
+public class SearchCriteria {
+
+ private String query;
+ private int offset;
+ private int count;
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public void setCount(int count) {
+ this.count = count;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.java
new file mode 100644
index 00000000..bf4b370a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.java
@@ -0,0 +1,69 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sourceforge.subsonic.service.MediaScannerService;
+import net.sourceforge.subsonic.service.SearchService;
+
+/**
+ * The outcome of a search.
+ *
+ * @author Sindre Mehus
+ * @see SearchService#search
+ */
+public class SearchResult {
+
+ private final List<MediaFile> mediaFiles = new ArrayList<MediaFile>();
+ private final List<Artist> artists = new ArrayList<Artist>();
+ private final List<Album> albums = new ArrayList<Album>();
+
+ private int offset;
+ private int totalHits;
+
+ public List<MediaFile> getMediaFiles() {
+ return mediaFiles;
+ }
+
+ public List<Artist> getArtists() {
+ return artists;
+ }
+
+ public List<Album> getAlbums() {
+ return albums;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ public int getTotalHits() {
+ return totalHits;
+ }
+
+ public void setTotalHits(int totalHits) {
+ this.totalHits = totalHits;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java
new file mode 100644
index 00000000..7494769b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java
@@ -0,0 +1,100 @@
+package net.sourceforge.subsonic.domain;
+
+import java.util.Date;
+
+/**
+ * A collection of media files that is shared with someone, and accessible via a direct URL.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class Share {
+
+ private int id;
+ private String name;
+ private String description;
+ private String username;
+ private Date created;
+ private Date expires;
+ private Date lastVisited;
+ private int visitCount;
+
+ public Share() {
+ }
+
+ public Share(int id, String name, String description, String username, Date created,
+ Date expires, Date lastVisited, int visitCount) {
+ this.id = id;
+ this.name = name;
+ this.description = description;
+ this.username = username;
+ this.created = created;
+ this.expires = expires;
+ this.lastVisited = lastVisited;
+ this.visitCount = visitCount;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getExpires() {
+ return expires;
+ }
+
+ public void setExpires(Date expires) {
+ this.expires = expires;
+ }
+
+ public Date getLastVisited() {
+ return lastVisited;
+ }
+
+ public void setLastVisited(Date lastVisited) {
+ this.lastVisited = lastVisited;
+ }
+
+ public int getVisitCount() {
+ return visitCount;
+ }
+
+ public void setVisitCount(int visitCount) {
+ this.visitCount = visitCount;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java
new file mode 100644
index 00000000..f8bd66bd
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java
@@ -0,0 +1,42 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Contains the ID and name for a theme.
+ *
+ * @author Sindre Mehus
+ */
+public class Theme {
+ private String id;
+ private String name;
+
+ public Theme(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java
new file mode 100644
index 00000000..f45b452a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java
@@ -0,0 +1,104 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Enumeration of transcoding schemes. Transcoding is the process of
+ * converting an audio stream to a lower bit rate.
+ *
+ * @author Sindre Mehus
+ */
+public enum TranscodeScheme {
+
+ OFF(0),
+ MAX_32(32),
+ MAX_40(40),
+ MAX_48(48),
+ MAX_56(56),
+ MAX_64(64),
+ MAX_80(80),
+ MAX_96(96),
+ MAX_112(112),
+ MAX_128(128),
+ MAX_160(160),
+ MAX_192(192),
+ MAX_224(224),
+ MAX_256(256),
+ MAX_320(320);
+
+ private int maxBitRate;
+
+ TranscodeScheme(int maxBitRate) {
+ this.maxBitRate = maxBitRate;
+ }
+
+ /**
+ * Returns the maximum bit rate for this transcoding scheme.
+ *
+ * @return The maximum bit rate for this transcoding scheme.
+ */
+ public int getMaxBitRate() {
+ return maxBitRate;
+ }
+
+ /**
+ * Returns the strictest transcode scheme (i.e., the scheme with the lowest max bitrate).
+ *
+ * @param other The other transcode scheme. May be <code>null</code>, in which case 'this' is returned.
+ * @return The strictest scheme.
+ */
+ public TranscodeScheme strictest(TranscodeScheme other) {
+ if (other == null || other == TranscodeScheme.OFF) {
+ return this;
+ }
+
+ if (this == TranscodeScheme.OFF) {
+ return other;
+ }
+
+ return maxBitRate < other.maxBitRate ? this : other;
+ }
+
+ /**
+ * Returns a human-readable string representation of this object.
+ *
+ * @return A human-readable string representation of this object.
+ */
+ public String toString() {
+ if (this == OFF) {
+ return "No limit";
+ }
+ return "" + getMaxBitRate() + " Kbps";
+ }
+
+ /**
+ * Returns the enum constant which corresponds to the given max bit rate.
+ *
+ * @param maxBitRate The max bit rate.
+ * @return The corresponding enum, or <code>null</code> if not found.
+ */
+ public static TranscodeScheme valueOf(int maxBitRate) {
+ for (TranscodeScheme scheme : values()) {
+ if (scheme.getMaxBitRate() == maxBitRate) {
+ return scheme;
+ }
+ }
+ return null;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java
new file mode 100644
index 00000000..57c8316f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java
@@ -0,0 +1,221 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Contains the configuration for a transcoding, i.e., a specification of how a given media format
+ * should be converted to another.
+ * <br/>
+ * A transcoding may contain up to three steps. Typically you need to convert in several steps, for
+ * instance from OGG to WAV to MP3.
+ *
+ * @author Sindre Mehus
+ */
+public class Transcoding {
+
+ private Integer id;
+ private String name;
+ private String sourceFormats;
+ private String targetFormat;
+ private String step1;
+ private String step2;
+ private String step3;
+ private boolean defaultActive;
+
+ /**
+ * Creates a new transcoding specification.
+ *
+ * @param id The system-generated ID.
+ * @param name The user-defined name.
+ * @param sourceFormats The source formats, e.g., "ogg wav aac".
+ * @param targetFormat The target format, e.g., "mp3".
+ * @param step1 The command to execute in step 1.
+ * @param step2 The command to execute in step 2.
+ * @param step3 The command to execute in step 3.
+ * @param defaultActive Whether the transcoding should be automatically activated for all players.
+ */
+ public Transcoding(Integer id, String name, String sourceFormats, String targetFormat, String step1,
+ String step2, String step3, boolean defaultActive) {
+ this.id = id;
+ this.name = name;
+ this.sourceFormats = sourceFormats;
+ this.targetFormat = targetFormat;
+ this.step1 = step1;
+ this.step2 = step2;
+ this.step3 = step3;
+ this.defaultActive = defaultActive;
+ }
+
+ /**
+ * Returns the system-generated ID.
+ *
+ * @return The system-generated ID.
+ */
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ /**
+ * Returns the user-defined name.
+ *
+ * @return The user-defined name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user-defined name.
+ *
+ * @param name The user-defined name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the source format, e.g., "ogg wav aac".
+ *
+ * @return The source format, e.g., "ogg wav aac".
+ */
+ public String getSourceFormats() {
+ return sourceFormats;
+ }
+
+ public String[] getSourceFormatsAsArray() {
+ return StringUtil.split(sourceFormats);
+ }
+
+ /**
+ * Sets the source formats, e.g., "ogg wav aac".
+ *
+ * @param sourceFormats The source formats, e.g., "ogg wav aac".
+ */
+ public void setSourceFormats(String sourceFormats) {
+ this.sourceFormats = sourceFormats;
+ }
+
+ /**
+ * Returns the target format, e.g., mp3.
+ *
+ * @return The target format, e.g., mp3.
+ */
+ public String getTargetFormat() {
+ return targetFormat;
+ }
+
+ /**
+ * Sets the target format, e.g., mp3.
+ *
+ * @param targetFormat The target format, e.g., mp3.
+ */
+ public void setTargetFormat(String targetFormat) {
+ this.targetFormat = targetFormat;
+ }
+
+ /**
+ * Returns the command to execute in step 1.
+ *
+ * @return The command to execute in step 1.
+ */
+ public String getStep1() {
+ return step1;
+ }
+
+ /**
+ * Sets the command to execute in step 1.
+ *
+ * @param step1 The command to execute in step 1.
+ */
+ public void setStep1(String step1) {
+ this.step1 = step1;
+ }
+
+ /**
+ * Returns the command to execute in step 2.
+ *
+ * @return The command to execute in step 2.
+ */
+ public String getStep2() {
+ return step2;
+ }
+
+ /**
+ * Sets the command to execute in step 2.
+ *
+ * @param step2 The command to execute in step 2.
+ */
+ public void setStep2(String step2) {
+ this.step2 = step2;
+ }
+
+ /**
+ * Returns the command to execute in step 3.
+ *
+ * @return The command to execute in step 3.
+ */
+ public String getStep3() {
+ return step3;
+ }
+
+ /**
+ * Sets the command to execute in step 3.
+ *
+ * @param step3 The command to execute in step 3.
+ */
+ public void setStep3(String step3) {
+ this.step3 = step3;
+ }
+
+ /**
+ * Returns whether the transcoding should be automatically activated for all players
+ */
+ public boolean isDefaultActive() {
+ return defaultActive;
+ }
+
+ /**
+ * Sets whether the transcoding should be automatically activated for all players
+ */
+ public void setDefaultActive(boolean defaultActive) {
+ this.defaultActive = defaultActive;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Transcoding that = (Transcoding) o;
+ return !(id != null ? !id.equals(that.id) : that.id != null);
+ }
+
+ public int hashCode() {
+ return (id != null ? id.hashCode() : 0);
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java
new file mode 100644
index 00000000..06930ae3
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java
@@ -0,0 +1,303 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.io.File;
+
+import net.sourceforge.subsonic.util.BoundedList;
+
+/**
+ * Status for a single transfer (stream, download or upload).
+ *
+ * @author Sindre Mehus
+ */
+public class TransferStatus {
+
+ private static final int HISTORY_LENGTH = 200;
+ private static final long SAMPLE_INTERVAL_MILLIS = 5000;
+
+ private Player player;
+ private File file;
+ private long bytesTransfered;
+ private long bytesSkipped;
+ private long bytesTotal;
+ private final SampleHistory history = new SampleHistory();
+ private boolean terminated;
+ private boolean active = true;
+
+ /**
+ * Return the number of bytes transferred.
+ *
+ * @return The number of bytes transferred.
+ */
+ public synchronized long getBytesTransfered() {
+ return bytesTransfered;
+ }
+
+ /**
+ * Adds the given byte count to the total number of bytes transferred.
+ *
+ * @param byteCount The byte count.
+ */
+ public synchronized void addBytesTransfered(long byteCount) {
+ setBytesTransfered(bytesTransfered + byteCount);
+ }
+
+ /**
+ * Sets the number of bytes transferred.
+ *
+ * @param bytesTransfered The number of bytes transferred.
+ */
+ public synchronized void setBytesTransfered(long bytesTransfered) {
+ this.bytesTransfered = bytesTransfered;
+ createSample(bytesTransfered, false);
+ }
+
+ private void createSample(long bytesTransfered, boolean force) {
+ long now = System.currentTimeMillis();
+
+ if (history.isEmpty()) {
+ history.add(new Sample(bytesTransfered, now));
+ } else {
+ Sample lastSample = history.getLast();
+ if (force || now - lastSample.getTimestamp() > TransferStatus.SAMPLE_INTERVAL_MILLIS) {
+ history.add(new Sample(bytesTransfered, now));
+ }
+ }
+ }
+
+ /**
+ * Returns the number of milliseconds since the transfer status was last updated.
+ *
+ * @return Number of milliseconds, or <code>0</code> if never updated.
+ */
+ public synchronized long getMillisSinceLastUpdate() {
+ if (history.isEmpty()) {
+ return 0L;
+ }
+ return System.currentTimeMillis() - history.getLast().timestamp;
+ }
+
+ /**
+ * Returns the total number of bytes, or 0 if unknown.
+ *
+ * @return The total number of bytes, or 0 if unknown.
+ */
+ public long getBytesTotal() {
+ return bytesTotal;
+ }
+
+ /**
+ * Sets the total number of bytes, or 0 if unknown.
+ *
+ * @param bytesTotal The total number of bytes, or 0 if unknown.
+ */
+ public void setBytesTotal(long bytesTotal) {
+ this.bytesTotal = bytesTotal;
+ }
+
+ /**
+ * Returns the number of bytes that has been skipped (for instance when
+ * resuming downloads).
+ *
+ * @return The number of skipped bytes.
+ */
+ public synchronized long getBytesSkipped() {
+ return bytesSkipped;
+ }
+
+ /**
+ * Sets the number of bytes that has been skipped (for instance when
+ * resuming downloads).
+ *
+ * @param bytesSkipped The number of skipped bytes.
+ */
+ public synchronized void setBytesSkipped(long bytesSkipped) {
+ this.bytesSkipped = bytesSkipped;
+ }
+
+
+ /**
+ * Adds the given byte count to the total number of bytes skipped.
+ *
+ * @param byteCount The byte count.
+ */
+ public synchronized void addBytesSkipped(long byteCount) {
+ bytesSkipped += byteCount;
+ }
+
+ /**
+ * Returns the file that is currently being transferred.
+ *
+ * @return The file that is currently being transferred.
+ */
+ public synchronized File getFile() {
+ return file;
+ }
+
+ /**
+ * Sets the file that is currently being transferred.
+ *
+ * @param file The file that is currently being transferred.
+ */
+ public synchronized void setFile(File file) {
+ this.file = file;
+ }
+
+ /**
+ * Returns the remote player for the stream.
+ *
+ * @return The remote player for the stream.
+ */
+ public synchronized Player getPlayer() {
+ return player;
+ }
+
+ /**
+ * Sets the remote player for the stream.
+ *
+ * @param player The remote player for the stream.
+ */
+ public synchronized void setPlayer(Player player) {
+ this.player = player;
+ }
+
+ /**
+ * Returns a history of samples for the stream
+ *
+ * @return A (copy of) the history list of samples.
+ */
+ public synchronized SampleHistory getHistory() {
+ return new SampleHistory(history);
+ }
+
+ /**
+ * Returns the history length in milliseconds.
+ *
+ * @return The history length in milliseconds.
+ */
+ public long getHistoryLengthMillis() {
+ return TransferStatus.SAMPLE_INTERVAL_MILLIS * (TransferStatus.HISTORY_LENGTH - 1);
+ }
+
+ /**
+ * Indicate that the stream should be terminated.
+ */
+ public void terminate() {
+ terminated = true;
+ }
+
+ /**
+ * Returns whether this stream has been terminated.
+ * Not that the <em>terminated status</em> is cleared by this method.
+ *
+ * @return Whether this stream has been terminated.
+ */
+ public boolean terminated() {
+ boolean result = terminated;
+ terminated = false;
+ return result;
+ }
+
+ /**
+ * Returns whether this transfer is active, i.e., if the connection is still established.
+ *
+ * @return Whether this transfer is active.
+ */
+ public boolean isActive() {
+ return active;
+ }
+
+ /**
+ * Sets whether this transfer is active, i.e., if the connection is still established.
+ *
+ * @param active Whether this transfer is active.
+ */
+ public void setActive(boolean active) {
+ this.active = active;
+
+ if (active) {
+ setBytesSkipped(0L);
+ setBytesTotal(0L);
+ setBytesTransfered(0L);
+ } else {
+ createSample(getBytesTransfered(), true);
+ }
+ }
+
+ /**
+ * A sample containing a timestamp and the number of bytes transferred up to that point in time.
+ */
+ public static class Sample {
+ private long bytesTransfered;
+ private long timestamp;
+
+ /**
+ * Creates a new sample.
+ *
+ * @param bytesTransfered The total number of bytes transferred.
+ * @param timestamp A point in time, in milliseconds.
+ */
+ public Sample(long bytesTransfered, long timestamp) {
+ this.bytesTransfered = bytesTransfered;
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * Returns the number of bytes transferred.
+ *
+ * @return The number of bytes transferred.
+ */
+ public long getBytesTransfered() {
+ return bytesTransfered;
+ }
+
+ /**
+ * Returns the timestamp of the sample.
+ *
+ * @return The timestamp in milliseconds.
+ */
+ public long getTimestamp() {
+ return timestamp;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("TransferStatus-").append(hashCode()).append(" [player: ").append(player.getId()).append(", file: ");
+ builder.append(file).append(", terminated: ").append(terminated).append(", active: ").append(active).append("]");
+ return builder.toString();
+ }
+
+ /**
+ * Contains recent history of samples.
+ */
+ public static class SampleHistory extends BoundedList<Sample> {
+
+ public SampleHistory() {
+ super(HISTORY_LENGTH);
+ }
+
+ public SampleHistory(SampleHistory other) {
+ super(HISTORY_LENGTH);
+ addAll(other);
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java
new file mode 100644
index 00000000..95e51004
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java
@@ -0,0 +1,245 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Represent a user.
+ *
+ * @author Sindre Mehus
+ */
+public class User {
+
+ public static final String USERNAME_ADMIN = "admin";
+
+ private final String username;
+ private String password;
+ private String email;
+ private boolean ldapAuthenticated;
+ private long bytesStreamed;
+ private long bytesDownloaded;
+ private long bytesUploaded;
+
+ private boolean isAdminRole;
+ private boolean isSettingsRole;
+ private boolean isDownloadRole;
+ private boolean isUploadRole;
+ private boolean isPlaylistRole;
+ private boolean isCoverArtRole;
+ private boolean isCommentRole;
+ private boolean isPodcastRole;
+ private boolean isStreamRole;
+ private boolean isJukeboxRole;
+ private boolean isShareRole;
+
+ public User(String username, String password, String email, boolean ldapAuthenticated,
+ long bytesStreamed, long bytesDownloaded, long bytesUploaded) {
+ this.username = username;
+ this.password = password;
+ this.email = email;
+ this.ldapAuthenticated = ldapAuthenticated;
+ this.bytesStreamed = bytesStreamed;
+ this.bytesDownloaded = bytesDownloaded;
+ this.bytesUploaded = bytesUploaded;
+ }
+
+ public User(String username, String password, String email) {
+ this(username, password, email, false, 0, 0, 0);
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public boolean isLdapAuthenticated() {
+ return ldapAuthenticated;
+ }
+
+ public void setLdapAuthenticated(boolean ldapAuthenticated) {
+ this.ldapAuthenticated = ldapAuthenticated;
+ }
+
+ public long getBytesStreamed() {
+ return bytesStreamed;
+ }
+
+ public void setBytesStreamed(long bytesStreamed) {
+ this.bytesStreamed = bytesStreamed;
+ }
+
+ public long getBytesDownloaded() {
+ return bytesDownloaded;
+ }
+
+ public void setBytesDownloaded(long bytesDownloaded) {
+ this.bytesDownloaded = bytesDownloaded;
+ }
+
+ public long getBytesUploaded() {
+ return bytesUploaded;
+ }
+
+ public void setBytesUploaded(long bytesUploaded) {
+ this.bytesUploaded = bytesUploaded;
+ }
+
+ public boolean isAdminRole() {
+ return isAdminRole;
+ }
+
+ public void setAdminRole(boolean isAdminRole) {
+ this.isAdminRole = isAdminRole;
+ }
+
+ public boolean isSettingsRole() {
+ return isSettingsRole;
+ }
+
+ public void setSettingsRole(boolean isSettingsRole) {
+ this.isSettingsRole = isSettingsRole;
+ }
+
+ public boolean isCommentRole() {
+ return isCommentRole;
+ }
+
+ public void setCommentRole(boolean isCommentRole) {
+ this.isCommentRole = isCommentRole;
+ }
+
+ public boolean isDownloadRole() {
+ return isDownloadRole;
+ }
+
+ public void setDownloadRole(boolean isDownloadRole) {
+ this.isDownloadRole = isDownloadRole;
+ }
+
+ public boolean isUploadRole() {
+ return isUploadRole;
+ }
+
+ public void setUploadRole(boolean isUploadRole) {
+ this.isUploadRole = isUploadRole;
+ }
+
+ public boolean isPlaylistRole() {
+ return isPlaylistRole;
+ }
+
+ public void setPlaylistRole(boolean isPlaylistRole) {
+ this.isPlaylistRole = isPlaylistRole;
+ }
+
+ public boolean isCoverArtRole() {
+ return isCoverArtRole;
+ }
+
+ public void setCoverArtRole(boolean isCoverArtRole) {
+ this.isCoverArtRole = isCoverArtRole;
+ }
+
+ public boolean isPodcastRole() {
+ return isPodcastRole;
+ }
+
+ public void setPodcastRole(boolean isPodcastRole) {
+ this.isPodcastRole = isPodcastRole;
+ }
+
+ public boolean isStreamRole() {
+ return isStreamRole;
+ }
+
+ public void setStreamRole(boolean streamRole) {
+ isStreamRole = streamRole;
+ }
+
+ public boolean isJukeboxRole() {
+ return isJukeboxRole;
+ }
+
+ public void setJukeboxRole(boolean jukeboxRole) {
+ isJukeboxRole = jukeboxRole;
+ }
+
+ public boolean isShareRole() {
+ return isShareRole;
+ }
+
+ public void setShareRole(boolean shareRole) {
+ isShareRole = shareRole;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer result = new StringBuffer(username);
+
+ if (isAdminRole) {
+ result.append(" [admin]");
+ }
+ if (isSettingsRole) {
+ result.append(" [settings]");
+ }
+ if (isDownloadRole) {
+ result.append(" [download]");
+ }
+ if (isUploadRole) {
+ result.append(" [upload]");
+ }
+ if (isPlaylistRole) {
+ result.append(" [playlist]");
+ }
+ if (isCoverArtRole) {
+ result.append(" [coverart]");
+ }
+ if (isCommentRole) {
+ result.append(" [comment]");
+ }
+ if (isPodcastRole) {
+ result.append(" [podcast]");
+ }
+ if (isStreamRole) {
+ result.append(" [stream]");
+ }
+ if (isJukeboxRole) {
+ result.append(" [jukebox]");
+ }
+ if (isShareRole) {
+ result.append(" [share]");
+ }
+
+ return result.toString();
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java
new file mode 100644
index 00000000..856591bc
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java
@@ -0,0 +1,328 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import java.util.*;
+
+/**
+ * Represent user-specific settings.
+ *
+ * @author Sindre Mehus
+ */
+public class UserSettings {
+
+ private String username;
+ private Locale locale;
+ private String themeId;
+ private boolean showNowPlayingEnabled;
+ private boolean showChatEnabled;
+ private boolean finalVersionNotificationEnabled;
+ private boolean betaVersionNotificationEnabled;
+ private Visibility mainVisibility = new Visibility();
+ private Visibility playlistVisibility = new Visibility();
+ private boolean lastFmEnabled;
+ private String lastFmUsername;
+ private String lastFmPassword;
+ private TranscodeScheme transcodeScheme = TranscodeScheme.OFF;
+ private int selectedMusicFolderId = -1;
+ private boolean partyModeEnabled;
+ private boolean nowPlayingAllowed;
+ private AvatarScheme avatarScheme = AvatarScheme.NONE;
+ private Integer systemAvatarId;
+ private Date changed = new Date();
+
+ public UserSettings(String username) {
+ this.username = username;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Locale getLocale() {
+ return locale;
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ public String getThemeId() {
+ return themeId;
+ }
+
+ public void setThemeId(String themeId) {
+ this.themeId = themeId;
+ }
+
+ public boolean isShowNowPlayingEnabled() {
+ return showNowPlayingEnabled;
+ }
+
+ public void setShowNowPlayingEnabled(boolean showNowPlayingEnabled) {
+ this.showNowPlayingEnabled = showNowPlayingEnabled;
+ }
+
+ public boolean isShowChatEnabled() {
+ return showChatEnabled;
+ }
+
+ public void setShowChatEnabled(boolean showChatEnabled) {
+ this.showChatEnabled = showChatEnabled;
+ }
+
+ public boolean isFinalVersionNotificationEnabled() {
+ return finalVersionNotificationEnabled;
+ }
+
+ public void setFinalVersionNotificationEnabled(boolean finalVersionNotificationEnabled) {
+ this.finalVersionNotificationEnabled = finalVersionNotificationEnabled;
+ }
+
+ public boolean isBetaVersionNotificationEnabled() {
+ return betaVersionNotificationEnabled;
+ }
+
+ public void setBetaVersionNotificationEnabled(boolean betaVersionNotificationEnabled) {
+ this.betaVersionNotificationEnabled = betaVersionNotificationEnabled;
+ }
+
+ public Visibility getMainVisibility() {
+ return mainVisibility;
+ }
+
+ public void setMainVisibility(Visibility mainVisibility) {
+ this.mainVisibility = mainVisibility;
+ }
+
+ public Visibility getPlaylistVisibility() {
+ return playlistVisibility;
+ }
+
+ public void setPlaylistVisibility(Visibility playlistVisibility) {
+ this.playlistVisibility = playlistVisibility;
+ }
+
+ public boolean isLastFmEnabled() {
+ return lastFmEnabled;
+ }
+
+ public void setLastFmEnabled(boolean lastFmEnabled) {
+ this.lastFmEnabled = lastFmEnabled;
+ }
+
+ public String getLastFmUsername() {
+ return lastFmUsername;
+ }
+
+ public void setLastFmUsername(String lastFmUsername) {
+ this.lastFmUsername = lastFmUsername;
+ }
+
+ public String getLastFmPassword() {
+ return lastFmPassword;
+ }
+
+ public void setLastFmPassword(String lastFmPassword) {
+ this.lastFmPassword = lastFmPassword;
+ }
+
+ public TranscodeScheme getTranscodeScheme() {
+ return transcodeScheme;
+ }
+
+ public void setTranscodeScheme(TranscodeScheme transcodeScheme) {
+ this.transcodeScheme = transcodeScheme;
+ }
+
+ public int getSelectedMusicFolderId() {
+ return selectedMusicFolderId;
+ }
+
+ public void setSelectedMusicFolderId(int selectedMusicFolderId) {
+ this.selectedMusicFolderId = selectedMusicFolderId;
+ }
+
+ public boolean isPartyModeEnabled() {
+ return partyModeEnabled;
+ }
+
+ public void setPartyModeEnabled(boolean partyModeEnabled) {
+ this.partyModeEnabled = partyModeEnabled;
+ }
+
+ public boolean isNowPlayingAllowed() {
+ return nowPlayingAllowed;
+ }
+
+ public void setNowPlayingAllowed(boolean nowPlayingAllowed) {
+ this.nowPlayingAllowed = nowPlayingAllowed;
+ }
+
+ public AvatarScheme getAvatarScheme() {
+ return avatarScheme;
+ }
+
+ public void setAvatarScheme(AvatarScheme avatarScheme) {
+ this.avatarScheme = avatarScheme;
+ }
+
+ public Integer getSystemAvatarId() {
+ return systemAvatarId;
+ }
+
+ public void setSystemAvatarId(Integer systemAvatarId) {
+ this.systemAvatarId = systemAvatarId;
+ }
+
+ /**
+ * Returns when the corresponding database entry was last changed.
+ *
+ * @return When the corresponding database entry was last changed.
+ */
+ public Date getChanged() {
+ return changed;
+ }
+
+ /**
+ * Sets when the corresponding database entry was last changed.
+ *
+ * @param changed When the corresponding database entry was last changed.
+ */
+ public void setChanged(Date changed) {
+ this.changed = changed;
+ }
+
+ /**
+ * Configuration of what information to display about a song.
+ */
+ public static class Visibility {
+ private int captionCutoff;
+ private boolean isTrackNumberVisible;
+ private boolean isArtistVisible;
+ private boolean isAlbumVisible;
+ private boolean isGenreVisible;
+ private boolean isYearVisible;
+ private boolean isBitRateVisible;
+ private boolean isDurationVisible;
+ private boolean isFormatVisible;
+ private boolean isFileSizeVisible;
+
+ public Visibility() {}
+
+ public Visibility(int captionCutoff, boolean trackNumberVisible, boolean artistVisible, boolean albumVisible,
+ boolean genreVisible, boolean yearVisible, boolean bitRateVisible,
+ boolean durationVisible, boolean formatVisible, boolean fileSizeVisible) {
+ this.captionCutoff = captionCutoff;
+ isTrackNumberVisible = trackNumberVisible;
+ isArtistVisible = artistVisible;
+ isAlbumVisible = albumVisible;
+ isGenreVisible = genreVisible;
+ isYearVisible = yearVisible;
+ isBitRateVisible = bitRateVisible;
+ isDurationVisible = durationVisible;
+ isFormatVisible = formatVisible;
+ isFileSizeVisible = fileSizeVisible;
+ }
+
+ public int getCaptionCutoff() {
+ return captionCutoff;
+ }
+
+ public void setCaptionCutoff(int captionCutoff) {
+ this.captionCutoff = captionCutoff;
+ }
+
+ public boolean isTrackNumberVisible() {
+ return isTrackNumberVisible;
+ }
+
+ public void setTrackNumberVisible(boolean trackNumberVisible) {
+ isTrackNumberVisible = trackNumberVisible;
+ }
+
+ public boolean isArtistVisible() {
+ return isArtistVisible;
+ }
+
+ public void setArtistVisible(boolean artistVisible) {
+ isArtistVisible = artistVisible;
+ }
+
+ public boolean isAlbumVisible() {
+ return isAlbumVisible;
+ }
+
+ public void setAlbumVisible(boolean albumVisible) {
+ isAlbumVisible = albumVisible;
+ }
+
+ public boolean isGenreVisible() {
+ return isGenreVisible;
+ }
+
+ public void setGenreVisible(boolean genreVisible) {
+ isGenreVisible = genreVisible;
+ }
+
+ public boolean isYearVisible() {
+ return isYearVisible;
+ }
+
+ public void setYearVisible(boolean yearVisible) {
+ isYearVisible = yearVisible;
+ }
+
+ public boolean isBitRateVisible() {
+ return isBitRateVisible;
+ }
+
+ public void setBitRateVisible(boolean bitRateVisible) {
+ isBitRateVisible = bitRateVisible;
+ }
+
+ public boolean isDurationVisible() {
+ return isDurationVisible;
+ }
+
+ public void setDurationVisible(boolean durationVisible) {
+ isDurationVisible = durationVisible;
+ }
+
+ public boolean isFormatVisible() {
+ return isFormatVisible;
+ }
+
+ public void setFormatVisible(boolean formatVisible) {
+ isFormatVisible = formatVisible;
+ }
+
+ public boolean isFileSizeVisible() {
+ return isFileSizeVisible;
+ }
+
+ public void setFileSizeVisible(boolean fileSizeVisible) {
+ isFileSizeVisible = fileSizeVisible;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.java
new file mode 100644
index 00000000..c4d42a99
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.java
@@ -0,0 +1,141 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Represents the version number of Subsonic.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $
+ */
+public class Version implements Comparable<Version> {
+ private int major;
+ private int minor;
+ private int beta;
+ private int bugfix;
+
+ /**
+ * Creates a new version instance by parsing the given string.
+ * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public Version(String version) {
+ String[] s = version.split("\\.");
+ major = Integer.valueOf(s[0]);
+ minor = Integer.valueOf(s[1]);
+
+ if (s.length > 2) {
+ if (s[2].contains("beta")) {
+ beta = Integer.valueOf(s[2].replace("beta", ""));
+ } else {
+ bugfix = Integer.valueOf(s[2]);
+ }
+ }
+ }
+
+ public int getMajor() {
+ return major;
+ }
+
+ public int getMinor() {
+ return minor;
+ }
+
+ /**
+ * Return whether this object is equal to another.
+ * @param o Object to compare to.
+ * @return Whether this object is equals to another.
+ */
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Version version = (Version) o;
+
+ if (beta != version.beta) return false;
+ if (bugfix != version.bugfix) return false;
+ if (major != version.major) return false;
+ return minor == version.minor;
+ }
+
+ /**
+ * Returns a hash code for this object.
+ * @return A hash code for this object.
+ */
+ public int hashCode() {
+ int result;
+ result = major;
+ result = 29 * result + minor;
+ result = 29 * result + beta;
+ result = 29 * result + bugfix;
+ return result;
+ }
+
+ /**
+ * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3".
+ */
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(major).append('.').append(minor);
+ if (beta != 0) {
+ buf.append(".beta").append(beta);
+ } else if (bugfix != 0) {
+ buf.append('.').append(bugfix);
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Compares this object with the specified object for order.
+ * @param version The object to compare to.
+ * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or
+ * greater than the specified object.
+ */
+ public int compareTo(Version version) {
+ if (major < version.major) {
+ return -1;
+ } else if (major > version.major) {
+ return 1;
+ }
+
+ if (minor < version.minor) {
+ return -1;
+ } else if (minor > version.minor) {
+ return 1;
+ }
+
+ if (bugfix < version.bugfix) {
+ return -1;
+ } else if (bugfix > version.bugfix) {
+ return 1;
+ }
+
+ int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta;
+ int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta;
+
+ if (thisBeta < otherBeta) {
+ return -1;
+ } else if (thisBeta > otherBeta) {
+ return 1;
+ }
+
+ return 0;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java
new file mode 100644
index 00000000..18661ba4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java
@@ -0,0 +1,50 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+/**
+ * Parameters used when transcoding videos.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class VideoTranscodingSettings {
+
+ private final int width;
+ private final int height;
+ private final int timeOffset;
+
+ public VideoTranscodingSettings(int width, int height, int timeOffset) {
+ this.width = width;
+ this.height = height;
+ this.timeOffset = timeOffset;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getTimeOffset() {
+ return timeOffset;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java
new file mode 100644
index 00000000..e36701e8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java
@@ -0,0 +1,56 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.domain;
+
+import org.wetorrent.upnp.GatewayDevice;
+import org.wetorrent.upnp.GatewayDiscover;
+
+import java.net.InetAddress;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class WeUPnPRouter implements Router {
+ private final GatewayDevice device;
+
+ private WeUPnPRouter(GatewayDevice device) {
+ this.device = device;
+ }
+
+ public static WeUPnPRouter findRouter() throws Exception {
+ GatewayDiscover discover = new GatewayDiscover();
+ discover.discover();
+ GatewayDevice device = discover.getValidGateway();
+ if (device == null) {
+ return null;
+ }
+
+ return new WeUPnPRouter(device);
+ }
+
+ public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception {
+ String localIp = InetAddress.getLocalHost().getHostAddress();
+ device.addPortMapping(externalPort, internalPort, localIp, "TCP", "Subsonic");
+ }
+
+ public void deletePortMapping(int externalPort, int internalPort) throws Exception {
+ device.deletePortMapping(externalPort, "TCP");
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java
new file mode 100644
index 00000000..c93d0603
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java
@@ -0,0 +1,107 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.filter;
+
+import net.sourceforge.subsonic.service.SettingsService;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This filter is executed very early in the filter chain. It verifies that
+ * the Subsonic home directory (c:\subsonic or /var/subsonic) exists and
+ * is writable. If not, a proper error message is given to the user.
+ * <p/>
+ * (The Subsonic home directory is usually created automatically, but a common
+ * problem on Linux is that the Tomcat user does not have the necessary
+ * privileges).
+ *
+ * @author Sindre Mehus
+ */
+public class BootstrapVerificationFilter implements Filter {
+
+ private boolean subsonicHomeVerified = false;
+
+
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws IOException, ServletException {
+
+ // Already verified?
+ if (subsonicHomeVerified) {
+ chain.doFilter(req, res);
+ return;
+ }
+
+ File home = SettingsService.getSubsonicHome();
+ if (!directoryExists(home)) {
+ error(res, "<p>The directory <b>" + home + "</b> does not exist. Please create it and make it writable, " +
+ "then restart the servlet container.</p>" +
+ "<p>(You can override the directory location by specifying -Dsubsonic.home=... when " +
+ "starting the servlet container.)</p>");
+
+ } else if (!directoryWritable(home)) {
+ error(res, "<p>The directory <b>" + home + "</b> is not writable. Please change file permissions, " +
+ "then restart the servlet container.</p>" +
+ "<p>(You can override the directory location by specifying -Dsubsonic.home=... when " +
+ "starting the servlet container.)</p>");
+
+ } else {
+ subsonicHomeVerified = true;
+ chain.doFilter(req, res);
+ }
+ }
+
+ private boolean directoryExists(File dir) {
+ return dir.exists() && dir.isDirectory();
+ }
+
+ private boolean directoryWritable(File dir) {
+ try {
+ File tempFile = File.createTempFile("test", null, dir);
+ tempFile.delete();
+ return true;
+ } catch (IOException x) {
+ return false;
+ }
+ }
+
+ private void error(ServletResponse res, String error) throws IOException {
+ ServletOutputStream out = res.getOutputStream();
+ out.println("<html>" +
+ "<head><title>Subsonic Error</title></head>" +
+ "<body>" +
+ "<h2>Subsonic Error</h2>" +
+ error +
+ "</body>" +
+ "</html>");
+ }
+
+ public void init(FilterConfig filterConfig) {
+ }
+
+ public void destroy() {
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java
new file mode 100644
index 00000000..52a98ad0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java
@@ -0,0 +1,147 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.filter;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.util.StringUtil;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * Servlet filter which decodes HTTP request parameters. If a parameter name ends with
+ * "Utf8Hex" ({@link #PARAM_SUFFIX}) , the corresponding parameter value is assumed to be the
+ * hexadecimal representation of the UTF-8 bytes of the value.
+ * <p/>
+ * Used to support request parameter values of any character encoding.
+ *
+ * @author Sindre Mehus
+ */
+public class ParameterDecodingFilter implements Filter {
+
+ public static final String PARAM_SUFFIX = "Utf8Hex";
+ private static final Logger LOG = Logger.getLogger(ParameterDecodingFilter.class);
+
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+
+ // Wrap request in decoder.
+ ServletRequest decodedRequest = new DecodingServletRequestWrapper((HttpServletRequest) request);
+
+ // Pass the request/response on
+ chain.doFilter(decodedRequest, response);
+ }
+
+ public void init(FilterConfig filterConfig) {
+ }
+
+ public void destroy() {
+ }
+
+ private static class DecodingServletRequestWrapper extends HttpServletRequestWrapper {
+
+ public DecodingServletRequestWrapper(HttpServletRequest servletRequest) {
+ super(servletRequest);
+ }
+
+ @Override
+ public String getParameter(String name) {
+ String[] values = getParameterValues(name);
+ if (values == null || values.length == 0) {
+ return null;
+ }
+ return values[0];
+ }
+
+ @Override
+ public Map getParameterMap() {
+ Map map = super.getParameterMap();
+ Map<String, String[]> result = new HashMap<String, String[]>();
+
+ for (Object o : map.entrySet()) {
+ Map.Entry entry = (Map.Entry) o;
+ String name = (String) entry.getKey();
+ String[] values = (String[]) entry.getValue();
+
+ if (name.endsWith(PARAM_SUFFIX)) {
+ result.put(name.replace(PARAM_SUFFIX, ""), decode(values));
+ } else {
+ result.put(name, values);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Enumeration getParameterNames() {
+ Enumeration e = super.getParameterNames();
+ Vector<String> v = new Vector<String>();
+ while (e.hasMoreElements()) {
+ String name = (String) e.nextElement();
+ if (name.endsWith(PARAM_SUFFIX)) {
+ name = name.replace(PARAM_SUFFIX, "");
+ }
+ v.add(name);
+ }
+
+ return v.elements();
+ }
+
+ @Override
+ public String[] getParameterValues(String name) {
+ String[] values = super.getParameterValues(name);
+ if (values != null) {
+ return values;
+ }
+
+ values = super.getParameterValues(name + PARAM_SUFFIX);
+ if (values != null) {
+ return decode(values);
+ }
+
+ return null;
+ }
+
+ private String[] decode(String[] values) {
+ if (values == null) {
+ return null;
+ }
+
+ String[] result = new String[values.length];
+ for (int i = 0; i < values.length; i++) {
+ try {
+ result[i] = StringUtil.utf8HexDecode(values[i]);
+ } catch (Exception x) {
+ LOG.error("Failed to decode parameter value '" + values[i] + "'");
+ result[i] = values[i];
+ }
+ }
+
+ return result;
+ }
+
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java
new file mode 100644
index 00000000..3b37e8d4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.filter;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+import java.io.*;
+
+/**
+ * Configurable filter for setting the character encoding to use for the HTTP request.
+ * Typically used to set UTF-8 encoding when reading request parameters with non-Latin
+ * content.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.1 $ $Date: 2006/03/01 16:58:08 $
+ */
+public class RequestEncodingFilter implements Filter {
+
+ private String encoding;
+
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletRequest request = (HttpServletRequest) req;
+ request.setCharacterEncoding(encoding);
+
+ // Pass the request/response on
+ chain.doFilter(req, res);
+ }
+
+ public void init(FilterConfig filterConfig) {
+ encoding = filterConfig.getInitParameter("encoding");
+ }
+
+ public void destroy() {
+ encoding = null;
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java
new file mode 100644
index 00000000..33f60f83
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java
@@ -0,0 +1,57 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.filter;
+
+import javax.servlet.*;
+import javax.servlet.http.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * Configurable filter for setting HTTP response headers. Can be used, for instance, to
+ * set cache control directives for certain resources.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.1 $ $Date: 2005/08/14 13:14:47 $
+ */
+public class ResponseHeaderFilter implements Filter {
+ private FilterConfig filterConfig;
+
+ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletResponse response = (HttpServletResponse) res;
+
+ // Sets the provided HTTP response parameters
+ for (Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements();) {
+ String headerName = (String) e.nextElement();
+ response.addHeader(headerName, filterConfig.getInitParameter(headerName));
+ }
+
+ // pass the request/response on
+ chain.doFilter(req, response);
+ }
+
+ public void init(FilterConfig filterConfig) {
+ this.filterConfig = filterConfig;
+ }
+
+ public void destroy() {
+ this.filterConfig = null;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java
new file mode 100644
index 00000000..231ad6e7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java
@@ -0,0 +1,104 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.i18n;
+
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.domain.*;
+import org.springframework.web.servlet.*;
+
+import javax.servlet.http.*;
+import java.util.*;
+
+/**
+ * Locale resolver implementation which returns the locale selected in the settings.
+ *
+ * @author Sindre Mehus
+ */
+public class SubsonicLocaleResolver implements LocaleResolver {
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private Set<Locale> locales;
+
+ /**
+ * Resolve the current locale via the given request.
+ *
+ * @param request Request to be used for resolution.
+ * @return The current locale.
+ */
+ public Locale resolveLocale(HttpServletRequest request) {
+ Locale locale = (Locale) request.getAttribute("subsonic.locale");
+ if (locale != null) {
+ return locale;
+ }
+
+ // Optimization: Cache locale in the request.
+ locale = doResolveLocale(request);
+ request.setAttribute("subsonic.locale", locale);
+
+ return locale;
+ }
+
+ private Locale doResolveLocale(HttpServletRequest request) {
+ Locale locale = null;
+
+ // Look for user-specific locale.
+ String username = securityService.getCurrentUsername(request);
+ if (username != null) {
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ if (userSettings != null) {
+ locale = userSettings.getLocale();
+ }
+ }
+
+ if (locale != null && localeExists(locale)) {
+ return locale;
+ }
+
+ // Return system locale.
+ locale = settingsService.getLocale();
+ return localeExists(locale) ? locale : Locale.ENGLISH;
+ }
+
+ /**
+ * Returns whether the given locale exists.
+ * @param locale The locale.
+ * @return Whether the locale exists.
+ */
+ private synchronized boolean localeExists(Locale locale) {
+ // Lazily create set of locales.
+ if (locales == null) {
+ locales = new HashSet<Locale>(Arrays.asList(settingsService.getAvailableLocales()));
+ }
+
+ return locales.contains(locale);
+ }
+
+ public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
+ throw new UnsupportedOperationException("Cannot change locale - use a different locale resolution strategy");
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java
new file mode 100644
index 00000000..1019f73a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java
@@ -0,0 +1,63 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.io;
+
+import net.sourceforge.subsonic.*;
+import org.apache.commons.io.*;
+
+import java.io.*;
+
+/**
+ * Utility class which reads everything from an input stream and optionally logs it.
+ *
+ * @see TranscodeInputStream
+ * @author Sindre Mehus
+ */
+public class InputStreamReaderThread extends Thread {
+
+ private static final Logger LOG = Logger.getLogger(InputStreamReaderThread.class);
+
+ private InputStream input;
+ private String name;
+ private boolean log;
+
+ public InputStreamReaderThread(InputStream input, String name, boolean log) {
+ super(name + " InputStreamLogger");
+ this.input = input;
+ this.name = name;
+ this.log = log;
+ }
+
+ public void run() {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(input));
+ for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+ if (log) {
+ LOG.debug('(' + name + ") " + line);
+ }
+ }
+ } catch (IOException x) {
+ // Intentionally ignored.
+ } finally {
+ IOUtils.closeQuietly(reader);
+ IOUtils.closeQuietly(input);
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java
new file mode 100644
index 00000000..3be7fdd9
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java
@@ -0,0 +1,154 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.io;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
+import net.sourceforge.subsonic.service.MediaFileService;
+import net.sourceforge.subsonic.service.SearchService;
+import net.sourceforge.subsonic.util.FileUtil;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.service.AudioScrobblerService;
+import net.sourceforge.subsonic.service.TranscodingService;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Implementation of {@link InputStream} which reads from a {@link net.sourceforge.subsonic.domain.PlayQueue}.
+ *
+ * @author Sindre Mehus
+ */
+public class PlayQueueInputStream extends InputStream {
+
+ private static final Logger LOG = Logger.getLogger(PlayQueueInputStream.class);
+
+ private final Player player;
+ private final TransferStatus status;
+ private final Integer maxBitRate;
+ private final String preferredTargetFormat;
+ private final VideoTranscodingSettings videoTranscodingSettings;
+ private final TranscodingService transcodingService;
+ private final AudioScrobblerService audioScrobblerService;
+ private final MediaFileService mediaFileService;
+ private MediaFile currentFile;
+ private InputStream currentInputStream;
+ private SearchService searchService;
+
+ public PlayQueueInputStream(Player player, TransferStatus status, Integer maxBitRate, String preferredTargetFormat,
+ VideoTranscodingSettings videoTranscodingSettings, TranscodingService transcodingService,
+ AudioScrobblerService audioScrobblerService, MediaFileService mediaFileService, SearchService searchService) {
+ this.player = player;
+ this.status = status;
+ this.maxBitRate = maxBitRate;
+ this.preferredTargetFormat = preferredTargetFormat;
+ this.videoTranscodingSettings = videoTranscodingSettings;
+ this.transcodingService = transcodingService;
+ this.audioScrobblerService = audioScrobblerService;
+ this.mediaFileService = mediaFileService;
+ this.searchService = searchService;
+ }
+
+ @Override
+ public int read() throws IOException {
+ byte[] b = new byte[1];
+ int n = read(b);
+ return n == -1 ? -1 : b[0];
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ prepare();
+ if (currentInputStream == null || player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
+ return -1;
+ }
+
+ int n = currentInputStream.read(b, off, len);
+
+ // If end of song reached, skip to next song and call read() again.
+ if (n == -1) {
+ player.getPlayQueue().next();
+ close();
+ return read(b, off, len);
+ } else {
+ status.addBytesTransfered(n);
+ }
+ return n;
+ }
+
+ private void prepare() throws IOException {
+ PlayQueue playQueue = player.getPlayQueue();
+
+ // If playlist is in auto-random mode, populate it with new random songs.
+ if (playQueue.getIndex() == -1 && playQueue.getRandomSearchCriteria() != null) {
+ populateRandomPlaylist(playQueue);
+ }
+
+ MediaFile result;
+ synchronized (playQueue) {
+ result = playQueue.getCurrentFile();
+ }
+ MediaFile file = result;
+ if (file == null) {
+ close();
+ } else if (!file.equals(currentFile)) {
+ close();
+ LOG.info(player.getUsername() + " listening to \"" + FileUtil.getShortPath(file.getFile()) + "\"");
+ mediaFileService.incrementPlayCount(file);
+ if (player.getClientId() == null) { // Don't scrobble REST players.
+ audioScrobblerService.register(file, player.getUsername(), false);
+ }
+
+ TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, videoTranscodingSettings);
+ currentInputStream = transcodingService.getTranscodedInputStream(parameters);
+ currentFile = file;
+ status.setFile(currentFile.getFile());
+ }
+ }
+
+ private void populateRandomPlaylist(PlayQueue playQueue) throws IOException {
+ List<MediaFile> files = searchService.getRandomSongs(playQueue.getRandomSearchCriteria());
+ playQueue.addFiles(false, files);
+ LOG.info("Recreated random playlist with " + playQueue.size() + " songs.");
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ if (currentInputStream != null) {
+ currentInputStream.close();
+ }
+ } finally {
+ if (player.getClientId() == null) { // Don't scrobble REST players.
+ audioScrobblerService.register(currentFile, player.getUsername(), true);
+ }
+ currentInputStream = null;
+ currentFile = null;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java
new file mode 100644
index 00000000..25bc03d2
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java
@@ -0,0 +1,150 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.io;
+
+import org.apache.commons.lang.math.Range;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+
+/**
+ * Special output stream for grabbing only part of a passed stream.
+ *
+ * @author Sindre Mehus (based on code found on http://www.koders.com/
+ */
+public class RangeOutputStream extends FilterOutputStream {
+
+ /**
+ * The starting index.
+ */
+ private long start;
+
+ /**
+ * The ending index.
+ */
+ private long end;
+
+ /**
+ * The current position.
+ */
+ protected long pos;
+
+ /**
+ * Wraps the given output stream in a RangeOutputStream, using the values
+ * in the given range, unless the range is <code>null</code> in which case
+ * the original OutputStream is returned.
+ *
+ * @param out The output stream to wrap in a RangeOutputStream.
+ * @param range The range, may be <code>null</code>.
+ * @return The possibly wrapped output stream.
+ */
+ public static OutputStream wrap(OutputStream out, Range range) {
+ if (range == null) {
+ return out;
+ }
+ return new RangeOutputStream(out, range.getMinimumLong(), range.getMaximumLong());
+ }
+
+ /**
+ * Creates the stream with the passed start and end.
+ *
+ * @param out The stream to write to.
+ * @param start The starting position.
+ * @param end The ending position.
+ */
+ public RangeOutputStream(OutputStream out, long start, long end) {
+ super(out);
+ this.start = start;
+ this.end = end;
+ pos = 0;
+ }
+
+ /**
+ * Writes the byte IF it is within the range, otherwise it only
+ * increments the position.
+ *
+ * @param b The byte to write.
+ * @throws IOException Thrown if there was a problem writing to the stream.
+ */
+ @Override
+ public void write(int b) throws IOException {
+ if ((pos >= start) && (pos <= end)) {
+ super.write(b);
+ }
+ pos++;
+ }
+
+ /**
+ * Writes the bytes IF it is within the range, otherwise it only
+ * increments the position.
+ *
+ * @param b The bytes to write.
+ * @param off The offset to start at.
+ * @param len The length to write.
+ * @throws IOException Thrown if there was a problem writing to the stream.
+ */
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ boolean allowWrite = false;
+ long newPos = pos + off, newOff = off, newLen = len;
+
+ // Check to see if we are in the range
+ if (newPos <= end) {
+ if (newPos >= start) {
+ // We are so check to make sure we don't leave it
+ if (newPos + newLen > end) {
+ newLen = end - newPos;
+ }
+
+ // Enable writing
+ allowWrite = true;
+ }
+
+ // We aren't yet in the range, but if see if the proposed write
+ // would place us there
+ else if (newPos + newLen >= start) {
+ // It would so, update the offset
+ newOff += start - newPos;
+
+ // New offset means, a new position, so update that too
+ newPos = newOff + pos;
+ newLen = len + (pos - newPos);
+
+ // Make sure we don't go past the range
+ if (newPos + newLen > end) {
+ newLen = end - newPos;
+ }
+
+ // Enable writting
+ allowWrite = true;
+ }
+ }
+
+ // If we have enabled writing, do the write!
+ if (allowWrite) {
+ out.write(b, (int) newOff, (int) newLen);
+ }
+
+ // Move the cursor along
+ pos += off + len;
+ }
+}
+
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java
new file mode 100644
index 00000000..9a8618c6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java
@@ -0,0 +1,205 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.io;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Implements SHOUTcast support by decorating an existing output stream.
+ * <p/>
+ * Based on protocol description found on
+ * <em>http://www.smackfu.com/stuff/programming/shoutcast.html</em>
+ *
+ * @author Sindre Mehus
+ */
+public class ShoutCastOutputStream extends OutputStream {
+
+ private static final Logger LOG = Logger.getLogger(ShoutCastOutputStream.class);
+
+ /**
+ * Number of bytes between each SHOUTcast metadata block.
+ */
+ public static final int META_DATA_INTERVAL = 20480;
+
+ /**
+ * The underlying output stream to decorate.
+ */
+ private OutputStream out;
+
+ /**
+ * What to write in the SHOUTcast metadata is fetched from the playlist.
+ */
+ private PlayQueue playQueue;
+
+ /**
+ * Keeps track of the number of bytes written (excluding meta-data). Between 0 and {@link #META_DATA_INTERVAL}.
+ */
+ private int byteCount;
+
+ /**
+ * The last stream title sent.
+ */
+ private String previousStreamTitle;
+
+ private SettingsService settingsService;
+
+ /**
+ * Creates a new SHOUTcast-decorated stream for the given output stream.
+ *
+ * @param out The output stream to decorate.
+ * @param playQueue Meta-data is fetched from this playlist.
+ */
+ public ShoutCastOutputStream(OutputStream out, PlayQueue playQueue, SettingsService settingsService) {
+ this.out = out;
+ this.playQueue = playQueue;
+ this.settingsService = settingsService;
+ }
+
+ /**
+ * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary.
+ */
+ public void write(byte[] b, int off, int len) throws IOException {
+
+ int bytesWritten = 0;
+ while (bytesWritten < len) {
+
+ // 'n' is the number of bytes to write before the next potential meta-data block.
+ int n = Math.min(len - bytesWritten, ShoutCastOutputStream.META_DATA_INTERVAL - byteCount);
+
+ out.write(b, off + bytesWritten, n);
+ bytesWritten += n;
+ byteCount += n;
+
+ // Reached meta-data block?
+ if (byteCount % ShoutCastOutputStream.META_DATA_INTERVAL == 0) {
+ writeMetaData();
+ byteCount = 0;
+ }
+ }
+ }
+
+ /**
+ * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary.
+ */
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /**
+ * Writes the given byte to the underlying stream, adding SHOUTcast meta-data as necessary.
+ */
+ public void write(int b) throws IOException {
+ byte[] buf = new byte[]{(byte) b};
+ write(buf);
+ }
+
+ /**
+ * Flushes the underlying stream.
+ */
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ /**
+ * Closes the underlying stream.
+ */
+ public void close() throws IOException {
+ out.close();
+ }
+
+ private void writeMetaData() throws IOException {
+ String streamTitle = StringUtils.trimToEmpty(settingsService.getWelcomeTitle());
+
+ MediaFile result;
+ synchronized (playQueue) {
+ result = playQueue.getCurrentFile();
+ }
+ MediaFile mediaFile = result;
+ if (mediaFile != null) {
+ streamTitle = mediaFile.getArtist() + " - " + mediaFile.getTitle();
+ }
+
+ byte[] bytes;
+
+ if (streamTitle.equals(previousStreamTitle)) {
+ bytes = new byte[0];
+ } else {
+ try {
+ previousStreamTitle = streamTitle;
+ bytes = createStreamTitle(streamTitle);
+ } catch (UnsupportedEncodingException x) {
+ LOG.warn("Failed to create SHOUTcast meta-data. Ignoring.", x);
+ bytes = new byte[0];
+ }
+ }
+
+ // Length in groups of 16 bytes.
+ int length = bytes.length / 16;
+ if (bytes.length % 16 > 0) {
+ length++;
+ }
+
+ // Write the length as a single byte.
+ out.write(length);
+
+ // Write the message.
+ out.write(bytes);
+
+ // Write padding zero bytes.
+ int padding = length * 16 - bytes.length;
+ for (int i = 0; i < padding; i++) {
+ out.write(0);
+ }
+ }
+
+ private byte[] createStreamTitle(String title) throws UnsupportedEncodingException {
+ // Remove any quotes from the title.
+ title = title.replaceAll("'", "");
+
+ // Convert non-ascii characters to similar ascii characters.
+ for (char[] chars : ShoutCastOutputStream.CHAR_MAP) {
+ title = title.replace(chars[0], chars[1]);
+ }
+
+ title = "StreamTitle='" + title + "';";
+ return title.getBytes("US-ASCII");
+ }
+
+ /**
+ * Maps from miscellaneous accented characters to similar-looking ASCII characters.
+ */
+ private static final char[][] CHAR_MAP = {
+ {'\u00C0', 'A'}, {'\u00C1', 'A'}, {'\u00C2', 'A'}, {'\u00C3', 'A'}, {'\u00C4', 'A'}, {'\u00C5', 'A'}, {'\u00C6', 'A'},
+ {'\u00C8', 'E'}, {'\u00C9', 'E'}, {'\u00CA', 'E'}, {'\u00CB', 'E'}, {'\u00CC', 'I'}, {'\u00CD', 'I'}, {'\u00CE', 'I'},
+ {'\u00CF', 'I'}, {'\u00D2', 'O'}, {'\u00D3', 'O'}, {'\u00D4', 'O'}, {'\u00D5', 'O'}, {'\u00D6', 'O'}, {'\u00D9', 'U'},
+ {'\u00DA', 'U'}, {'\u00DB', 'U'}, {'\u00DC', 'U'}, {'\u00DF', 'B'}, {'\u00E0', 'a'}, {'\u00E1', 'a'}, {'\u00E2', 'a'},
+ {'\u00E3', 'a'}, {'\u00E4', 'a'}, {'\u00E5', 'a'}, {'\u00E6', 'a'}, {'\u00E7', 'c'}, {'\u00E8', 'e'}, {'\u00E9', 'e'},
+ {'\u00EA', 'e'}, {'\u00EB', 'e'}, {'\u00EC', 'i'}, {'\u00ED', 'i'}, {'\u00EE', 'i'}, {'\u00EF', 'i'}, {'\u00F1', 'n'},
+ {'\u00F2', 'o'}, {'\u00F3', 'o'}, {'\u00F4', 'o'}, {'\u00F5', 'o'}, {'\u00F6', 'o'}, {'\u00F8', 'o'}, {'\u00F9', 'u'},
+ {'\u00FA', 'u'}, {'\u00FB', 'u'}, {'\u00FC', 'u'}, {'\u2013', '-'}
+ };
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java
new file mode 100644
index 00000000..b7a5e31e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java
@@ -0,0 +1,124 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.io;
+
+import net.sourceforge.subsonic.*;
+
+import org.apache.commons.io.*;
+
+import java.io.*;
+
+/**
+ * Subclass of {@link InputStream} which provides on-the-fly transcoding.
+ * Instances of <code>TranscodeInputStream</code> can be chained together, for instance to convert
+ * from OGG to WAV to MP3.
+ *
+ * @author Sindre Mehus
+ */
+public class TranscodeInputStream extends InputStream {
+
+ private static final Logger LOG = Logger.getLogger(TranscodeInputStream.class);
+
+ private InputStream processInputStream;
+ private OutputStream processOutputStream;
+ private Process process;
+ private final File tmpFile;
+
+ /**
+ * Creates a transcoded input stream by executing an external process. If <code>in</code> is not null,
+ * data from it is copied to the command.
+ *
+ * @param processBuilder Used to create the external process.
+ * @param in Data to feed to the process. May be {@code null}.
+ * @param tmpFile Temporary file to delete when this stream is closed. May be {@code null}.
+ * @throws IOException If an I/O error occurs.
+ */
+ public TranscodeInputStream(ProcessBuilder processBuilder, final InputStream in, File tmpFile) throws IOException {
+ this.tmpFile = tmpFile;
+
+ StringBuffer buf = new StringBuffer("Starting transcoder: ");
+ for (String s : processBuilder.command()) {
+ buf.append('[').append(s).append("] ");
+ }
+ LOG.debug(buf);
+
+ process = processBuilder.start();
+ processOutputStream = process.getOutputStream();
+ processInputStream = process.getInputStream();
+
+ // Must read stderr from the process, otherwise it may block.
+ final String name = processBuilder.command().get(0);
+ new InputStreamReaderThread(process.getErrorStream(), name, true).start();
+
+ // Copy data in a separate thread
+ if (in != null) {
+ new Thread(name + " TranscodedInputStream copy thread") {
+ public void run() {
+ try {
+ IOUtils.copy(in, processOutputStream);
+ } catch (IOException x) {
+ // Intentionally ignored. Will happen if the remote player closes the stream.
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(processOutputStream);
+ }
+ }
+ }.start();
+ }
+ }
+
+ /**
+ * @see InputStream#read()
+ */
+ public int read() throws IOException {
+ return processInputStream.read();
+ }
+
+ /**
+ * @see InputStream#read(byte[])
+ */
+ public int read(byte[] b) throws IOException {
+ return processInputStream.read(b);
+ }
+
+ /**
+ * @see InputStream#read(byte[], int, int)
+ */
+ public int read(byte[] b, int off, int len) throws IOException {
+ return processInputStream.read(b, off, len);
+ }
+
+ /**
+ * @see InputStream#close()
+ */
+ public void close() throws IOException {
+ IOUtils.closeQuietly(processInputStream);
+ IOUtils.closeQuietly(processOutputStream);
+
+ if (process != null) {
+ process.destroy();
+ }
+
+ if (tmpFile != null) {
+ if (!tmpFile.delete()) {
+ LOG.warn("Failed to delete tmp file: " + tmpFile);
+ }
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java
new file mode 100644
index 00000000..fee4ff2c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java
@@ -0,0 +1,131 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ldap;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.ldap.DefaultInitialDirContextFactory;
+import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch;
+import org.acegisecurity.providers.ldap.LdapAuthenticator;
+import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator;
+import org.acegisecurity.userdetails.ldap.LdapUserDetails;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * LDAP authenticator which uses a delegate {@link BindAuthenticator}, and which
+ * supports dynamically changing LDAP provider URL and search filter.
+ *
+ * @author Sindre Mehus
+ */
+public class SubsonicLdapBindAuthenticator implements LdapAuthenticator {
+
+ private static final Logger LOG = Logger.getLogger(SubsonicLdapBindAuthenticator.class);
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+
+ private long authenticatorTimestamp;
+ private BindAuthenticator delegateAuthenticator;
+
+ public LdapUserDetails authenticate(String username, String password) {
+
+ // LDAP authentication must be enabled on the system.
+ if (!settingsService.isLdapEnabled()) {
+ throw new BadCredentialsException("LDAP authentication disabled.");
+ }
+
+ // User must be defined in Subsonic, unless auto-shadowing is enabled.
+ User user = securityService.getUserByName(username);
+ if (user == null && !settingsService.isLdapAutoShadowing()) {
+ throw new BadCredentialsException("User does not exist.");
+ }
+
+ // LDAP authentication must be enabled for the given user.
+ if (user != null && !user.isLdapAuthenticated()) {
+ throw new BadCredentialsException("LDAP authentication disabled for user.");
+ }
+
+ try {
+ createDelegate();
+ LdapUserDetails details = delegateAuthenticator.authenticate(username, password);
+ if (details != null) {
+ LOG.info("User '" + username + "' successfully authenticated in LDAP. DN: " + details.getDn());
+
+ if (user == null) {
+ User newUser = new User(username, "", null, true, 0L, 0L, 0L);
+ newUser.setStreamRole(true);
+ newUser.setSettingsRole(true);
+ securityService.createUser(newUser);
+ LOG.info("Created local user '" + username + "' for DN " + details.getDn());
+ }
+ }
+
+ return details;
+ } catch (RuntimeException x) {
+ LOG.info("Failed to authenticate user '" + username + "' in LDAP.", x);
+ throw x;
+ }
+ }
+
+ /**
+ * Creates the delegate {@link BindAuthenticator}.
+ */
+ private synchronized void createDelegate() {
+
+ // Only create it if necessary.
+ if (delegateAuthenticator == null || authenticatorTimestamp < settingsService.getSettingsChanged()) {
+
+ DefaultInitialDirContextFactory contextFactory = new DefaultInitialDirContextFactory(settingsService.getLdapUrl());
+
+ String managerDn = settingsService.getLdapManagerDn();
+ String managerPassword = settingsService.getLdapManagerPassword();
+ if (StringUtils.isNotEmpty(managerDn) && StringUtils.isNotEmpty(managerPassword)) {
+ contextFactory.setManagerDn(managerDn);
+ contextFactory.setManagerPassword(managerPassword);
+ }
+
+ Map<String, String> extraEnvVars = new HashMap<String, String>();
+ extraEnvVars.put("java.naming.referral", "follow");
+ contextFactory.setExtraEnvVars(extraEnvVars);
+
+ FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch("", settingsService.getLdapSearchFilter(), contextFactory);
+ userSearch.setSearchSubtree(true);
+ userSearch.setDerefLinkFlag(true);
+
+ delegateAuthenticator = new BindAuthenticator(contextFactory);
+ delegateAuthenticator.setUserSearch(userSearch);
+
+ authenticatorTimestamp = settingsService.getSettingsChanged();
+ }
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java
new file mode 100644
index 00000000..a3b9359e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java
@@ -0,0 +1,50 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.ldap;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.ldap.LdapDataAccessException;
+import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
+import org.acegisecurity.userdetails.UserDetailsService;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.ldap.LdapUserDetails;
+
+/**
+ * An {@link LdapAuthoritiesPopulator} that retrieves the roles from the
+ * database using the {@link UserDetailsService} instead of retrieving the roles
+ * from LDAP. An instance of this class can be configured for the
+ * {@link org.acegisecurity.providers.ldap.LdapAuthenticationProvider} when
+ * authentication should be done using LDAP and authorization using the
+ * information stored in the database.
+ *
+ * @author Thomas M. Hofmann
+ */
+public class UserDetailsServiceBasedAuthoritiesPopulator implements LdapAuthoritiesPopulator {
+
+ private UserDetailsService userDetailsService;
+
+ public GrantedAuthority[] getGrantedAuthorities(LdapUserDetails userDetails) throws LdapDataAccessException {
+ UserDetails details = userDetailsService.loadUserByUsername(userDetails.getUsername());
+ return details.getAuthorities();
+ }
+
+ public void setUserDetailsService(UserDetailsService userDetailsService) {
+ this.userDetailsService = userDetailsService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java
new file mode 100644
index 00000000..add44643
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java
@@ -0,0 +1,246 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.security;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.service.SettingsService;
+import net.sourceforge.subsonic.domain.Version;
+import net.sourceforge.subsonic.controller.RESTController;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.XMLBuilder;
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.context.SecurityContextHolder;
+import org.acegisecurity.providers.ProviderManager;
+import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.web.bind.ServletRequestUtils;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Performs authentication based on credentials being present in the HTTP request parameters. Also checks
+ * API versions and license information.
+ * <p/>
+ * The username should be set in parameter "u", and the password should be set in parameter "p".
+ * The REST protocol version should be set in parameter "v".
+ *
+ * The password can either be in plain text or be UTF-8 hexencoded preceded by "enc:".
+ *
+ * @author Sindre Mehus
+ */
+public class RESTRequestParameterProcessingFilter implements Filter {
+
+ private static final Logger LOG = Logger.getLogger(RESTRequestParameterProcessingFilter.class);
+ private static final long TRIAL_DAYS = 35L;
+
+ private ProviderManager authenticationManager;
+ private SettingsService settingsService;
+
+ /**
+ * {@inheritDoc}
+ */
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ if (!(request instanceof HttpServletRequest)) {
+ throw new ServletException("Can only process HttpServletRequest");
+ }
+ if (!(response instanceof HttpServletResponse)) {
+ throw new ServletException("Can only process HttpServletResponse");
+ }
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ String username = StringUtils.trimToNull(httpRequest.getParameter("u"));
+ String password = decrypt(StringUtils.trimToNull(httpRequest.getParameter("p")));
+ String version = StringUtils.trimToNull(httpRequest.getParameter("v"));
+ String client = StringUtils.trimToNull(httpRequest.getParameter("c"));
+
+ RESTController.ErrorCode errorCode = null;
+
+ // The username and password parameters are not required if the user
+ // was previously authenticated, for example using Basic Auth.
+ Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication();
+ boolean missingCredentials = previousAuth == null && (username == null || password == null);
+ if (missingCredentials || version == null || client == null) {
+ errorCode = RESTController.ErrorCode.MISSING_PARAMETER;
+ }
+
+ if (errorCode == null) {
+ errorCode = checkAPIVersion(version);
+ }
+
+ if (errorCode == null) {
+ errorCode = authenticate(username, password, previousAuth);
+ }
+
+ if (errorCode == null) {
+ String restMethod = StringUtils.substringAfterLast(httpRequest.getRequestURI(), "/");
+ errorCode = checkLicense(client, restMethod);
+ }
+
+ if (errorCode == null) {
+ chain.doFilter(request, response);
+ } else {
+ SecurityContextHolder.getContext().setAuthentication(null);
+ sendErrorXml(httpRequest, httpResponse, errorCode);
+ }
+ }
+
+ private RESTController.ErrorCode checkAPIVersion(String version) {
+ Version serverVersion = new Version(StringUtil.getRESTProtocolVersion());
+ Version clientVersion = new Version(version);
+
+ if (serverVersion.getMajor() > clientVersion.getMajor()) {
+ return RESTController.ErrorCode.PROTOCOL_MISMATCH_CLIENT_TOO_OLD;
+ } else if (serverVersion.getMajor() < clientVersion.getMajor()) {
+ return RESTController.ErrorCode.PROTOCOL_MISMATCH_SERVER_TOO_OLD;
+ } else if (serverVersion.getMinor() < clientVersion.getMinor()) {
+ return RESTController.ErrorCode.PROTOCOL_MISMATCH_SERVER_TOO_OLD;
+ }
+ return null;
+ }
+
+ private RESTController.ErrorCode authenticate(String username, String password, Authentication previousAuth) {
+
+ // Previously authenticated and username not overridden?
+ if (username == null && previousAuth != null) {
+ return null;
+ }
+
+ // Ensure password is given.
+ if (password == null) {
+ return RESTController.ErrorCode.MISSING_PARAMETER;
+ }
+
+ try {
+ UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
+ Authentication authResult = authenticationManager.authenticate(authRequest);
+ SecurityContextHolder.getContext().setAuthentication(authResult);
+// LOG.info("Authentication succeeded for user " + username);
+ } catch (AuthenticationException x) {
+ LOG.info("Authentication failed for user " + username);
+ return RESTController.ErrorCode.NOT_AUTHENTICATED;
+ }
+ return null;
+ }
+
+ private RESTController.ErrorCode checkLicense(String client, String restMethod) {
+ if (settingsService.isLicenseValid()) {
+ return null;
+ }
+
+ if (settingsService.getRESTTrialExpires(client) == null) {
+ Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L);
+ settingsService.setRESTTrialExpires(client, expiryDate);
+ settingsService.save();
+ LOG.info("REST access for client '" + client + "' will expire " + expiryDate);
+ } else if (settingsService.getRESTTrialExpires(client).before(new Date())) {
+
+ // Exception: iPhone clients are allowed to call any method except stream.view and download.view.
+ List<String> iPhoneClients = Arrays.asList("iSub", "zsubsonic");
+ List<String> restrictedMethods = Arrays.asList("stream.view", "download.view");
+ if (iPhoneClients.contains(client) && !restrictedMethods.contains(restMethod)) {
+ return null;
+ }
+
+ LOG.info("REST access for client '" + client + "' has expired.");
+ return RESTController.ErrorCode.NOT_LICENSED;
+ }
+
+ return null;
+ }
+
+ public static String decrypt(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (!s.startsWith("enc:")) {
+ return s;
+ }
+ try {
+ return StringUtil.utf8HexDecode(s.substring(4));
+ } catch (Exception e) {
+ return s;
+ }
+ }
+
+ private void sendErrorXml(HttpServletRequest request, HttpServletResponse response, RESTController.ErrorCode errorCode) throws IOException {
+ String format = ServletRequestUtils.getStringParameter(request, "f", "xml");
+ boolean json = "json".equals(format);
+ boolean jsonp = "jsonp".equals(format);
+ XMLBuilder builder;
+
+ response.setCharacterEncoding(StringUtil.ENCODING_UTF8);
+
+ if (json) {
+ builder = XMLBuilder.createJSONBuilder();
+ response.setContentType("application/json");
+ } else if (jsonp) {
+ builder = XMLBuilder.createJSONPBuilder(request.getParameter("callback"));
+ response.setContentType("text/javascript");
+ } else {
+ builder = XMLBuilder.createXMLBuilder();
+ response.setContentType("text/xml");
+ }
+
+ builder.preamble(StringUtil.ENCODING_UTF8);
+ builder.add("subsonic-response", false,
+ new XMLBuilder.Attribute("xmlns", "http://subsonic.org/restapi"),
+ new XMLBuilder.Attribute("status", "failed"),
+ new XMLBuilder.Attribute("version", StringUtil.getRESTProtocolVersion()));
+
+ builder.add("error", true,
+ new XMLBuilder.Attribute("code", errorCode.getCode()),
+ new XMLBuilder.Attribute("message", errorCode.getMessage()));
+ builder.end();
+ response.getWriter().print(builder);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void destroy() {
+ }
+
+ public void setAuthenticationManager(ProviderManager authenticationManager) {
+ this.authenticationManager = authenticationManager;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java
new file mode 100644
index 00000000..9ae4d765
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java
@@ -0,0 +1,71 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+/**
+ * Provides services for generating ads.
+ *
+ * @author Sindre Mehus
+ */
+public class AdService {
+
+ private final String[] ads = {
+
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=computers_accesories&banner=1CH7VNNWF908JYQPHX82&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=21&l=ur1&category=game_downloads&banner=13PTQH69Q2290VF8SR82&f=ifr' width='125' height='125' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='ad/omakasa.html' width='120' height='240' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=29&l=ur1&category=homeaudiohometheater&banner=0T4YJ6YBNCMJM9GGAK02&f=ifr' width='120' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=21&l=ur1&category=50mp3albums5each&banner=19QT8FZHDHFZDN87C482&f=ifr' width='125' height='125' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=21&l=ur1&category=computers_accesories&banner=0Q1FJ9TBD13SA09DSMR2&f=ifr' width='125' height='125' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=mp3&banner=0TBQHNYNA4B47J02NFG2&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<OBJECT classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab' id='Player_3d36fdd7-b2fa-4dfd-b517-c5efe035a14d' WIDTH='120px' HEIGHT='500px'> <PARAM NAME='movie' VALUE='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8010%2F3d36fdd7-b2fa-4dfd-b517-c5efe035a14d&Operation=GetDisplayTemplate'><PARAM NAME='quality' VALUE='high'><PARAM NAME='bgcolor' VALUE='#FFFFFF'><PARAM NAME='allowscriptaccess' VALUE='always'><embed src='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8010%2F3d36fdd7-b2fa-4dfd-b517-c5efe035a14d&Operation=GetDisplayTemplate' id='Player_3d36fdd7-b2fa-4dfd-b517-c5efe035a14d' quality='high' bgcolor='#ffffff' name='Player_3d36fdd7-b2fa-4dfd-b517-c5efe035a14d' allowscriptaccess='always' type='application/x-shockwave-flash' align='middle' height='500px' width='120px'/> </OBJECT> <NOSCRIPT><A HREF='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8010%2F3d36fdd7-b2fa-4dfd-b517-c5efe035a14d&Operation=NoScript'>Amazon.com Widgets</A></NOSCRIPT>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=kindle&banner=19NTJJCKSX6TY1C567G2&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='ad/omakasa.html' width='120' height='240' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=14&l=ur1&category=electronicsrot&f=ifr' width='160' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<OBJECT classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab' id='Player_3fde1609-804d-46de-8802-2a16321cf533' WIDTH='160px' HEIGHT='400px'> <PARAM NAME='movie' VALUE='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8009%2F3fde1609-804d-46de-8802-2a16321cf533&Operation=GetDisplayTemplate'><PARAM NAME='quality' VALUE='high'><PARAM NAME='bgcolor' VALUE='#FFFFFF'><PARAM NAME='allowscriptaccess' VALUE='always'><embed src='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8009%2F3fde1609-804d-46de-8802-2a16321cf533&Operation=GetDisplayTemplate' id='Player_3fde1609-804d-46de-8802-2a16321cf533' quality='high' bgcolor='#ffffff' name='Player_3fde1609-804d-46de-8802-2a16321cf533' allowscriptaccess='always' type='application/x-shockwave-flash' align='middle' height='400px' width='160px'/> </OBJECT> <NOSCRIPT><A HREF='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8009%2F3fde1609-804d-46de-8802-2a16321cf533&Operation=NoScript'>Amazon.com Widgets</A></NOSCRIPT>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=unboxdigital&banner=10NVPFMW8ACPNX4T4E82&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<OBJECT classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab' id='Player_2e2141ca-ec13-4dc9-88f2-08be95e47e6d' WIDTH='160px' HEIGHT='300px'> <PARAM NAME='movie' VALUE='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8014%2F2e2141ca-ec13-4dc9-88f2-08be95e47e6d&Operation=GetDisplayTemplate'><PARAM NAME='quality' VALUE='high'><PARAM NAME='bgcolor' VALUE='#FFFFFF'><PARAM NAME='allowscriptaccess' VALUE='always'><embed src='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8014%2F2e2141ca-ec13-4dc9-88f2-08be95e47e6d&Operation=GetDisplayTemplate' id='Player_2e2141ca-ec13-4dc9-88f2-08be95e47e6d' quality='high' bgcolor='#ffffff' name='Player_2e2141ca-ec13-4dc9-88f2-08be95e47e6d' allowscriptaccess='always' type='application/x-shockwave-flash' align='middle' height='300px' width='160px'></embed></OBJECT> <NOSCRIPT><A HREF='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8014%2F2e2141ca-ec13-4dc9-88f2-08be95e47e6d&Operation=NoScript'>Amazon.com Widgets</A></NOSCRIPT>",
+ "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=14&l=ur1&category=musicandentertainmentrot&f=ifr' width='160' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://www.subsonic.org/pages/subsonic-ad.jsp' width='180' height='400' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>",
+ "<iframe src='http://www.subsonic.org/pages/zazeen-ad.jsp' width='120' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>"
+ };
+ private int adInterval;
+ private int pageCount;
+ private int adIndex;
+
+ /**
+ * Returns an ad or <code>null</code> if no ad should be displayed.
+ */
+ public String getAd() {
+ if (pageCount++ % adInterval == 0) {
+
+ adIndex = (adIndex + 1) % ads.length;
+ return ads[adIndex];
+ }
+
+ return null;
+ }
+
+ /**
+ * Set by Spring.
+ */
+ public void setAdInterval(int adInterval) {
+ this.adInterval = adInterval;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java
new file mode 100644
index 00000000..9ca402b8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java
@@ -0,0 +1,331 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Provides services for "audioscrobbling", which is the process of
+ * registering what songs are played at www.last.fm.
+ * <p/>
+ * See http://www.last.fm/api/submissions
+ *
+ * @author Sindre Mehus
+ */
+public class AudioScrobblerService {
+
+ private static final Logger LOG = Logger.getLogger(AudioScrobblerService.class);
+ private static final int MAX_PENDING_REGISTRATION = 2000;
+ private static final long MIN_REGISTRATION_INTERVAL = 30000L;
+
+ private RegistrationThread thread;
+ private final Map<String, Long> lastRegistrationTimes = new HashMap<String, Long>();
+ private final LinkedBlockingQueue<RegistrationData> queue = new LinkedBlockingQueue<RegistrationData>();
+
+ private SettingsService settingsService;
+
+ /**
+ * Registers the given media file at www.last.fm. This method returns immediately, the actual registration is done
+ * by a separate thread.
+ *
+ * @param mediaFile The media file to register.
+ * @param username The user which played the music file.
+ * @param submission Whether this is a submission or a now playing notification.
+ */
+ public synchronized void register(MediaFile mediaFile, String username, boolean submission) {
+
+ if (thread == null) {
+ thread = new RegistrationThread();
+ thread.start();
+ }
+
+ if (queue.size() >= MAX_PENDING_REGISTRATION) {
+ LOG.warn("Last.fm scrobbler queue is full. Ignoring " + mediaFile);
+ return;
+ }
+
+ RegistrationData registrationData = createRegistrationData(mediaFile, username, submission);
+ if (registrationData == null) {
+ return;
+ }
+
+ try {
+ queue.put(registrationData);
+ } catch (InterruptedException x) {
+ LOG.warn("Interrupted while queuing Last.fm scrobble.", x);
+ }
+ }
+
+ /**
+ * Returns registration details, or <code>null</code> if not eligible for registration.
+ */
+ private RegistrationData createRegistrationData(MediaFile mediaFile, String username, boolean submission) {
+
+ if (mediaFile == null || mediaFile.isVideo()) {
+ return null;
+ }
+
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ if (!userSettings.isLastFmEnabled() || userSettings.getLastFmUsername() == null || userSettings.getLastFmPassword() == null) {
+ return null;
+ }
+
+ long now = System.currentTimeMillis();
+
+ // Don't register submissions more often than every 30 seconds.
+ if (submission) {
+ Long lastRegistrationTime = lastRegistrationTimes.get(username);
+ if (lastRegistrationTime != null && now - lastRegistrationTime < MIN_REGISTRATION_INTERVAL) {
+ return null;
+ }
+ lastRegistrationTimes.put(username, now);
+ }
+
+ RegistrationData reg = new RegistrationData();
+ reg.username = userSettings.getLastFmUsername();
+ reg.password = userSettings.getLastFmPassword();
+ reg.artist = mediaFile.getArtist();
+ reg.album = mediaFile.getAlbumName();
+ reg.title = mediaFile.getTitle();
+ reg.duration = mediaFile.getDurationSeconds() == null ? 0 : mediaFile.getDurationSeconds();
+ reg.time = new Date(now);
+ reg.submission = submission;
+
+ return reg;
+ }
+
+ /**
+ * Scrobbles the given song data at last.fm, using the protocol defined at http://www.last.fm/api/submissions.
+ *
+ * @param registrationData Registration data for the song.
+ */
+ private void scrobble(RegistrationData registrationData) throws Exception {
+ if (registrationData == null) {
+ return;
+ }
+
+ String[] lines = authenticate(registrationData);
+ if (lines == null) {
+ return;
+ }
+
+ String sessionId = lines[1];
+ String nowPlayingUrl = lines[2];
+ String submissionUrl = lines[3];
+
+ if (registrationData.submission) {
+ lines = registerSubmission(registrationData, sessionId, submissionUrl);
+ } else {
+ lines = registerNowPlaying(registrationData, sessionId, nowPlayingUrl);
+ }
+
+ if (lines[0].startsWith("FAILED")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm: " + lines[0]);
+ } else if (lines[0].startsWith("BADSESSION")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Invalid session.");
+ } else if (lines[0].startsWith("OK")) {
+ LOG.debug("Successfully registered " + (registrationData.submission ? "submission" : "now playing") +
+ " for song '" + registrationData.title + "' for user " + registrationData.username + " at Last.fm.");
+ }
+ }
+
+ /**
+ * Returns the following lines if authentication succeeds:
+ * <p/>
+ * Line 0: Always "OK"
+ * Line 1: Session ID, e.g., "17E61E13454CDD8B68E8D7DEEEDF6170"
+ * Line 2: URL to use for now playing, e.g., "http://post.audioscrobbler.com:80/np_1.2"
+ * Line 3: URL to use for submissions, e.g., "http://post2.audioscrobbler.com:80/protocol_1.2"
+ * <p/>
+ * If authentication fails, <code>null</code> is returned.
+ */
+ private String[] authenticate(RegistrationData registrationData) throws Exception {
+ String clientId = "sub";
+ String clientVersion = "0.1";
+ long timestamp = System.currentTimeMillis() / 1000L;
+ String authToken = calculateAuthenticationToken(registrationData.password, timestamp);
+ String[] lines = executeGetRequest("http://post.audioscrobbler.com/?hs=true&p=1.2.1&c=" + clientId + "&v=" +
+ clientVersion + "&u=" + registrationData.username + "&t=" + timestamp + "&a=" + authToken);
+
+ if (lines[0].startsWith("BANNED")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Client version is banned.");
+ return null;
+ }
+
+ if (lines[0].startsWith("BADAUTH")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Wrong username or password.");
+ return null;
+ }
+
+ if (lines[0].startsWith("BADTIME")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Bad timestamp, please check local clock.");
+ return null;
+ }
+
+ if (lines[0].startsWith("FAILED")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm: " + lines[0]);
+ return null;
+ }
+
+ if (!lines[0].startsWith("OK")) {
+ LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Unknown response: " + lines[0]);
+ return null;
+ }
+
+ return lines;
+ }
+
+ private String[] registerSubmission(RegistrationData registrationData, String sessionId, String url) throws IOException {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("s", sessionId);
+ params.put("a[0]", registrationData.artist);
+ params.put("t[0]", registrationData.title);
+ params.put("i[0]", String.valueOf(registrationData.time.getTime() / 1000L));
+ params.put("o[0]", "P");
+ params.put("r[0]", "");
+ params.put("l[0]", String.valueOf(registrationData.duration));
+ params.put("b[0]", registrationData.album);
+ params.put("n[0]", "");
+ params.put("m[0]", "");
+ return executePostRequest(url, params);
+ }
+
+ private String[] registerNowPlaying(RegistrationData registrationData, String sessionId, String url) throws IOException {
+ Map<String, String> params = new HashMap<String, String>();
+ params.put("s", sessionId);
+ params.put("a", registrationData.artist);
+ params.put("t", registrationData.title);
+ params.put("b", registrationData.album);
+ params.put("l", String.valueOf(registrationData.duration));
+ params.put("n", "");
+ params.put("m", "");
+ return executePostRequest(url, params);
+ }
+
+ private String calculateAuthenticationToken(String password, long timestamp) {
+ return DigestUtils.md5Hex(DigestUtils.md5Hex(password) + timestamp);
+ }
+
+ private String[] executeGetRequest(String url) throws IOException {
+ return executeRequest(new HttpGet(url));
+ }
+
+ private String[] executePostRequest(String url, Map<String, String> parameters) throws IOException {
+ List<NameValuePair> params = new ArrayList<NameValuePair>();
+ for (Map.Entry<String, String> entry : parameters.entrySet()) {
+ params.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
+ }
+
+ HttpPost request = new HttpPost(url);
+ request.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8));
+
+ return executeRequest(request);
+ }
+
+ private String[] executeRequest(HttpUriRequest request) throws IOException {
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 15000);
+
+ try {
+ ResponseHandler<String> responseHandler = new BasicResponseHandler();
+ String response = client.execute(request, responseHandler);
+ return response.split("\\n");
+
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ private class RegistrationThread extends Thread {
+ private RegistrationThread() {
+ super("AudioScrobbler Registration");
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ RegistrationData registrationData = null;
+ try {
+ registrationData = queue.take();
+ scrobble(registrationData);
+ } catch (IOException x) {
+ handleNetworkError(registrationData, x);
+ } catch (Exception x) {
+ LOG.warn("Error in Last.fm registration.", x);
+ }
+ }
+ }
+
+ private void handleNetworkError(RegistrationData registrationData, IOException x) {
+ try {
+ queue.put(registrationData);
+ LOG.info("Last.fm registration for " + registrationData.title +
+ " encountered network error. Will try again later. In queue: " + queue.size(), x);
+ } catch (InterruptedException e) {
+ LOG.error("Failed to reschedule Last.fm registration for " + registrationData.title, e);
+ }
+ try {
+ sleep(15L * 60L * 1000L); // Wait 15 minutes.
+ } catch (InterruptedException e) {
+ LOG.error("Failed to sleep after Last.fm registration failure for " + registrationData.title, e);
+ }
+ }
+ }
+
+ private static class RegistrationData {
+ private String username;
+ private String password;
+ private String artist;
+ private String album;
+ private String title;
+ private int duration;
+ private Date time;
+ public boolean submission;
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java
new file mode 100644
index 00000000..9f2eff22
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java
@@ -0,0 +1,206 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.PlayQueue;
+import net.sourceforge.subsonic.domain.Transcoding;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
+import net.sourceforge.subsonic.service.jukebox.AudioPlayer;
+import net.sourceforge.subsonic.util.FileUtil;
+import org.apache.commons.io.IOUtils;
+
+import java.io.InputStream;
+
+import static net.sourceforge.subsonic.service.jukebox.AudioPlayer.State.EOM;
+
+/**
+ * Plays music on the local audio device.
+ *
+ * @author Sindre Mehus
+ */
+public class JukeboxService implements AudioPlayer.Listener {
+
+ private static final Logger LOG = Logger.getLogger(JukeboxService.class);
+
+ private AudioPlayer audioPlayer;
+ private TranscodingService transcodingService;
+ private AudioScrobblerService audioScrobblerService;
+ private StatusService statusService;
+ private SettingsService settingsService;
+ private SecurityService securityService;
+
+ private Player player;
+ private TransferStatus status;
+ private MediaFile currentPlayingFile;
+ private float gain = 0.5f;
+ private int offset;
+ private MediaFileService mediaFileService;
+
+ /**
+ * Updates the jukebox by starting or pausing playback on the local audio device.
+ *
+ * @param player The player in question.
+ * @param offset Start playing after this many seconds into the track.
+ */
+ public synchronized void updateJukebox(Player player, int offset) throws Exception {
+ User user = securityService.getUserByName(player.getUsername());
+ if (!user.isJukeboxRole()) {
+ LOG.warn(user.getUsername() + " is not authorized for jukebox playback.");
+ return;
+ }
+
+ if (player.getPlayQueue().getStatus() == PlayQueue.Status.PLAYING) {
+ this.player = player;
+ MediaFile result;
+ synchronized (player.getPlayQueue()) {
+ result = player.getPlayQueue().getCurrentFile();
+ }
+ play(result, offset);
+ } else {
+ if (audioPlayer != null) {
+ audioPlayer.pause();
+ }
+ }
+ }
+
+ private synchronized void play(MediaFile file, int offset) {
+ InputStream in = null;
+ try {
+
+ // Resume if possible.
+ boolean sameFile = file != null && file.equals(currentPlayingFile);
+ boolean paused = audioPlayer != null && audioPlayer.getState() == AudioPlayer.State.PAUSED;
+ if (sameFile && paused && offset == 0) {
+ audioPlayer.play();
+ } else {
+ this.offset = offset;
+ if (audioPlayer != null) {
+ audioPlayer.close();
+ if (currentPlayingFile != null) {
+ onSongEnd(currentPlayingFile);
+ }
+ }
+
+ if (file != null) {
+ TranscodingService.Parameters parameters = new TranscodingService.Parameters(file, new VideoTranscodingSettings(0, 0, offset));
+ String command = settingsService.getJukeboxCommand();
+ parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false));
+ in = transcodingService.getTranscodedInputStream(parameters);
+ audioPlayer = new AudioPlayer(in, this);
+ audioPlayer.setGain(gain);
+ audioPlayer.play();
+ onSongStart(file);
+ }
+ }
+
+ currentPlayingFile = file;
+
+ } catch (Exception x) {
+ LOG.error("Error in jukebox: " + x, x);
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ public synchronized void stateChanged(AudioPlayer audioPlayer, AudioPlayer.State state) {
+ if (state == EOM) {
+ player.getPlayQueue().next();
+ MediaFile result;
+ synchronized (player.getPlayQueue()) {
+ result = player.getPlayQueue().getCurrentFile();
+ }
+ play(result, 0);
+ }
+ }
+
+ public synchronized float getGain() {
+ return gain;
+ }
+
+ public synchronized int getPosition() {
+ return audioPlayer == null ? 0 : offset + audioPlayer.getPosition();
+ }
+
+ /**
+ * Returns the player which currently uses the jukebox.
+ *
+ * @return The player, may be {@code null}.
+ */
+ public Player getPlayer() {
+ return player;
+ }
+
+ private void onSongStart(MediaFile file) {
+ LOG.info(player.getUsername() + " starting jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\"");
+ status = statusService.createStreamStatus(player);
+ status.setFile(file.getFile());
+ status.addBytesTransfered(file.getFileSize());
+ mediaFileService.incrementPlayCount(file);
+ scrobble(file, false);
+ }
+
+ private void onSongEnd(MediaFile file) {
+ LOG.info(player.getUsername() + " stopping jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\"");
+ if (status != null) {
+ statusService.removeStreamStatus(status);
+ }
+ scrobble(file, true);
+ }
+
+ private void scrobble(MediaFile file, boolean submission) {
+ if (player.getClientId() == null) { // Don't scrobble REST players.
+ audioScrobblerService.register(file, player.getUsername(), submission);
+ }
+ }
+
+ public synchronized void setGain(float gain) {
+ this.gain = gain;
+ if (audioPlayer != null) {
+ audioPlayer.setGain(gain);
+ }
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+
+ public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
+ this.audioScrobblerService = audioScrobblerService;
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java
new file mode 100644
index 00000000..bc575714
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java
@@ -0,0 +1,614 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sf.ehcache.Ehcache;
+import net.sf.ehcache.Element;
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.AlbumDao;
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.Album;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MediaFileComparator;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser;
+import net.sourceforge.subsonic.service.metadata.MetaData;
+import net.sourceforge.subsonic.service.metadata.MetaDataParser;
+import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory;
+import net.sourceforge.subsonic.util.FileUtil;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import static net.sourceforge.subsonic.domain.MediaFile.MediaType.*;
+
+/**
+ * Provides services for instantiating and caching media files and cover art.
+ *
+ * @author Sindre Mehus
+ */
+public class MediaFileService {
+
+ private static final Logger LOG = Logger.getLogger(MediaFileService.class);
+
+ private Ehcache mediaFileMemoryCache;
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private MediaFileDao mediaFileDao;
+ private AlbumDao albumDao;
+ private MetaDataParserFactory metaDataParserFactory;
+
+ /**
+ * Returns a media file instance for the given file. If possible, a cached value is returned.
+ *
+ * @param file A file on the local file system.
+ * @return A media file instance, or null if not found.
+ * @throws SecurityException If access is denied to the given file.
+ */
+ public MediaFile getMediaFile(File file) {
+ return getMediaFile(file, settingsService.isFastCacheEnabled());
+ }
+
+ /**
+ * Returns a media file instance for the given file. If possible, a cached value is returned.
+ *
+ * @param file A file on the local file system.
+ * @return A media file instance, or null if not found.
+ * @throws SecurityException If access is denied to the given file.
+ */
+ public MediaFile getMediaFile(File file, boolean useFastCache) {
+
+ // Look in fast memory cache first.
+ Element element = mediaFileMemoryCache.get(file);
+ MediaFile result = element == null ? null : (MediaFile) element.getObjectValue();
+ if (result != null) {
+ return result;
+ }
+
+ if (!securityService.isReadAllowed(file)) {
+ throw new SecurityException("Access denied to file " + file);
+ }
+
+ // Secondly, look in database.
+ result = mediaFileDao.getMediaFile(file.getPath());
+ if (result != null) {
+ result = checkLastModified(result, useFastCache);
+ mediaFileMemoryCache.put(new Element(file, result));
+ return result;
+ }
+
+ if (!FileUtil.exists(file)) {
+ return null;
+ }
+ // Not found in database, must read from disk.
+ result = createMediaFile(file);
+
+ // Put in cache and database.
+ mediaFileMemoryCache.put(new Element(file, result));
+ mediaFileDao.createOrUpdateMediaFile(result);
+
+ return result;
+ }
+
+ private MediaFile checkLastModified(MediaFile mediaFile, boolean useFastCache) {
+ if (useFastCache || mediaFile.getChanged().getTime() >= FileUtil.lastModified(mediaFile.getFile())) {
+ return mediaFile;
+ }
+ mediaFile = createMediaFile(mediaFile.getFile());
+ mediaFileDao.createOrUpdateMediaFile(mediaFile);
+ return mediaFile;
+ }
+
+ /**
+ * Returns a media file instance for the given path name. If possible, a cached value is returned.
+ *
+ * @param pathName A path name for a file on the local file system.
+ * @return A media file instance.
+ * @throws SecurityException If access is denied to the given file.
+ */
+ public MediaFile getMediaFile(String pathName) {
+ return getMediaFile(new File(pathName));
+ }
+
+ // TODO: Optimize with memory caching.
+ public MediaFile getMediaFile(int id) {
+ MediaFile mediaFile = mediaFileDao.getMediaFile(id);
+ if (mediaFile == null) {
+ return null;
+ }
+
+ if (!securityService.isReadAllowed(mediaFile.getFile())) {
+ throw new SecurityException("Access denied to file " + mediaFile);
+ }
+
+ return checkLastModified(mediaFile, settingsService.isFastCacheEnabled());
+ }
+
+ public MediaFile getParentOf(MediaFile mediaFile) {
+ if (mediaFile.getParentPath() == null) {
+ return null;
+ }
+ return getMediaFile(mediaFile.getParentPath());
+ }
+
+ public List<MediaFile> getChildrenOf(String parentPath, boolean includeFiles, boolean includeDirectories, boolean sort) {
+ return getChildrenOf(new File(parentPath), includeFiles, includeDirectories, sort);
+ }
+
+ public List<MediaFile> getChildrenOf(File parent, boolean includeFiles, boolean includeDirectories, boolean sort) {
+ return getChildrenOf(getMediaFile(parent), includeFiles, includeDirectories, sort);
+ }
+
+ /**
+ * Returns all media files that are children of a given media file.
+ *
+ * @param includeFiles Whether files should be included in the result.
+ * @param includeDirectories Whether directories should be included in the result.
+ * @param sort Whether to sort files in the same directory.
+ * @return All children media files.
+ */
+ public List<MediaFile> getChildrenOf(MediaFile parent, boolean includeFiles, boolean includeDirectories, boolean sort) {
+ return getChildrenOf(parent, includeFiles, includeDirectories, sort, settingsService.isFastCacheEnabled());
+ }
+
+ /**
+ * Returns all media files that are children of a given media file.
+ *
+ * @param includeFiles Whether files should be included in the result.
+ * @param includeDirectories Whether directories should be included in the result.
+ * @param sort Whether to sort files in the same directory.
+ * @return All children media files.
+ */
+ public List<MediaFile> getChildrenOf(MediaFile parent, boolean includeFiles, boolean includeDirectories, boolean sort, boolean useFastCache) {
+
+ if (!parent.isDirectory()) {
+ return Collections.emptyList();
+ }
+
+ // Make sure children are stored and up-to-date in the database.
+ if (!useFastCache) {
+ updateChildren(parent);
+ }
+
+ List<MediaFile> result = new ArrayList<MediaFile>();
+ for (MediaFile child : mediaFileDao.getChildrenOf(parent.getPath())) {
+ child = checkLastModified(child, useFastCache);
+ if (child.isDirectory() && includeDirectories) {
+ result.add(child);
+ }
+ if (child.isFile() && includeFiles) {
+ result.add(child);
+ }
+ }
+
+ if (sort) {
+ Comparator<MediaFile> comparator = new MediaFileComparator(settingsService.isSortAlbumsByYear());
+ // Note: Intentionally not using Collections.sort() since it can be problematic on Java 7.
+ // http://www.oracle.com/technetwork/java/javase/compatibility-417013.html#jdk7
+ Set<MediaFile> set = new TreeSet<MediaFile>(comparator);
+ set.addAll(result);
+ result = new ArrayList<MediaFile>(set);
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns whether the given file is the root of a media folder.
+ *
+ * @see MusicFolder
+ */
+ public boolean isRoot(MediaFile mediaFile) {
+ for (MusicFolder musicFolder : settingsService.getAllMusicFolders(false, true)) {
+ if (mediaFile.getPath().equals(musicFolder.getPath().getPath())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns all genres in the music collection.
+ *
+ * @return Sorted list of genres.
+ */
+ public List<String> getGenres() {
+ return mediaFileDao.getGenres();
+ }
+
+ /**
+ * Returns the most frequently played albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most frequently played albums.
+ */
+ public List<MediaFile> getMostFrequentlyPlayedAlbums(int offset, int count) {
+ return mediaFileDao.getMostFrequentlyPlayedAlbums(offset, count);
+ }
+
+ /**
+ * Returns the most recently played albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most recently played albums.
+ */
+ public List<MediaFile> getMostRecentlyPlayedAlbums(int offset, int count) {
+ return mediaFileDao.getMostRecentlyPlayedAlbums(offset, count);
+ }
+
+ /**
+ * Returns the most recently added albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @return The most recently added albums.
+ */
+ public List<MediaFile> getNewestAlbums(int offset, int count) {
+ return mediaFileDao.getNewestAlbums(offset, count);
+ }
+
+ /**
+ * Returns the most recently starred albums.
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @param username Returns albums starred by this user.
+ * @return The most recently starred albums for this user.
+ */
+ public List<MediaFile> getStarredAlbums(int offset, int count, String username) {
+ return mediaFileDao.getStarredAlbums(offset, count, username);
+ }
+
+ /**
+ * Returns albums in alphabetial order.
+ *
+ *
+ * @param offset Number of albums to skip.
+ * @param count Maximum number of albums to return.
+ * @param byArtist Whether to sort by artist name
+ * @return Albums in alphabetical order.
+ */
+ public List<MediaFile> getAlphabetialAlbums(int offset, int count, boolean byArtist) {
+ return mediaFileDao.getAlphabetialAlbums(offset, count, byArtist);
+ }
+
+ public Date getMediaFileStarredDate(int id, String username) {
+ return mediaFileDao.getMediaFileStarredDate(id, username);
+ }
+
+ public void populateStarredDate(List<MediaFile> mediaFiles, String username) {
+ for (MediaFile mediaFile : mediaFiles) {
+ populateStarredDate(mediaFile, username);
+ }
+ }
+
+ public void populateStarredDate(MediaFile mediaFile, String username) {
+ Date starredDate = mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username);
+ mediaFile.setStarredDate(starredDate);
+ }
+
+ private void updateChildren(MediaFile parent) {
+
+ // Check timestamps.
+ if (parent.getChildrenLastUpdated().getTime() >= parent.getChanged().getTime()) {
+ return;
+ }
+
+ List<MediaFile> storedChildren = mediaFileDao.getChildrenOf(parent.getPath());
+ Map<String, MediaFile> storedChildrenMap = new HashMap<String, MediaFile>();
+ for (MediaFile child : storedChildren) {
+ storedChildrenMap.put(child.getPath(), child);
+ }
+
+ List<File> children = filterMediaFiles(FileUtil.listFiles(parent.getFile()));
+ for (File child : children) {
+ if (storedChildrenMap.remove(child.getPath()) == null) {
+ // Add children that are not already stored.
+ mediaFileDao.createOrUpdateMediaFile(createMediaFile(child));
+ }
+ }
+
+ // Delete children that no longer exist on disk.
+ for (String path : storedChildrenMap.keySet()) {
+ mediaFileDao.deleteMediaFile(path);
+ }
+
+ // Update timestamp in parent.
+ parent.setChildrenLastUpdated(parent.getChanged());
+ parent.setPresent(true);
+ mediaFileDao.createOrUpdateMediaFile(parent);
+ }
+
+ private List<File> filterMediaFiles(File[] candidates) {
+ List<File> result = new ArrayList<File>();
+ for (File candidate : candidates) {
+ String suffix = FilenameUtils.getExtension(candidate.getName()).toLowerCase();
+ if (!isExcluded(candidate) && (FileUtil.isDirectory(candidate) || isAudioFile(suffix) || isVideoFile(suffix))) {
+ result.add(candidate);
+ }
+ }
+ return result;
+ }
+
+ private boolean isAudioFile(String suffix) {
+ for (String s : settingsService.getMusicFileTypesAsArray()) {
+ if (suffix.equals(s.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isVideoFile(String suffix) {
+ for (String s : settingsService.getVideoFileTypesAsArray()) {
+ if (suffix.equals(s.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the given file is excluded.
+ *
+ * @param file The child file in question.
+ * @return Whether the child file is excluded.
+ */
+ private boolean isExcluded(File file) {
+
+ // Exclude all hidden files starting with a "." or "@eaDir" (thumbnail dir created on Synology devices).
+ String name = file.getName();
+ return name.startsWith(".") || name.startsWith("@eaDir") || name.equals("Thumbs.db");
+ }
+
+ private MediaFile createMediaFile(File file) {
+ MediaFile mediaFile = new MediaFile();
+ Date lastModified = new Date(FileUtil.lastModified(file));
+ mediaFile.setPath(file.getPath());
+ mediaFile.setFolder(securityService.getRootFolderForFile(file));
+ mediaFile.setParentPath(file.getParent());
+ mediaFile.setChanged(lastModified);
+ mediaFile.setLastScanned(new Date());
+ mediaFile.setPlayCount(0);
+ mediaFile.setChildrenLastUpdated(new Date(0));
+ mediaFile.setCreated(lastModified);
+ mediaFile.setMediaType(DIRECTORY);
+ mediaFile.setPresent(true);
+
+ if (file.isFile()) {
+
+ MetaDataParser parser = metaDataParserFactory.getParser(file);
+ if (parser != null) {
+ MetaData metaData = parser.getMetaData(file);
+ mediaFile.setArtist(metaData.getArtist());
+ mediaFile.setAlbumArtist(metaData.getArtist());
+ mediaFile.setAlbumName(metaData.getAlbumName());
+ mediaFile.setTitle(metaData.getTitle());
+ mediaFile.setDiscNumber(metaData.getDiscNumber());
+ mediaFile.setTrackNumber(metaData.getTrackNumber());
+ mediaFile.setGenre(metaData.getGenre());
+ mediaFile.setYear(metaData.getYear());
+ mediaFile.setDurationSeconds(metaData.getDurationSeconds());
+ mediaFile.setBitRate(metaData.getBitRate());
+ mediaFile.setVariableBitRate(metaData.getVariableBitRate());
+ mediaFile.setHeight(metaData.getHeight());
+ mediaFile.setWidth(metaData.getWidth());
+ }
+ String format = StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(mediaFile.getPath())));
+ mediaFile.setFormat(format);
+ mediaFile.setFileSize(FileUtil.length(file));
+ mediaFile.setMediaType(getMediaType(mediaFile));
+
+ } else {
+
+ // Is this an album?
+ if (!isRoot(mediaFile)) {
+ File[] children = FileUtil.listFiles(file);
+ File firstChild = null;
+ for (File child : filterMediaFiles(children)) {
+ if (FileUtil.isFile(child)) {
+ firstChild = child;
+ break;
+ }
+ }
+
+ if (firstChild != null) {
+ mediaFile.setMediaType(ALBUM);
+
+ // Guess artist/album name and year.
+ MetaDataParser parser = metaDataParserFactory.getParser(firstChild);
+ if (parser != null) {
+ MetaData metaData = parser.getMetaData(firstChild);
+ mediaFile.setArtist(metaData.getArtist());
+ mediaFile.setAlbumName(metaData.getAlbumName());
+ mediaFile.setYear(metaData.getYear());
+ }
+
+ // Look for cover art.
+ try {
+ File coverArt = findCoverArt(children);
+ if (coverArt != null) {
+ mediaFile.setCoverArtPath(coverArt.getPath());
+ }
+ } catch (IOException x) {
+ LOG.error("Failed to find cover art.", x);
+ }
+
+ } else {
+ mediaFile.setArtist(file.getName());
+ }
+ }
+ }
+
+ return mediaFile;
+ }
+
+ private MediaFile.MediaType getMediaType(MediaFile mediaFile) {
+ if (isVideoFile(mediaFile.getFormat())) {
+ return VIDEO;
+ }
+ String path = mediaFile.getPath().toLowerCase();
+ String genre = StringUtils.trimToEmpty(mediaFile.getGenre()).toLowerCase();
+ if (path.contains("podcast") || genre.contains("podcast")) {
+ return PODCAST;
+ }
+ if (path.contains("audiobook") || genre.contains("audiobook") || path.contains("audio book") || genre.contains("audio book")) {
+ return AUDIOBOOK;
+ }
+ return MUSIC;
+ }
+
+ public void refreshMediaFile(MediaFile mediaFile) {
+ mediaFile = createMediaFile(mediaFile.getFile());
+ mediaFileDao.createOrUpdateMediaFile(mediaFile);
+ mediaFileMemoryCache.remove(mediaFile.getFile());
+ }
+
+ /**
+ * Returns a cover art image for the given media file.
+ */
+ public File getCoverArt(MediaFile mediaFile) {
+ if (mediaFile.getCoverArtFile() != null) {
+ return mediaFile.getCoverArtFile();
+ }
+ MediaFile parent = getParentOf(mediaFile);
+ return parent == null ? null : parent.getCoverArtFile();
+ }
+
+ /**
+ * Finds a cover art image for the given directory, by looking for it on the disk.
+ */
+ private File findCoverArt(File[] candidates) throws IOException {
+ for (String mask : settingsService.getCoverArtFileTypesAsArray()) {
+ for (File candidate : candidates) {
+ if (candidate.isFile() && candidate.getName().toUpperCase().endsWith(mask.toUpperCase()) && !candidate.getName().startsWith(".")) {
+ return candidate;
+ }
+ }
+ }
+
+ // Look for embedded images in audiofiles. (Only check first audio file encountered).
+ JaudiotaggerParser parser = new JaudiotaggerParser();
+ for (File candidate : candidates) {
+ if (parser.isApplicable(candidate)) {
+ if (parser.isImageAvailable(getMediaFile(candidate))) {
+ return candidate;
+ } else {
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaFileMemoryCache(Ehcache mediaFileMemoryCache) {
+ this.mediaFileMemoryCache = mediaFileMemoryCache;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ /**
+ * Returns all media files that are children, grand-children etc of a given media file.
+ * Directories are not included in the result.
+ *
+ * @param sort Whether to sort files in the same directory.
+ * @return All descendant music files.
+ */
+ public List<MediaFile> getDescendantsOf(MediaFile ancestor, boolean sort) {
+
+ if (ancestor.isFile()) {
+ return Arrays.asList(ancestor);
+ }
+
+ List<MediaFile> result = new ArrayList<MediaFile>();
+
+ for (MediaFile child : getChildrenOf(ancestor, true, true, sort)) {
+ if (child.isDirectory()) {
+ result.addAll(getDescendantsOf(child, sort));
+ } else {
+ result.add(child);
+ }
+ }
+ return result;
+ }
+
+ public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) {
+ this.metaDataParserFactory = metaDataParserFactory;
+ }
+
+ public void updateMediaFile(MediaFile mediaFile) {
+ mediaFileDao.createOrUpdateMediaFile(mediaFile);
+ }
+
+ /**
+ * Increments the play count and last played date for the given media file and its
+ * directory and album.
+ */
+ public void incrementPlayCount(MediaFile file) {
+ Date now = new Date();
+ file.setLastPlayed(now);
+ file.setPlayCount(file.getPlayCount() + 1);
+ updateMediaFile(file);
+
+ MediaFile parent = getParentOf(file);
+ if (!isRoot(parent)) {
+ parent.setLastPlayed(now);
+ parent.setPlayCount(parent.getPlayCount() + 1);
+ updateMediaFile(parent);
+ }
+
+ Album album = albumDao.getAlbum(file.getAlbumArtist(), file.getAlbumName());
+ if (album != null) {
+ album.setLastPlayed(now);
+ album.setPlayCount(album.getPlayCount() + 1);
+ albumDao.createOrUpdateAlbum(album);
+ }
+ }
+
+ public void setAlbumDao(AlbumDao albumDao) {
+ this.albumDao = albumDao;
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java
new file mode 100644
index 00000000..84f2d31c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java
@@ -0,0 +1,354 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.File;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.AlbumDao;
+import net.sourceforge.subsonic.dao.ArtistDao;
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.Album;
+import net.sourceforge.subsonic.domain.Artist;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MediaLibraryStatistics;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.util.FileUtil;
+import org.apache.commons.lang.ObjectUtils;
+
+/**
+ * Provides services for scanning the music library.
+ *
+ * @author Sindre Mehus
+ */
+public class MediaScannerService {
+
+ private static final int INDEX_VERSION = 15;
+ private static final Logger LOG = Logger.getLogger(MediaScannerService.class);
+
+ private MediaLibraryStatistics statistics;
+
+ private boolean scanning;
+ private Timer timer;
+ private SettingsService settingsService;
+ private SearchService searchService;
+ private MediaFileService mediaFileService;
+ private MediaFileDao mediaFileDao;
+ private ArtistDao artistDao;
+ private AlbumDao albumDao;
+ private int scanCount;
+
+ public void init() {
+ deleteOldIndexFiles();
+ statistics = mediaFileDao.getStatistics();
+ schedule();
+ }
+
+ /**
+ * Schedule background execution of media library scanning.
+ */
+ public synchronized void schedule() {
+ if (timer != null) {
+ timer.cancel();
+ }
+ timer = new Timer(true);
+
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ scanLibrary();
+ }
+ };
+
+ long daysBetween = settingsService.getIndexCreationInterval();
+ int hour = settingsService.getIndexCreationHour();
+
+ if (daysBetween == -1) {
+ LOG.info("Automatic media scanning disabled.");
+ return;
+ }
+
+ Date now = new Date();
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(now);
+ cal.set(Calendar.HOUR_OF_DAY, hour);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+
+ if (cal.getTime().before(now)) {
+ cal.add(Calendar.DATE, 1);
+ }
+
+ Date firstTime = cal.getTime();
+ long period = daysBetween * 24L * 3600L * 1000L;
+ timer.schedule(task, firstTime, period);
+
+ LOG.info("Automatic media library scanning scheduled to run every " + daysBetween + " day(s), starting at " + firstTime);
+
+ // In addition, create index immediately if it doesn't exist on disk.
+ if (settingsService.getLastScanned() == null) {
+ LOG.info("Media library never scanned. Doing it now.");
+ scanLibrary();
+ }
+ }
+
+ /**
+ * Returns whether the media library is currently being scanned.
+ */
+ public synchronized boolean isScanning() {
+ return scanning;
+ }
+
+ /**
+ * Returns the number of files scanned so far.
+ */
+ public int getScanCount() {
+ return scanCount;
+ }
+
+ /**
+ * Scans the media library.
+ * The scanning is done asynchronously, i.e., this method returns immediately.
+ */
+ public synchronized void scanLibrary() {
+ if (isScanning()) {
+ return;
+ }
+ scanning = true;
+
+ Thread thread = new Thread("MediaLibraryScanner") {
+ @Override
+ public void run() {
+ doScanLibrary();
+ }
+ };
+
+ thread.setPriority(Thread.MIN_PRIORITY);
+ thread.start();
+ }
+
+ private void doScanLibrary() {
+ LOG.info("Starting to scan media library.");
+
+ try {
+ Date lastScanned = new Date();
+ Map<String, Integer> albumCount = new HashMap<String, Integer>();
+ scanCount = 0;
+
+ searchService.startIndexing();
+
+ // Recurse through all files on disk.
+ for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) {
+ MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false);
+ scanFile(root, musicFolder, lastScanned, albumCount);
+ }
+ mediaFileDao.markNonPresent(lastScanned);
+ artistDao.markNonPresent(lastScanned);
+ albumDao.markNonPresent(lastScanned);
+
+ // Update statistics
+ statistics = mediaFileDao.getStatistics();
+
+ settingsService.setLastScanned(lastScanned);
+ settingsService.save(false);
+ LOG.info("Scanned media library with " + scanCount + " entries.");
+
+ } catch (Throwable x) {
+ LOG.error("Failed to scan media library.", x);
+ } finally {
+ scanning = false;
+ searchService.stopIndexing();
+ }
+ }
+
+ private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, Map<String, Integer> albumCount) {
+ scanCount++;
+ if (scanCount % 250 == 0) {
+ LOG.info("Scanned media library with " + scanCount + " entries.");
+ }
+
+ searchService.index(file);
+
+ // Update the root folder if it has changed.
+ if (!musicFolder.getPath().getPath().equals(file.getFolder())) {
+ file.setFolder(musicFolder.getPath().getPath());
+ mediaFileDao.createOrUpdateMediaFile(file);
+ }
+
+ if (file.isDirectory()) {
+ for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) {
+ scanFile(child, musicFolder, lastScanned, albumCount);
+ }
+ for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) {
+ scanFile(child, musicFolder, lastScanned, albumCount);
+ }
+ } else {
+ updateAlbum(file, lastScanned, albumCount);
+ updateArtist(file, lastScanned, albumCount);
+ }
+
+ mediaFileDao.markPresent(file.getPath(), lastScanned);
+ artistDao.markPresent(file.getArtist(), lastScanned);
+ }
+
+ private void updateAlbum(MediaFile file, Date lastScanned, Map<String, Integer> albumCount) {
+ if (file.getAlbumName() == null || file.getArtist() == null || file.getParentPath() == null || !file.isAudio()) {
+ return;
+ }
+
+ Album album = albumDao.getAlbumForFile(file);
+ if (album == null) {
+ album = new Album();
+ album.setPath(file.getParentPath());
+ album.setName(file.getAlbumName());
+ album.setArtist(file.getArtist());
+ album.setCreated(file.getChanged());
+ }
+ if (album.getCoverArtPath() == null) {
+ MediaFile parent = mediaFileService.getParentOf(file);
+ if (parent != null) {
+ album.setCoverArtPath(parent.getCoverArtPath());
+ }
+ }
+ boolean firstEncounter = !lastScanned.equals(album.getLastScanned());
+ if (firstEncounter) {
+ album.setDurationSeconds(0);
+ album.setSongCount(0);
+ Integer n = albumCount.get(file.getArtist());
+ albumCount.put(file.getArtist(), n == null ? 1 : n + 1);
+ }
+ if (file.getDurationSeconds() != null) {
+ album.setDurationSeconds(album.getDurationSeconds() + file.getDurationSeconds());
+ }
+ if (file.isAudio()) {
+ album.setSongCount(album.getSongCount() + 1);
+ }
+
+ album.setLastScanned(lastScanned);
+ album.setPresent(true);
+ albumDao.createOrUpdateAlbum(album);
+ if (firstEncounter) {
+ searchService.index(album);
+ }
+
+ // Update the file's album artist, if necessary.
+ if (!ObjectUtils.equals(album.getArtist(), file.getAlbumArtist())) {
+ file.setAlbumArtist(album.getArtist());
+ mediaFileDao.createOrUpdateMediaFile(file);
+ }
+ }
+
+ private void updateArtist(MediaFile file, Date lastScanned, Map<String, Integer> albumCount) {
+ if (file.getArtist() == null || !file.isAudio()) {
+ return;
+ }
+
+ Artist artist = artistDao.getArtist(file.getArtist());
+ if (artist == null) {
+ artist = new Artist();
+ artist.setName(file.getArtist());
+ }
+ if (artist.getCoverArtPath() == null) {
+ MediaFile parent = mediaFileService.getParentOf(file);
+ if (parent != null) {
+ artist.setCoverArtPath(parent.getCoverArtPath());
+ }
+ }
+ boolean firstEncounter = !lastScanned.equals(artist.getLastScanned());
+
+ Integer n = albumCount.get(artist.getName());
+ artist.setAlbumCount(n == null ? 0 : n);
+
+ artist.setLastScanned(lastScanned);
+ artist.setPresent(true);
+ artistDao.createOrUpdateArtist(artist);
+
+ if (firstEncounter) {
+ searchService.index(artist);
+ }
+ }
+
+ /**
+ * Returns media library statistics, including the number of artists, albums and songs.
+ *
+ * @return Media library statistics.
+ */
+ public MediaLibraryStatistics getStatistics() {
+ return statistics;
+ }
+
+ /**
+ * Deletes old versions of the index file.
+ */
+ private void deleteOldIndexFiles() {
+ for (int i = 2; i < INDEX_VERSION; i++) {
+ File file = getIndexFile(i);
+ try {
+ if (FileUtil.exists(file)) {
+ if (file.delete()) {
+ LOG.info("Deleted old index file: " + file.getPath());
+ }
+ }
+ } catch (Exception x) {
+ LOG.warn("Failed to delete old index file: " + file.getPath(), x);
+ }
+ }
+ }
+
+ /**
+ * Returns the index file for the given index version.
+ *
+ * @param version The index version.
+ * @return The index file for the given index version.
+ */
+ private File getIndexFile(int version) {
+ File home = SettingsService.getSubsonicHome();
+ return new File(home, "subsonic" + version + ".index");
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSearchService(SearchService searchService) {
+ this.searchService = searchService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ public void setArtistDao(ArtistDao artistDao) {
+ this.artistDao = artistDao;
+ }
+
+ public void setAlbumDao(AlbumDao albumDao) {
+ this.albumDao = albumDao;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java
new file mode 100644
index 00000000..b6ee682e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java
@@ -0,0 +1,250 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.MusicIndex;
+import net.sourceforge.subsonic.domain.MusicIndex.Artist;
+
+/**
+ * Provides services for grouping artists by index.
+ *
+ * @author Sindre Mehus
+ */
+public class MusicIndexService {
+
+ private SettingsService settingsService;
+ private MediaFileService mediaFileService;
+ private MediaFileDao mediaFileDao;
+
+ /**
+ * Returns a map from music indexes to sets of artists that are direct children of the given music folders.
+ *
+ * @param folders The music folders.
+ * @return A map from music indexes to sets of artists that are direct children of this music file.
+ * @throws IOException If an I/O error occurs.
+ */
+ public SortedMap<MusicIndex, SortedSet<Artist>> getIndexedArtists(List<MusicFolder> folders) throws IOException {
+
+ String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray();
+ String[] shortcuts = settingsService.getShortcutsAsArray();
+ final List<MusicIndex> indexes = createIndexesFromExpression(settingsService.getIndexString());
+
+ Comparator<MusicIndex> indexComparator = new MusicIndexComparator(indexes);
+ SortedSet<Artist> artists = createArtists(folders, ignoredArticles, shortcuts);
+ SortedMap<MusicIndex, SortedSet<Artist>> result = new TreeMap<MusicIndex, SortedSet<Artist>>(indexComparator);
+
+ for (Artist artist : artists) {
+ MusicIndex index = getIndex(artist, indexes);
+ SortedSet<Artist> artistSet = result.get(index);
+ if (artistSet == null) {
+ artistSet = new TreeSet<Artist>();
+ result.put(index, artistSet);
+ }
+ artistSet.add(artist);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a new instance by parsing the given expression. The expression consists of an index name, followed by
+ * an optional list of one-character prefixes. For example:<p/>
+ * <p/>
+ * The expression <em>"A"</em> will create the index <em>"A" -&gt; ["A"]</em><br/>
+ * The expression <em>"The"</em> will create the index <em>"The" -&gt; ["The"]</em><br/>
+ * The expression <em>"A(A&Aring;&AElig;)"</em> will create the index <em>"A" -&gt; ["A", "&Aring;", "&AElig;"]</em><br/>
+ * The expression <em>"X-Z(XYZ)"</em> will create the index <em>"X-Z" -&gt; ["X", "Y", "Z"]</em>
+ *
+ * @param expr The expression to parse.
+ * @return A new instance.
+ */
+ protected MusicIndex createIndexFromExpression(String expr) {
+ int separatorIndex = expr.indexOf('(');
+ if (separatorIndex == -1) {
+
+ MusicIndex index = new MusicIndex(expr);
+ index.addPrefix(expr);
+ return index;
+ }
+
+ MusicIndex index = new MusicIndex(expr.substring(0, separatorIndex));
+ String prefixString = expr.substring(separatorIndex + 1, expr.length() - 1);
+ for (int i = 0; i < prefixString.length(); i++) {
+ index.addPrefix(prefixString.substring(i, i + 1));
+ }
+ return index;
+ }
+
+ /**
+ * Creates a list of music indexes by parsing the given expression. The expression is a space-separated list of
+ * sub-expressions, for which the rules described in {@link #createIndexFromExpression} apply.
+ *
+ * @param expr The expression to parse.
+ * @return A list of music indexes.
+ */
+ protected List<MusicIndex> createIndexesFromExpression(String expr) {
+ List<MusicIndex> result = new ArrayList<MusicIndex>();
+
+ StringTokenizer tokenizer = new StringTokenizer(expr, " ");
+ while (tokenizer.hasMoreTokens()) {
+ MusicIndex index = createIndexFromExpression(tokenizer.nextToken());
+ result.add(index);
+ }
+
+ return result;
+ }
+
+ private SortedSet<Artist> createArtists(List<MusicFolder> folders, String[] ignoredArticles, String[] shortcuts) throws IOException {
+ return settingsService.isOrganizeByFolderStructure() ?
+ createArtistsByFolderStructure(folders, ignoredArticles, shortcuts) :
+ createArtistsByTagStructure(folders, ignoredArticles, shortcuts);
+ }
+
+ private SortedSet<Artist> createArtistsByFolderStructure(List<MusicFolder> folders, String[] ignoredArticles, String[] shortcuts) {
+ SortedMap<String, Artist> artistMap = new TreeMap<String, Artist>();
+ Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts));
+
+ for (MusicFolder folder : folders) {
+
+ MediaFile root = mediaFileService.getMediaFile(folder.getPath(), true);
+ List<MediaFile> children = mediaFileService.getChildrenOf(root, false, true, true, true);
+ for (MediaFile child : children) {
+ if (shortcutSet.contains(child.getName())) {
+ continue;
+ }
+
+ String sortableName = createSortableName(child.getName(), ignoredArticles);
+ Artist artist = artistMap.get(sortableName);
+ if (artist == null) {
+ artist = new Artist(child.getName(), sortableName);
+ artistMap.put(sortableName, artist);
+ }
+ artist.addMediaFile(child);
+ }
+ }
+
+ return new TreeSet<Artist>(artistMap.values());
+ }
+
+ private SortedSet<Artist> createArtistsByTagStructure(List<MusicFolder> folders, String[] ignoredArticles, String[] shortcuts) {
+ Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts));
+ SortedSet<Artist> artists = new TreeSet<Artist>();
+
+ // TODO: Filter by folder
+ for (String artistName : mediaFileDao.getArtists()) {
+
+ if (shortcutSet.contains(artistName)) {
+ continue;
+ }
+
+ String sortableName = createSortableName(artistName, ignoredArticles);
+ Artist artist = new Artist(artistName, sortableName);
+ artists.add(artist);
+ }
+
+ return artists;
+ }
+
+ private String createSortableName(String name, String[] ignoredArticles) {
+ String uppercaseName = name.toUpperCase();
+ for (String article : ignoredArticles) {
+ if (uppercaseName.startsWith(article.toUpperCase() + " ")) {
+ return name.substring(article.length() + 1) + ", " + article;
+ }
+ }
+ return name;
+ }
+
+ /**
+ * Returns the music index to which the given artist belongs.
+ *
+ * @param artist The artist in question.
+ * @param indexes List of available indexes.
+ * @return The music index to which this music file belongs, or {@link MusicIndex#OTHER} if no index applies.
+ */
+ private MusicIndex getIndex(Artist artist, List<MusicIndex> indexes) {
+ String sortableName = artist.getSortableName().toUpperCase();
+ for (MusicIndex index : indexes) {
+ for (String prefix : index.getPrefixes()) {
+ if (sortableName.startsWith(prefix.toUpperCase())) {
+ return index;
+ }
+ }
+ }
+ return MusicIndex.OTHER;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ private static class MusicIndexComparator implements Comparator<MusicIndex>, Serializable {
+
+ private List<MusicIndex> indexes;
+
+ public MusicIndexComparator(List<MusicIndex> indexes) {
+ this.indexes = indexes;
+ }
+
+ public int compare(MusicIndex a, MusicIndex b) {
+ int indexA = indexes.indexOf(a);
+ int indexB = indexes.indexOf(b);
+
+ if (indexA == -1) {
+ indexA = Integer.MAX_VALUE;
+ }
+ if (indexB == -1) {
+ indexB = Integer.MAX_VALUE;
+ }
+
+ if (indexA < indexB) {
+ return -1;
+ }
+ if (indexA > indexB) {
+ return 1;
+ }
+ return 0;
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java
new file mode 100644
index 00000000..b54026a0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java
@@ -0,0 +1,336 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.util.EntityUtils;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.NATPMPRouter;
+import net.sourceforge.subsonic.domain.Router;
+import net.sourceforge.subsonic.domain.SBBIRouter;
+import net.sourceforge.subsonic.domain.WeUPnPRouter;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.Util;
+
+/**
+ * Provides network-related services, including port forwarding on UPnP routers and
+ * URL redirection from http://xxxx.subsonic.org.
+ *
+ * @author Sindre Mehus
+ */
+public class NetworkService {
+
+ private static final Logger LOG = Logger.getLogger(NetworkService.class);
+ private static final long PORT_FORWARDING_DELAY = 3600L;
+ private static final long URL_REDIRECTION_DELAY = 2 * 3600L;
+
+ private static final String URL_REDIRECTION_REGISTER_URL = getBackendUrl() + "/backend/redirect/register.view";
+ private static final String URL_REDIRECTION_UNREGISTER_URL = getBackendUrl() + "/backend/redirect/unregister.view";
+ private static final String URL_REDIRECTION_TEST_URL = getBackendUrl() + "/backend/redirect/test.view";
+
+ private SettingsService settingsService;
+ private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
+ private final PortForwardingTask portForwardingTask = new PortForwardingTask();
+ private final URLRedirectionTask urlRedirectionTask = new URLRedirectionTask();
+ private Future<?> portForwardingFuture;
+ private Future<?> urlRedirectionFuture;
+
+ private final Status portForwardingStatus = new Status();
+ private final Status urlRedirectionStatus = new Status();
+ private boolean testUrlRedirection;
+
+ public void init() {
+ initPortForwarding();
+ initUrlRedirection(false);
+ }
+
+ /**
+ * Configures UPnP port forwarding.
+ */
+ public synchronized void initPortForwarding() {
+ portForwardingStatus.setText("Idle");
+ if (portForwardingFuture != null) {
+ portForwardingFuture.cancel(true);
+ }
+ portForwardingFuture = executor.scheduleWithFixedDelay(portForwardingTask, 0L, PORT_FORWARDING_DELAY, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Configures URL redirection.
+ *
+ * @param test Whether to test that the redirection works.
+ */
+ public synchronized void initUrlRedirection(boolean test) {
+ urlRedirectionStatus.setText("Idle");
+ if (urlRedirectionFuture != null) {
+ urlRedirectionFuture.cancel(true);
+ }
+ testUrlRedirection = test;
+ urlRedirectionFuture = executor.scheduleWithFixedDelay(urlRedirectionTask, 0L, URL_REDIRECTION_DELAY, TimeUnit.SECONDS);
+ }
+
+ public Status getPortForwardingStatus() {
+ return portForwardingStatus;
+ }
+
+ public Status getURLRedirecionStatus() {
+ return urlRedirectionStatus;
+ }
+
+ public static String getBackendUrl() {
+ return "true".equals(System.getProperty("subsonic.test")) ? "http://localhost:8181" : "http://subsonic.org";
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ private class PortForwardingTask extends Task {
+
+ @Override
+ protected void execute() {
+
+ boolean enabled = settingsService.isPortForwardingEnabled();
+ portForwardingStatus.setText("Looking for router...");
+ Router router = findRouter();
+ if (router == null) {
+ LOG.warn("No UPnP router found.");
+ portForwardingStatus.setText("No router found.");
+ } else {
+
+ portForwardingStatus.setText("Router found.");
+
+ int port = settingsService.getPort();
+ int httpsPort = settingsService.getHttpsPort();
+
+ // Create new NAT entry.
+ if (enabled) {
+ try {
+ router.addPortMapping(port, port, 0);
+ String message = "Successfully forwarding port " + port;
+
+ if (httpsPort != 0 && httpsPort != port) {
+ router.addPortMapping(httpsPort, httpsPort, 0);
+ message += " and port " + httpsPort;
+ }
+ message += ".";
+
+ LOG.info(message);
+ portForwardingStatus.setText(message);
+ } catch (Throwable x) {
+ String message = "Failed to create port forwarding.";
+ LOG.warn(message, x);
+ portForwardingStatus.setText(message + " See log for details.");
+ }
+ }
+
+ // Delete NAT entry.
+ else {
+ try {
+ router.deletePortMapping(port, port);
+ LOG.info("Deleted port mapping for port " + port);
+ if (httpsPort != 0 && httpsPort != port) {
+ router.deletePortMapping(httpsPort, httpsPort);
+ LOG.info("Deleted port mapping for port " + httpsPort);
+ }
+ } catch (Throwable x) {
+ LOG.warn("Failed to delete port mapping.", x);
+ }
+ portForwardingStatus.setText("Port forwarding disabled.");
+ }
+ }
+
+ // Don't do it again if disabled.
+ if (!enabled && portForwardingFuture != null) {
+ portForwardingFuture.cancel(false);
+ }
+ }
+
+ private Router findRouter() {
+ try {
+ Router router = SBBIRouter.findRouter();
+ if (router != null) {
+ return router;
+ }
+ } catch (Throwable x) {
+ LOG.warn("Failed to find UPnP router using SBBI library.", x);
+ }
+
+ try {
+ Router router = WeUPnPRouter.findRouter();
+ if (router != null) {
+ return router;
+ }
+ } catch (Throwable x) {
+ LOG.warn("Failed to find UPnP router using WeUPnP library.", x);
+ }
+
+ try {
+ Router router = NATPMPRouter.findRouter();
+ if (router != null) {
+ return router;
+ }
+ } catch (Throwable x) {
+ LOG.warn("Failed to find NAT-PMP router.", x);
+ }
+
+ return null;
+ }
+ }
+
+ private class URLRedirectionTask extends Task {
+
+ @Override
+ protected void execute() {
+
+ boolean enable = settingsService.isUrlRedirectionEnabled();
+ HttpPost request = new HttpPost(enable ? URL_REDIRECTION_REGISTER_URL : URL_REDIRECTION_UNREGISTER_URL);
+
+ int port = settingsService.getPort();
+ boolean trial = !settingsService.isLicenseValid();
+ Date trialExpires = settingsService.getUrlRedirectTrialExpires();
+
+ List<NameValuePair> params = new ArrayList<NameValuePair>();
+ params.add(new BasicNameValuePair("serverId", settingsService.getServerId()));
+ params.add(new BasicNameValuePair("redirectFrom", settingsService.getUrlRedirectFrom()));
+ params.add(new BasicNameValuePair("port", String.valueOf(port)));
+ params.add(new BasicNameValuePair("localIp", Util.getLocalIpAddress()));
+ params.add(new BasicNameValuePair("localPort", String.valueOf(port)));
+ params.add(new BasicNameValuePair("contextPath", settingsService.getUrlRedirectContextPath()));
+ params.add(new BasicNameValuePair("trial", String.valueOf(trial)));
+ if (trial && trialExpires != null) {
+ params.add(new BasicNameValuePair("trialExpires", String.valueOf(trialExpires.getTime())));
+ } else {
+ params.add(new BasicNameValuePair("licenseHolder", settingsService.getLicenseEmail()));
+ }
+
+ HttpClient client = new DefaultHttpClient();
+
+ try {
+ urlRedirectionStatus.setText(enable ? "Registering web address..." : "Unregistering web address...");
+ request.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8));
+
+ HttpResponse response = client.execute(request);
+ StatusLine status = response.getStatusLine();
+
+ switch (status.getStatusCode()) {
+ case HttpStatus.SC_BAD_REQUEST:
+ urlRedirectionStatus.setText(EntityUtils.toString(response.getEntity()));
+ break;
+ case HttpStatus.SC_OK:
+ urlRedirectionStatus.setText(enable ? "Successfully registered web address." : "Web address disabled.");
+ break;
+ default:
+ throw new IOException(status.getStatusCode() + " " + status.getReasonPhrase());
+ }
+
+ } catch (Throwable x) {
+ LOG.warn(enable ? "Failed to register web address." : "Failed to unregister web address.", x);
+ urlRedirectionStatus.setText(enable ? ("Failed to register web address. " + x.getMessage() +
+ " (" + x.getClass().getSimpleName() + ")") : "Web address disabled.");
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+
+ // Test redirection, but only once.
+ if (testUrlRedirection) {
+ testUrlRedirection = false;
+ testUrlRedirection();
+ }
+
+ // Don't do it again if disabled.
+ if (!enable && urlRedirectionFuture != null) {
+ urlRedirectionFuture.cancel(false);
+ }
+ }
+
+ private void testUrlRedirection() {
+
+ HttpGet request = new HttpGet(URL_REDIRECTION_TEST_URL + "?redirectFrom=" + settingsService.getUrlRedirectFrom());
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 30000);
+
+ try {
+ urlRedirectionStatus.setText("Testing web address " + settingsService.getUrlRedirectFrom() + ".subsonic.org. Please wait...");
+ String response = client.execute(request, new BasicResponseHandler());
+ urlRedirectionStatus.setText(response);
+
+ } catch (Throwable x) {
+ LOG.warn("Failed to test web address.", x);
+ urlRedirectionStatus.setText("Failed to test web address. " + x.getMessage() + " (" + x.getClass().getSimpleName() + ")");
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+ }
+
+ private abstract class Task implements Runnable {
+ public void run() {
+ String name = getClass().getSimpleName();
+ try {
+ execute();
+ } catch (Throwable x) {
+ LOG.error("Error executing " + name + ": " + x.getMessage(), x);
+ }
+ }
+
+ protected abstract void execute();
+ }
+
+ public static class Status {
+
+ private String text;
+ private Date date;
+
+ public void setText(String text) {
+ this.text = text;
+ date = new Date();
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java
new file mode 100644
index 00000000..0f24b2b8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java
@@ -0,0 +1,317 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.dao.PlayerDao;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.Transcoding;
+import net.sourceforge.subsonic.domain.TransferStatus;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.lang.StringUtils;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Provides services for maintaining the set of players.
+ *
+ * @author Sindre Mehus
+ * @see Player
+ */
+public class PlayerService {
+
+ private static final String COOKIE_NAME = "player";
+ private static final int COOKIE_EXPIRY = 365 * 24 * 3600; // One year
+
+ private PlayerDao playerDao;
+ private StatusService statusService;
+ private SecurityService securityService;
+ private TranscodingService transcodingService;
+
+ public void init() {
+ playerDao.deleteOldPlayers(60);
+ }
+
+ /**
+ * Equivalent to <code>getPlayer(request, response, true)</code> .
+ */
+ public Player getPlayer(HttpServletRequest request, HttpServletResponse response) {
+ return getPlayer(request, response, true, false);
+ }
+
+ /**
+ * Returns the player associated with the given HTTP request. If no such player exists, a new
+ * one is created.
+ *
+ * @param request The HTTP request.
+ * @param response The HTTP response.
+ * @param remoteControlEnabled Whether this method should return a remote-controlled player.
+ * @param isStreamRequest Whether the HTTP request is a request for streaming data.
+ * @return The player associated with the given HTTP request.
+ */
+ public synchronized Player getPlayer(HttpServletRequest request, HttpServletResponse response,
+ boolean remoteControlEnabled, boolean isStreamRequest) {
+
+ // Find by 'player' request parameter.
+ Player player = getPlayerById(request.getParameter("player"));
+
+ // Find in session context.
+ if (player == null && remoteControlEnabled) {
+ String playerId = (String) request.getSession().getAttribute("player");
+ if (playerId != null) {
+ player = getPlayerById(playerId);
+ }
+ }
+
+ // Find by cookie.
+ String username = securityService.getCurrentUsername(request);
+ if (player == null && remoteControlEnabled) {
+ player = getPlayerById(getPlayerIdFromCookie(request, username));
+ }
+
+ // Make sure we're not hijacking the player of another user.
+ if (player != null && player.getUsername() != null && username != null && !player.getUsername().equals(username)) {
+ player = null;
+ }
+
+ // Look for player with same IP address and user name.
+ if (player == null) {
+ player = getPlayerByIpAddressAndUsername(request.getRemoteAddr(), username);
+
+ // Don't use this player if it's used by REST API.
+ if (player != null && player.getClientId() != null) {
+ player = null;
+ }
+ }
+
+ // If no player was found, create it.
+ if (player == null) {
+ player = new Player();
+ createPlayer(player);
+// LOG.debug("Created player " + player.getId() + " (remoteControlEnabled: " + remoteControlEnabled +
+// ", isStreamRequest: " + isStreamRequest + ", username: " + username +
+// ", ip: " + request.getRemoteAddr() + ").");
+ }
+
+ // Update player data.
+ boolean isUpdate = false;
+ if (username != null && player.getUsername() == null) {
+ player.setUsername(username);
+ isUpdate = true;
+ }
+ if (player.getIpAddress() == null || isStreamRequest ||
+ (!isPlayerConnected(player) && player.isDynamicIp() && !request.getRemoteAddr().equals(player.getIpAddress()))) {
+ player.setIpAddress(request.getRemoteAddr());
+ isUpdate = true;
+ }
+ String userAgent = request.getHeader("user-agent");
+ if (isStreamRequest) {
+ player.setType(userAgent);
+ player.setLastSeen(new Date());
+ isUpdate = true;
+ }
+
+ if (isUpdate) {
+ updatePlayer(player);
+ }
+
+ // Set cookie in response.
+ if (response != null) {
+ String cookieName = COOKIE_NAME + "-" + StringUtil.utf8HexEncode(username);
+ Cookie cookie = new Cookie(cookieName, player.getId());
+ cookie.setMaxAge(COOKIE_EXPIRY);
+ String path = request.getContextPath();
+ if (StringUtils.isEmpty(path)) {
+ path = "/";
+ }
+ cookie.setPath(path);
+ response.addCookie(cookie);
+ }
+
+ // Save player in session context.
+ if (remoteControlEnabled) {
+ request.getSession().setAttribute("player", player.getId());
+ }
+
+ return player;
+ }
+
+ /**
+ * Updates the given player.
+ *
+ * @param player The player to update.
+ */
+ public void updatePlayer(Player player) {
+ playerDao.updatePlayer(player);
+ }
+
+ /**
+ * Returns the player with the given ID.
+ *
+ * @param id The unique player ID.
+ * @return The player with the given ID, or <code>null</code> if no such player exists.
+ */
+ public Player getPlayerById(String id) {
+ return playerDao.getPlayerById(id);
+ }
+
+ /**
+ * Returns whether the given player is connected.
+ *
+ * @param player The player in question.
+ * @return Whether the player is connected.
+ */
+ private boolean isPlayerConnected(Player player) {
+ for (TransferStatus status : statusService.getStreamStatusesForPlayer(player)) {
+ if (status.isActive()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the player with the given IP address and username. If no username is given, only IP address is
+ * used as search criteria.
+ *
+ * @param ipAddress The IP address.
+ * @param username The remote user.
+ * @return The player with the given IP address, or <code>null</code> if no such player exists.
+ */
+ private Player getPlayerByIpAddressAndUsername(final String ipAddress, final String username) {
+ if (ipAddress == null) {
+ return null;
+ }
+ for (Player player : getAllPlayers()) {
+ boolean ipMatches = ipAddress.equals(player.getIpAddress());
+ boolean userMatches = username == null || username.equals(player.getUsername());
+ if (ipMatches && userMatches) {
+ return player;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reads the player ID from the cookie in the HTTP request.
+ *
+ * @param request The HTTP request.
+ * @param username The name of the current user.
+ * @return The player ID embedded in the cookie, or <code>null</code> if cookie is not present.
+ */
+ private String getPlayerIdFromCookie(HttpServletRequest request, String username) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies == null) {
+ return null;
+ }
+ String cookieName = COOKIE_NAME + "-" + StringUtil.utf8HexEncode(username);
+ for (Cookie cookie : cookies) {
+ if (cookieName.equals(cookie.getName())) {
+ return cookie.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all players owned by the given username and client ID.
+ *
+ * @param username The name of the user.
+ * @param clientId The third-party client ID (used if this player is managed over the
+ * Subsonic REST API). May be <code>null</code>.
+ * @return All relevant players.
+ */
+ public List<Player> getPlayersForUserAndClientId(String username, String clientId) {
+ return playerDao.getPlayersForUserAndClientId(username, clientId);
+ }
+
+ /**
+ * Returns all currently registered players.
+ *
+ * @return All currently registered players.
+ */
+ public List<Player> getAllPlayers() {
+ return playerDao.getAllPlayers();
+ }
+
+ /**
+ * Removes the player with the given ID.
+ *
+ * @param id The unique player ID.
+ */
+ public synchronized void removePlayerById(String id) {
+ playerDao.deletePlayer(id);
+ }
+
+ /**
+ * Creates and returns a clone of the given player.
+ *
+ * @param playerId The ID of the player to clone.
+ * @return The cloned player.
+ */
+ public Player clonePlayer(String playerId) {
+ Player player = getPlayerById(playerId);
+ if (player.getName() != null) {
+ player.setName(player.getName() + " (copy)");
+ }
+
+ createPlayer(player);
+ return player;
+ }
+
+ /**
+ * Creates the given player, and activates all transcodings.
+ *
+ * @param player The player to create.
+ */
+ public void createPlayer(Player player) {
+ playerDao.createPlayer(player);
+
+ List<Transcoding> transcodings = transcodingService.getAllTranscodings();
+ List<Transcoding> defaultActiveTranscodings = new ArrayList<Transcoding>();
+ for (Transcoding transcoding : transcodings) {
+ if (transcoding.isDefaultActive()) {
+ defaultActiveTranscodings.add(transcoding);
+ }
+ }
+
+ transcodingService.setTranscodingsForPlayer(player, defaultActiveTranscodings);
+ }
+
+ public void setStatusService(StatusService statusService) {
+ this.statusService = statusService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setPlayerDao(PlayerDao playerDao) {
+ this.playerDao = playerDao;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java
new file mode 100644
index 00000000..6208c3dc
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java
@@ -0,0 +1,426 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.util.Pair;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+import org.jdom.Namespace;
+import org.jdom.input.SAXBuilder;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.MediaFileDao;
+import net.sourceforge.subsonic.dao.PlaylistDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Playlist;
+import net.sourceforge.subsonic.util.StringUtil;
+
+/**
+ * Provides services for loading and saving playlists to and from persistent storage.
+ *
+ * @author Sindre Mehus
+ * @see net.sourceforge.subsonic.domain.PlayQueue
+ */
+public class PlaylistService {
+
+ private static final Logger LOG = Logger.getLogger(PlaylistService.class);
+ private MediaFileService mediaFileService;
+ private MediaFileDao mediaFileDao;
+ private PlaylistDao playlistDao;
+ private SecurityService securityService;
+ private SettingsService settingsService;
+
+ public void init() {
+ try {
+ importPlaylists();
+ } catch (Throwable x) {
+ LOG.warn("Failed to import playlists: " + x, x);
+ }
+ }
+
+ public List<Playlist> getReadablePlaylistsForUser(String username) {
+ return playlistDao.getReadablePlaylistsForUser(username);
+ }
+
+ public List<Playlist> getWritablePlaylistsForUser(String username) {
+
+ // Admin users are allowed to modify all playlists that are visible to them.
+ if (securityService.isAdmin(username)) {
+ return getReadablePlaylistsForUser(username);
+ }
+
+ return playlistDao.getWritablePlaylistsForUser(username);
+ }
+
+ public Playlist getPlaylist(int id) {
+ return playlistDao.getPlaylist(id);
+ }
+
+ public List<String> getPlaylistUsers(int playlistId) {
+ return playlistDao.getPlaylistUsers(playlistId);
+ }
+
+ public List<MediaFile> getFilesInPlaylist(int id) {
+ return mediaFileDao.getFilesInPlaylist(id);
+ }
+
+ public void setFilesInPlaylist(int id, List<MediaFile> files) {
+ playlistDao.setFilesInPlaylist(id, files);
+ }
+
+ public void createPlaylist(Playlist playlist) {
+ playlistDao.createPlaylist(playlist);
+ }
+
+ public void addPlaylistUser(int playlistId, String username) {
+ playlistDao.addPlaylistUser(playlistId, username);
+ }
+
+ public void deletePlaylistUser(int playlistId, String username) {
+ playlistDao.deletePlaylistUser(playlistId, username);
+ }
+
+ public boolean isReadAllowed(Playlist playlist, String username) {
+ if (username == null) {
+ return false;
+ }
+ if (username.equals(playlist.getUsername()) || playlist.isPublic()) {
+ return true;
+ }
+ return playlistDao.getPlaylistUsers(playlist.getId()).contains(username);
+ }
+
+ public boolean isWriteAllowed(Playlist playlist, String username) {
+ return username != null && username.equals(playlist.getUsername());
+ }
+
+ public void deletePlaylist(int id) {
+ playlistDao.deletePlaylist(id);
+ }
+
+ public void updatePlaylist(Playlist playlist) {
+ playlistDao.updatePlaylist(playlist);
+ }
+
+ public Playlist importPlaylist(String username, String playlistName, String fileName, String format, InputStream inputStream) throws Exception {
+ PlaylistFormat playlistFormat = PlaylistFormat.getPlaylistFormat(format);
+ if (playlistFormat == null) {
+ throw new Exception("Unsupported playlist format: " + format);
+ }
+
+ Pair<List<MediaFile>, List<String>> result = parseFiles(IOUtils.toByteArray(inputStream), playlistFormat);
+ if (result.getFirst().isEmpty() && !result.getSecond().isEmpty()) {
+ throw new Exception("No songs in the playlist were found.");
+ }
+
+ for (String error : result.getSecond()) {
+ LOG.warn("File in playlist '" + fileName + "' not found: " + error);
+ }
+
+ Date now = new Date();
+ Playlist playlist = new Playlist();
+ playlist.setUsername(username);
+ playlist.setCreated(now);
+ playlist.setChanged(now);
+ playlist.setPublic(true);
+ playlist.setName(playlistName);
+ playlist.setImportedFrom(fileName);
+
+ createPlaylist(playlist);
+ setFilesInPlaylist(playlist.getId(), result.getFirst());
+
+ return playlist;
+ }
+
+ private Pair<List<MediaFile>, List<String>> parseFiles(byte[] playlist, PlaylistFormat playlistFormat) throws IOException {
+ Pair<List<MediaFile>, List<String>> result = null;
+
+ // Try with multiple encodings; use the one that finds the most files.
+ String[] encodings = {StringUtil.ENCODING_LATIN, StringUtil.ENCODING_UTF8, Charset.defaultCharset().name()};
+ for (String encoding : encodings) {
+ Pair<List<MediaFile>, List<String>> files = parseFilesWithEncoding(playlist, playlistFormat, encoding);
+ if (result == null || result.getFirst().size() < files.getFirst().size()) {
+ result = files;
+ }
+ }
+ return result;
+ }
+
+ private Pair<List<MediaFile>, List<String>> parseFilesWithEncoding(byte[] playlist, PlaylistFormat playlistFormat, String encoding) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(playlist), encoding));
+ return playlistFormat.parse(reader, mediaFileService);
+ }
+
+ public void exportPlaylist(int id, OutputStream out) throws Exception {
+ PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StringUtil.ENCODING_UTF8));
+ new M3UFormat().format(getFilesInPlaylist(id), writer);
+ }
+
+ /**
+ * Implementation of M3U playlist format.
+ */
+ private void importPlaylists() throws Exception {
+ String playlistFolderPath = settingsService.getPlaylistFolder();
+ if (playlistFolderPath == null) {
+ return;
+ }
+ File playlistFolder = new File(playlistFolderPath);
+ if (!playlistFolder.exists()) {
+ return;
+ }
+
+ List<Playlist> allPlaylists = playlistDao.getAllPlaylists();
+ for (File file : playlistFolder.listFiles()) {
+ try {
+ importPlaylistIfNotExisting(file, allPlaylists);
+ } catch (Exception x) {
+ LOG.warn("Failed to auto-import playlist " + file + ". " + x.getMessage());
+ }
+ }
+ }
+
+ private void importPlaylistIfNotExisting(File file, List<Playlist> allPlaylists) throws Exception {
+ String format = FilenameUtils.getExtension(file.getPath());
+ if (PlaylistFormat.getPlaylistFormat(format) == null) {
+ return;
+ }
+
+ String fileName = file.getName();
+ for (Playlist playlist : allPlaylists) {
+ if (fileName.equals(playlist.getImportedFrom())) {
+ return; // Already imported.
+ }
+ }
+ InputStream in = new FileInputStream(file);
+ try {
+ importPlaylist(User.USERNAME_ADMIN, FilenameUtils.getBaseName(fileName), fileName, format, in);
+ LOG.info("Auto-imported playlist " + file);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ public void setPlaylistDao(PlaylistDao playlistDao) {
+ this.playlistDao = playlistDao;
+ }
+
+ public void setMediaFileDao(MediaFileDao mediaFileDao) {
+ this.mediaFileDao = mediaFileDao;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+ /**
+ * Abstract superclass for playlist formats.
+ */
+
+ private abstract static class PlaylistFormat {
+ public abstract Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException;
+
+ public abstract void format(List<MediaFile> files, PrintWriter writer) throws IOException;
+
+ public static PlaylistFormat getPlaylistFormat(String format) {
+ if (format == null) {
+ return null;
+ }
+ if (format.equalsIgnoreCase("m3u") || format.equalsIgnoreCase("m3u8")) {
+ return new M3UFormat();
+ }
+ if (format.equalsIgnoreCase("pls")) {
+ return new PLSFormat();
+ }
+ if (format.equalsIgnoreCase("xspf")) {
+ return new XSPFFormat();
+ }
+ return null;
+ }
+
+ protected MediaFile getMediaFile(MediaFileService mediaFileService, String path) {
+ try {
+ MediaFile file = mediaFileService.getMediaFile(path);
+ if (file != null && file.exists()) {
+ return file;
+ }
+ } catch (SecurityException x) {
+ // Ignored
+ }
+ return null;
+ }
+ }
+
+ private static class M3UFormat extends PlaylistFormat {
+ public Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException {
+ List<MediaFile> ok = new ArrayList<MediaFile>();
+ List<String> error = new ArrayList<String>();
+ String line = reader.readLine();
+ while (line != null) {
+ if (!line.startsWith("#")) {
+ MediaFile file = getMediaFile(mediaFileService, line);
+ if (file != null) {
+ ok.add(file);
+ } else {
+ error.add(line);
+ }
+ }
+ line = reader.readLine();
+ }
+ return new Pair<List<MediaFile>, List<String>>(ok, error);
+ }
+
+ public void format(List<MediaFile> files, PrintWriter writer) throws IOException {
+ writer.println("#EXTM3U");
+ for (MediaFile file : files) {
+ writer.println(file.getPath());
+ }
+ if (writer.checkError()) {
+ throw new IOException("Error when writing playlist");
+ }
+ }
+ }
+
+ /**
+ * Implementation of PLS playlist format.
+ */
+ private static class PLSFormat extends PlaylistFormat {
+ public Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException {
+ List<MediaFile> ok = new ArrayList<MediaFile>();
+ List<String> error = new ArrayList<String>();
+
+ Pattern pattern = Pattern.compile("^File\\d+=(.*)$");
+ String line = reader.readLine();
+ while (line != null) {
+
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ String path = matcher.group(1);
+ MediaFile file = getMediaFile(mediaFileService, path);
+ if (file != null) {
+ ok.add(file);
+ } else {
+ error.add(path);
+ }
+ }
+ line = reader.readLine();
+ }
+ return new Pair<List<MediaFile>, List<String>>(ok, error);
+ }
+
+ public void format(List<MediaFile> files, PrintWriter writer) throws IOException {
+ writer.println("[playlist]");
+ int counter = 0;
+
+ for (MediaFile file : files) {
+ counter++;
+ writer.println("File" + counter + '=' + file.getPath());
+ }
+ writer.println("NumberOfEntries=" + counter);
+ writer.println("Version=2");
+
+ if (writer.checkError()) {
+ throw new IOException("Error when writing playlist.");
+ }
+ }
+ }
+
+ /**
+ * Implementation of XSPF (http://www.xspf.org/) playlist format.
+ */
+ private static class XSPFFormat extends PlaylistFormat {
+ public Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException {
+ List<MediaFile> ok = new ArrayList<MediaFile>();
+ List<String> error = new ArrayList<String>();
+
+ SAXBuilder builder = new SAXBuilder();
+ Document document;
+ try {
+ document = builder.build(reader);
+ } catch (JDOMException x) {
+ LOG.warn("Failed to parse XSPF playlist.", x);
+ throw new IOException("Failed to parse XSPF playlist.");
+ }
+
+ Element root = document.getRootElement();
+ Namespace ns = root.getNamespace();
+ Element trackList = root.getChild("trackList", ns);
+ List<?> tracks = trackList.getChildren("track", ns);
+
+ for (Object obj : tracks) {
+ Element track = (Element) obj;
+ String location = track.getChildText("location", ns);
+ if (location != null && location.startsWith("file://")) {
+ location = location.replaceFirst("file://", "");
+ MediaFile file = getMediaFile(mediaFileService, location);
+ if (file != null) {
+ ok.add(file);
+ } else {
+ error.add(location);
+ }
+ }
+ }
+ return new Pair<List<MediaFile>, List<String>>(ok, error);
+ }
+
+ public void format(List<MediaFile> files, PrintWriter writer) throws IOException {
+ writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ writer.println("<playlist version=\"1\" xmlns=\"http://xspf.org/ns/0/\">");
+ writer.println(" <trackList>");
+
+ for (MediaFile file : files) {
+ writer.println(" <track><location>file://" + StringEscapeUtils.escapeXml(file.getPath()) + "</location></track>");
+ }
+ writer.println(" </trackList>");
+ writer.println("</playlist>");
+
+ if (writer.checkError()) {
+ throw new IOException("Error when writing playlist.");
+ }
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java
new file mode 100644
index 00000000..09184df6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java
@@ -0,0 +1,599 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.PodcastDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.PodcastChannel;
+import net.sourceforge.subsonic.domain.PodcastEpisode;
+import net.sourceforge.subsonic.domain.PodcastStatus;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+import org.jdom.input.SAXBuilder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides services for Podcast reception.
+ *
+ * @author Sindre Mehus
+ */
+public class PodcastService {
+
+ private static final Logger LOG = Logger.getLogger(PodcastService.class);
+ private static final DateFormat[] RSS_DATE_FORMATS = {new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US),
+ new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z", Locale.US)};
+
+ private static final Namespace[] ITUNES_NAMESPACES = {Namespace.getNamespace("http://www.itunes.com/DTDs/Podcast-1.0.dtd"),
+ Namespace.getNamespace("http://www.itunes.com/dtds/podcast-1.0.dtd")};
+
+ private final ExecutorService refreshExecutor;
+ private final ExecutorService downloadExecutor;
+ private final ScheduledExecutorService scheduledExecutor;
+ private ScheduledFuture<?> scheduledRefresh;
+ private PodcastDao podcastDao;
+ private SettingsService settingsService;
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+
+ public PodcastService() {
+ ThreadFactory threadFactory = new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread t = Executors.defaultThreadFactory().newThread(r);
+ t.setDaemon(true);
+ return t;
+ }
+ };
+ refreshExecutor = Executors.newFixedThreadPool(5, threadFactory);
+ downloadExecutor = Executors.newFixedThreadPool(3, threadFactory);
+ scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
+ }
+
+ public synchronized void init() {
+ // Clean up partial downloads.
+ for (PodcastChannel channel : getAllChannels()) {
+ for (PodcastEpisode episode : getEpisodes(channel.getId(), false)) {
+ if (episode.getStatus() == PodcastStatus.DOWNLOADING) {
+ deleteEpisode(episode.getId(), false);
+ LOG.info("Deleted Podcast episode '" + episode.getTitle() + "' since download was interrupted.");
+ }
+ }
+ }
+
+ schedule();
+ }
+
+ public synchronized void schedule() {
+ Runnable task = new Runnable() {
+ public void run() {
+ LOG.info("Starting scheduled Podcast refresh.");
+ refreshAllChannels(true);
+ LOG.info("Completed scheduled Podcast refresh.");
+ }
+ };
+
+ if (scheduledRefresh != null) {
+ scheduledRefresh.cancel(true);
+ }
+
+ int hoursBetween = settingsService.getPodcastUpdateInterval();
+
+ if (hoursBetween == -1) {
+ LOG.info("Automatic Podcast update disabled.");
+ return;
+ }
+
+ long periodMillis = hoursBetween * 60L * 60L * 1000L;
+ long initialDelayMillis = 5L * 60L * 1000L;
+
+ scheduledRefresh = scheduledExecutor.scheduleAtFixedRate(task, initialDelayMillis, periodMillis, TimeUnit.MILLISECONDS);
+ Date firstTime = new Date(System.currentTimeMillis() + initialDelayMillis);
+ LOG.info("Automatic Podcast update scheduled to run every " + hoursBetween + " hour(s), starting at " + firstTime);
+ }
+
+ /**
+ * Creates a new Podcast channel.
+ *
+ * @param url The URL of the Podcast channel.
+ */
+ public void createChannel(String url) {
+ url = sanitizeUrl(url);
+ PodcastChannel channel = new PodcastChannel(url);
+ int channelId = podcastDao.createChannel(channel);
+
+ refreshChannels(Arrays.asList(getChannel(channelId)), true);
+ }
+
+ private String sanitizeUrl(String url) {
+ return url.replace(" ", "%20");
+ }
+
+ private PodcastChannel getChannel(int channelId) {
+ for (PodcastChannel channel : getAllChannels()) {
+ if (channelId == channel.getId()) {
+ return channel;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all Podcast channels.
+ *
+ * @return Possibly empty list of all Podcast channels.
+ */
+ public List<PodcastChannel> getAllChannels() {
+ return podcastDao.getAllChannels();
+ }
+
+ /**
+ * Returns all Podcast episodes for a given channel.
+ *
+ * @param channelId The Podcast channel ID.
+ * @param includeDeleted Whether to include logically deleted episodes in the result.
+ * @return Possibly empty list of all Podcast episodes for the given channel, sorted in
+ * reverse chronological order (newest episode first).
+ */
+ public List<PodcastEpisode> getEpisodes(int channelId, boolean includeDeleted) {
+ List<PodcastEpisode> all = podcastDao.getEpisodes(channelId);
+ addMediaFileIdToEpisodes(all);
+ if (includeDeleted) {
+ return all;
+ }
+
+ List<PodcastEpisode> filtered = new ArrayList<PodcastEpisode>();
+ for (PodcastEpisode episode : all) {
+ if (episode.getStatus() != PodcastStatus.DELETED) {
+ filtered.add(episode);
+ }
+ }
+ return filtered;
+ }
+
+ public PodcastEpisode getEpisode(int episodeId, boolean includeDeleted) {
+ PodcastEpisode episode = podcastDao.getEpisode(episodeId);
+ if (episode == null) {
+ return null;
+ }
+ if (episode.getStatus() == PodcastStatus.DELETED && !includeDeleted) {
+ return null;
+ }
+ addMediaFileIdToEpisodes(Arrays.asList(episode));
+ return episode;
+ }
+
+ private void addMediaFileIdToEpisodes(List<PodcastEpisode> episodes) {
+ for (PodcastEpisode episode : episodes) {
+ if (episode.getPath() != null) {
+ MediaFile mediaFile = mediaFileService.getMediaFile(episode.getPath());
+ if (mediaFile != null) {
+ episode.setMediaFileId(mediaFile.getId());
+ }
+ }
+ }
+ }
+
+ private PodcastEpisode getEpisode(int channelId, String url) {
+ if (url == null) {
+ return null;
+ }
+
+ for (PodcastEpisode episode : getEpisodes(channelId, true)) {
+ if (url.equals(episode.getUrl())) {
+ return episode;
+ }
+ }
+ return null;
+ }
+
+ public void refreshAllChannels(boolean downloadEpisodes) {
+ refreshChannels(getAllChannels(), downloadEpisodes);
+ }
+
+ private void refreshChannels(final List<PodcastChannel> channels, final boolean downloadEpisodes) {
+ for (final PodcastChannel channel : channels) {
+ Runnable task = new Runnable() {
+ public void run() {
+ doRefreshChannel(channel, downloadEpisodes);
+ }
+ };
+ refreshExecutor.submit(task);
+ }
+ }
+
+ @SuppressWarnings({"unchecked"})
+ private void doRefreshChannel(PodcastChannel channel, boolean downloadEpisodes) {
+ InputStream in = null;
+ HttpClient client = new DefaultHttpClient();
+
+ try {
+ channel.setStatus(PodcastStatus.DOWNLOADING);
+ channel.setErrorMessage(null);
+ podcastDao.updateChannel(channel);
+
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 2 * 60 * 1000); // 2 minutes
+ HttpConnectionParams.setSoTimeout(client.getParams(), 10 * 60 * 1000); // 10 minutes
+ HttpGet method = new HttpGet(channel.getUrl());
+
+ HttpResponse response = client.execute(method);
+ in = response.getEntity().getContent();
+
+ Document document = new SAXBuilder().build(in);
+ Element channelElement = document.getRootElement().getChild("channel");
+
+ channel.setTitle(channelElement.getChildTextTrim("title"));
+ channel.setDescription(channelElement.getChildTextTrim("description"));
+ channel.setStatus(PodcastStatus.COMPLETED);
+ channel.setErrorMessage(null);
+ podcastDao.updateChannel(channel);
+
+ refreshEpisodes(channel, channelElement.getChildren("item"));
+
+ } catch (Exception x) {
+ LOG.warn("Failed to get/parse RSS file for Podcast channel " + channel.getUrl(), x);
+ channel.setStatus(PodcastStatus.ERROR);
+ channel.setErrorMessage(x.toString());
+ podcastDao.updateChannel(channel);
+ } finally {
+ IOUtils.closeQuietly(in);
+ client.getConnectionManager().shutdown();
+ }
+
+ if (downloadEpisodes) {
+ for (final PodcastEpisode episode : getEpisodes(channel.getId(), false)) {
+ if (episode.getStatus() == PodcastStatus.NEW && episode.getUrl() != null) {
+ downloadEpisode(episode);
+ }
+ }
+ }
+ }
+
+ public void downloadEpisode(final PodcastEpisode episode) {
+ Runnable task = new Runnable() {
+ public void run() {
+ doDownloadEpisode(episode);
+ }
+ };
+ downloadExecutor.submit(task);
+ }
+
+ private void refreshEpisodes(PodcastChannel channel, List<Element> episodeElements) {
+
+ List<PodcastEpisode> episodes = new ArrayList<PodcastEpisode>();
+
+ for (Element episodeElement : episodeElements) {
+
+ String title = episodeElement.getChildTextTrim("title");
+ String duration = getITunesElement(episodeElement, "duration");
+ String description = episodeElement.getChildTextTrim("description");
+ if (StringUtils.isBlank(description)) {
+ description = getITunesElement(episodeElement, "summary");
+ }
+
+ Element enclosure = episodeElement.getChild("enclosure");
+ if (enclosure == null) {
+ LOG.debug("No enclosure found for episode " + title);
+ continue;
+ }
+
+ String url = enclosure.getAttributeValue("url");
+ url = sanitizeUrl(url);
+ if (url == null) {
+ LOG.debug("No enclosure URL found for episode " + title);
+ continue;
+ }
+
+ if (getEpisode(channel.getId(), url) == null) {
+ Long length = null;
+ try {
+ length = new Long(enclosure.getAttributeValue("length"));
+ } catch (Exception x) {
+ LOG.warn("Failed to parse enclosure length.", x);
+ }
+
+ Date date = parseDate(episodeElement.getChildTextTrim("pubDate"));
+ PodcastEpisode episode = new PodcastEpisode(null, channel.getId(), url, null, title, description, date,
+ duration, length, 0L, PodcastStatus.NEW, null);
+ episodes.add(episode);
+ LOG.info("Created Podcast episode " + title);
+ }
+ }
+
+ // Sort episode in reverse chronological order (newest first)
+ Collections.sort(episodes, new Comparator<PodcastEpisode>() {
+ public int compare(PodcastEpisode a, PodcastEpisode b) {
+ long timeA = a.getPublishDate() == null ? 0L : a.getPublishDate().getTime();
+ long timeB = b.getPublishDate() == null ? 0L : b.getPublishDate().getTime();
+
+ if (timeA < timeB) {
+ return 1;
+ }
+ if (timeA > timeB) {
+ return -1;
+ }
+ return 0;
+ }
+ });
+
+ // Create episodes in database, skipping the proper number of episodes.
+ int downloadCount = settingsService.getPodcastEpisodeDownloadCount();
+ if (downloadCount == -1) {
+ downloadCount = Integer.MAX_VALUE;
+ }
+
+ for (int i = 0; i < episodes.size(); i++) {
+ PodcastEpisode episode = episodes.get(i);
+ if (i >= downloadCount) {
+ episode.setStatus(PodcastStatus.SKIPPED);
+ }
+ podcastDao.createEpisode(episode);
+ }
+ }
+
+ private Date parseDate(String s) {
+ for (DateFormat dateFormat : RSS_DATE_FORMATS) {
+ try {
+ return dateFormat.parse(s);
+ } catch (Exception x) {
+ // Ignored.
+ }
+ }
+ LOG.warn("Failed to parse publish date: '" + s + "'.");
+ return null;
+ }
+
+ private String getITunesElement(Element element, String childName) {
+ for (Namespace ns : ITUNES_NAMESPACES) {
+ String value = element.getChildTextTrim(childName, ns);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ private void doDownloadEpisode(PodcastEpisode episode) {
+ InputStream in = null;
+ OutputStream out = null;
+
+ if (getEpisode(episode.getId(), false) == null) {
+ LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download.");
+ return;
+ }
+
+ LOG.info("Starting to download Podcast from " + episode.getUrl());
+
+ HttpClient client = new DefaultHttpClient();
+ try {
+ PodcastChannel channel = getChannel(episode.getChannelId());
+
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 2 * 60 * 1000); // 2 minutes
+ HttpConnectionParams.setSoTimeout(client.getParams(), 10 * 60 * 1000); // 10 minutes
+ HttpGet method = new HttpGet(episode.getUrl());
+
+ HttpResponse response = client.execute(method);
+ in = response.getEntity().getContent();
+
+ File file = getFile(channel, episode);
+ out = new FileOutputStream(file);
+
+ episode.setStatus(PodcastStatus.DOWNLOADING);
+ episode.setBytesDownloaded(0L);
+ episode.setErrorMessage(null);
+ episode.setPath(file.getPath());
+ podcastDao.updateEpisode(episode);
+
+ byte[] buffer = new byte[4096];
+ long bytesDownloaded = 0;
+ int n;
+ long nextLogCount = 30000L;
+
+ while ((n = in.read(buffer)) != -1) {
+ out.write(buffer, 0, n);
+ bytesDownloaded += n;
+
+ if (bytesDownloaded > nextLogCount) {
+ episode.setBytesDownloaded(bytesDownloaded);
+ nextLogCount += 30000L;
+ if (getEpisode(episode.getId(), false) == null) {
+ break;
+ }
+ podcastDao.updateEpisode(episode);
+ }
+ }
+
+ if (getEpisode(episode.getId(), false) == null) {
+ LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download.");
+ IOUtils.closeQuietly(out);
+ file.delete();
+ } else {
+ episode.setBytesDownloaded(bytesDownloaded);
+ podcastDao.updateEpisode(episode);
+ LOG.info("Downloaded " + bytesDownloaded + " bytes from Podcast " + episode.getUrl());
+ IOUtils.closeQuietly(out);
+ episode.setStatus(PodcastStatus.COMPLETED);
+ podcastDao.updateEpisode(episode);
+ deleteObsoleteEpisodes(channel);
+ }
+
+ } catch (Exception x) {
+ LOG.warn("Failed to download Podcast from " + episode.getUrl(), x);
+ episode.setStatus(PodcastStatus.ERROR);
+ episode.setErrorMessage(x.toString());
+ podcastDao.updateEpisode(episode);
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(out);
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ private synchronized void deleteObsoleteEpisodes(PodcastChannel channel) {
+ int episodeCount = settingsService.getPodcastEpisodeRetentionCount();
+ if (episodeCount == -1) {
+ return;
+ }
+
+ List<PodcastEpisode> episodes = getEpisodes(channel.getId(), false);
+
+ // Don't do anything if other episodes of the same channel is currently downloading.
+ for (PodcastEpisode episode : episodes) {
+ if (episode.getStatus() == PodcastStatus.DOWNLOADING) {
+ return;
+ }
+ }
+
+ // Reverse array to get chronological order (oldest episodes first).
+ Collections.reverse(episodes);
+
+ int episodesToDelete = Math.max(0, episodes.size() - episodeCount);
+ for (int i = 0; i < episodesToDelete; i++) {
+ deleteEpisode(episodes.get(i).getId(), true);
+ LOG.info("Deleted old Podcast episode " + episodes.get(i).getUrl());
+ }
+ }
+
+ private synchronized File getFile(PodcastChannel channel, PodcastEpisode episode) {
+
+ File podcastDir = new File(settingsService.getPodcastFolder());
+ File channelDir = new File(podcastDir, StringUtil.fileSystemSafe(channel.getTitle()));
+
+ if (!channelDir.exists()) {
+ boolean ok = channelDir.mkdirs();
+ if (!ok) {
+ throw new RuntimeException("Failed to create directory " + channelDir);
+ }
+
+ MediaFile mediaFile = mediaFileService.getMediaFile(channelDir);
+ mediaFile.setComment(channel.getDescription());
+ mediaFileService.updateMediaFile(mediaFile);
+ }
+
+ String filename = StringUtil.getUrlFile(episode.getUrl());
+ if (filename == null) {
+ filename = episode.getTitle();
+ }
+ filename = StringUtil.fileSystemSafe(filename);
+ String extension = FilenameUtils.getExtension(filename);
+ filename = FilenameUtils.removeExtension(filename);
+ if (StringUtils.isBlank(extension)) {
+ extension = "mp3";
+ }
+
+ File file = new File(channelDir, filename + "." + extension);
+ for (int i = 0; file.exists(); i++) {
+ file = new File(channelDir, filename + i + "." + extension);
+ }
+
+ if (!securityService.isWriteAllowed(file)) {
+ throw new SecurityException("Access denied to file " + file);
+ }
+ return file;
+ }
+
+ /**
+ * Deletes the Podcast channel with the given ID.
+ *
+ * @param channelId The Podcast channel ID.
+ */
+ public void deleteChannel(int channelId) {
+ // Delete all associated episodes (in case they have files that need to be deleted).
+ List<PodcastEpisode> episodes = getEpisodes(channelId, false);
+ for (PodcastEpisode episode : episodes) {
+ deleteEpisode(episode.getId(), false);
+ }
+ podcastDao.deleteChannel(channelId);
+ }
+
+ /**
+ * Deletes the Podcast episode with the given ID.
+ *
+ * @param episodeId The Podcast episode ID.
+ * @param logicalDelete Whether to perform a logical delete by setting the
+ * episode status to {@link PodcastStatus#DELETED}.
+ */
+ public void deleteEpisode(int episodeId, boolean logicalDelete) {
+ PodcastEpisode episode = podcastDao.getEpisode(episodeId);
+ if (episode == null) {
+ return;
+ }
+
+ // Delete file.
+ if (episode.getPath() != null) {
+ File file = new File(episode.getPath());
+ if (file.exists()) {
+ file.delete();
+ // TODO: Delete directory if empty?
+ }
+ }
+
+ if (logicalDelete) {
+ episode.setStatus(PodcastStatus.DELETED);
+ episode.setErrorMessage(null);
+ podcastDao.updateEpisode(episode);
+ } else {
+ podcastDao.deleteEpisode(episodeId);
+ }
+ }
+
+ public void setPodcastDao(PodcastDao podcastDao) {
+ this.podcastDao = podcastDao;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.java
new file mode 100644
index 00000000..6208faf2
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.java
@@ -0,0 +1,101 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.dao.*;
+import net.sourceforge.subsonic.domain.*;
+import net.sourceforge.subsonic.util.FileUtil;
+
+import java.util.*;
+import java.io.File;
+
+/**
+ * Provides services for user ratings.
+ *
+ * @author Sindre Mehus
+ */
+public class RatingService {
+
+ private RatingDao ratingDao;
+ private SecurityService securityService;
+ private MediaFileService mediaFileService;
+
+ /**
+ * Returns the highest rated music files.
+ *
+ * @param offset Number of files to skip.
+ * @param count Maximum number of files to return.
+ * @return The highest rated music files.
+ */
+ public List<MediaFile> getHighestRated(int offset, int count) {
+ List<String> highestRated = ratingDao.getHighestRated(offset, count);
+ List<MediaFile> result = new ArrayList<MediaFile>();
+ for (String path : highestRated) {
+ File file = new File(path);
+ if (FileUtil.exists(file) && securityService.isReadAllowed(file)) {
+ result.add(mediaFileService.getMediaFile(path));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Sets the rating for a music file and a given user.
+ *
+ * @param username The user name.
+ * @param mediaFile The music file.
+ * @param rating The rating between 1 and 5, or <code>null</code> to remove the rating.
+ */
+ public void setRatingForUser(String username, MediaFile mediaFile, Integer rating) {
+ ratingDao.setRatingForUser(username, mediaFile, rating);
+ }
+
+ /**
+ * Returns the average rating for the given music file.
+ *
+ * @param mediaFile The music file.
+ * @return The average rating, or <code>null</code> if no ratings are set.
+ */
+ public Double getAverageRating(MediaFile mediaFile) {
+ return ratingDao.getAverageRating(mediaFile);
+ }
+
+ /**
+ * Returns the rating for the given user and music file.
+ *
+ * @param username The user name.
+ * @param mediaFile The music file.
+ * @return The rating, or <code>null</code> if no rating is set.
+ */
+ public Integer getRatingForUser(String username, MediaFile mediaFile) {
+ return ratingDao.getRatingForUser(username, mediaFile);
+ }
+
+ public void setRatingDao(RatingDao ratingDao) {
+ this.ratingDao = ratingDao;
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java
new file mode 100644
index 00000000..2698bbd6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java
@@ -0,0 +1,567 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.AlbumDao;
+import net.sourceforge.subsonic.dao.ArtistDao;
+import net.sourceforge.subsonic.domain.Album;
+import net.sourceforge.subsonic.domain.Artist;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.RandomSearchCriteria;
+import net.sourceforge.subsonic.domain.SearchCriteria;
+import net.sourceforge.subsonic.domain.SearchResult;
+import net.sourceforge.subsonic.util.FileUtil;
+import org.apache.lucene.analysis.ASCIIFoldingFilter;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.LowerCaseFilter;
+import org.apache.lucene.analysis.StopFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.analysis.standard.StandardFilter;
+import org.apache.lucene.analysis.standard.StandardTokenizer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.NumericField;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queryParser.MultiFieldQueryParser;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.Version;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import static net.sourceforge.subsonic.service.SearchService.IndexType.*;
+import static net.sourceforge.subsonic.service.SearchService.IndexType.SONG;
+
+/**
+ * Performs Lucene-based searching and indexing.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ * @see MediaScannerService
+ */
+public class SearchService {
+
+ private static final Logger LOG = Logger.getLogger(SearchService.class);
+
+ private static final String FIELD_ID = "id";
+ private static final String FIELD_TITLE = "title";
+ private static final String FIELD_ALBUM = "album";
+ private static final String FIELD_ARTIST = "artist";
+ private static final String FIELD_GENRE = "genre";
+ private static final String FIELD_YEAR = "year";
+ private static final String FIELD_MEDIA_TYPE = "mediaType";
+ private static final String FIELD_FOLDER = "folder";
+
+ private static final Version LUCENE_VERSION = Version.LUCENE_30;
+
+ private MediaFileService mediaFileService;
+ private SettingsService settingsService;
+ private ArtistDao artistDao;
+ private AlbumDao albumDao;
+
+ private IndexWriter artistWriter;
+ private IndexWriter artistId3Writer;
+ private IndexWriter albumWriter;
+ private IndexWriter albumId3Writer;
+ private IndexWriter songWriter;
+
+ public SearchService() {
+ removeLocks();
+ }
+
+
+ public void startIndexing() {
+ try {
+ artistWriter = createIndexWriter(ARTIST);
+ artistId3Writer = createIndexWriter(ARTIST_ID3);
+ albumWriter = createIndexWriter(ALBUM);
+ albumId3Writer = createIndexWriter(ALBUM_ID3);
+ songWriter = createIndexWriter(SONG);
+ } catch (Exception x) {
+ LOG.error("Failed to create search index.", x);
+ }
+ }
+
+ public void index(MediaFile mediaFile) {
+ try {
+ if (mediaFile.isFile()) {
+ songWriter.addDocument(SONG.createDocument(mediaFile));
+ } else if (mediaFile.isAlbum()) {
+ albumWriter.addDocument(ALBUM.createDocument(mediaFile));
+ } else {
+ artistWriter.addDocument(ARTIST.createDocument(mediaFile));
+ }
+ } catch (Exception x) {
+ LOG.error("Failed to create search index for " + mediaFile, x);
+ }
+ }
+
+ public void index(Artist artist) {
+ try {
+ artistId3Writer.addDocument(ARTIST_ID3.createDocument(artist));
+ } catch (Exception x) {
+ LOG.error("Failed to create search index for " + artist, x);
+ }
+ }
+
+ public void index(Album album) {
+ try {
+ albumId3Writer.addDocument(ALBUM_ID3.createDocument(album));
+ } catch (Exception x) {
+ LOG.error("Failed to create search index for " + album, x);
+ }
+ }
+
+ public void stopIndexing() {
+ try {
+ artistWriter.optimize();
+ artistId3Writer.optimize();
+ albumWriter.optimize();
+ albumId3Writer.optimize();
+ songWriter.optimize();
+ } catch (Exception x) {
+ LOG.error("Failed to create search index.", x);
+ } finally {
+ FileUtil.closeQuietly(artistId3Writer);
+ FileUtil.closeQuietly(artistWriter);
+ FileUtil.closeQuietly(albumWriter);
+ FileUtil.closeQuietly(albumId3Writer);
+ FileUtil.closeQuietly(songWriter);
+ }
+ }
+
+ public SearchResult search(SearchCriteria criteria, IndexType indexType) {
+ SearchResult result = new SearchResult();
+ int offset = criteria.getOffset();
+ int count = criteria.getCount();
+ result.setOffset(offset);
+
+ IndexReader reader = null;
+ try {
+ reader = createIndexReader(indexType);
+ Searcher searcher = new IndexSearcher(reader);
+ Analyzer analyzer = new SubsonicAnalyzer();
+
+ MultiFieldQueryParser queryParser = new MultiFieldQueryParser(LUCENE_VERSION, indexType.getFields(), analyzer, indexType.getBoosts());
+ Query query = queryParser.parse(criteria.getQuery());
+
+ TopDocs topDocs = searcher.search(query, null, offset + count);
+ result.setTotalHits(topDocs.totalHits);
+
+ int start = Math.min(offset, topDocs.totalHits);
+ int end = Math.min(start + count, topDocs.totalHits);
+ for (int i = start; i < end; i++) {
+ Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
+ switch (indexType) {
+ case SONG:
+ case ARTIST:
+ case ALBUM:
+ MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID)));
+ addIfNotNull(mediaFile, result.getMediaFiles());
+ break;
+ case ARTIST_ID3:
+ Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID)));
+ addIfNotNull(artist, result.getArtists());
+ break;
+ case ALBUM_ID3:
+ Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID)));
+ addIfNotNull(album, result.getAlbums());
+ break;
+ default:
+ break;
+ }
+ }
+
+ } catch (Throwable x) {
+ LOG.error("Failed to execute Lucene search.", x);
+ } finally {
+ FileUtil.closeQuietly(reader);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a number of random songs.
+ *
+ * @param criteria Search criteria.
+ * @return List of random songs.
+ */
+ public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+
+ String musicFolderPath = null;
+ if (criteria.getMusicFolderId() != null) {
+ MusicFolder musicFolder = settingsService.getMusicFolderById(criteria.getMusicFolderId());
+ musicFolderPath = musicFolder.getPath().getPath();
+ }
+
+ IndexReader reader = null;
+ try {
+ reader = createIndexReader(SONG);
+ Searcher searcher = new IndexSearcher(reader);
+
+ BooleanQuery query = new BooleanQuery();
+ query.add(new TermQuery(new Term(FIELD_MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())), BooleanClause.Occur.MUST);
+ if (criteria.getGenre() != null) {
+ String genre = normalizeGenre(criteria.getGenre());
+ query.add(new TermQuery(new Term(FIELD_GENRE, genre)), BooleanClause.Occur.MUST);
+ }
+ if (criteria.getFromYear() != null || criteria.getToYear() != null) {
+ NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FIELD_YEAR, criteria.getFromYear(), criteria.getToYear(), true, true);
+ query.add(rangeQuery, BooleanClause.Occur.MUST);
+ }
+ if (musicFolderPath != null) {
+ query.add(new TermQuery(new Term(FIELD_FOLDER, musicFolderPath)), BooleanClause.Occur.MUST);
+ }
+
+ TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
+ Random random = new Random(System.currentTimeMillis());
+
+ for (int i = 0; i < Math.min(criteria.getCount(), topDocs.totalHits); i++) {
+ int index = random.nextInt(topDocs.totalHits);
+ Document doc = searcher.doc(topDocs.scoreDocs[index].doc);
+ int id = Integer.valueOf(doc.get(FIELD_ID));
+ try {
+ result.add(mediaFileService.getMediaFile(id));
+ } catch (Exception x) {
+ LOG.warn("Failed to get media file " + id);
+ }
+ }
+
+ } catch (Throwable x) {
+ LOG.error("Failed to search or random songs.", x);
+ } finally {
+ FileUtil.closeQuietly(reader);
+ }
+ return result;
+ }
+
+ private static String normalizeGenre(String genre) {
+ return genre.toLowerCase().replace(" ", "");
+ }
+
+ /**
+ * Returns a number of random albums.
+ *
+ * @param count Number of albums to return.
+ * @return List of random albums.
+ */
+ public List<MediaFile> getRandomAlbums(int count) {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+
+ IndexReader reader = null;
+ try {
+ reader = createIndexReader(ALBUM);
+ Searcher searcher = new IndexSearcher(reader);
+
+ Query query = new MatchAllDocsQuery();
+ TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
+ Random random = new Random(System.currentTimeMillis());
+
+ for (int i = 0; i < Math.min(count, topDocs.totalHits); i++) {
+ int index = random.nextInt(topDocs.totalHits);
+ Document doc = searcher.doc(topDocs.scoreDocs[index].doc);
+ int id = Integer.valueOf(doc.get(FIELD_ID));
+ try {
+ addIfNotNull(mediaFileService.getMediaFile(id), result);
+ } catch (Exception x) {
+ LOG.warn("Failed to get media file " + id, x);
+ }
+ }
+
+ } catch (Throwable x) {
+ LOG.error("Failed to search for random albums.", x);
+ } finally {
+ FileUtil.closeQuietly(reader);
+ }
+ return result;
+ }
+
+ /**
+ * Returns a number of random albums, using ID3 tag.
+ *
+ * @param count Number of albums to return.
+ * @return List of random albums.
+ */
+ public List<Album> getRandomAlbumsId3(int count) {
+ List<Album> result = new ArrayList<Album>();
+
+ IndexReader reader = null;
+ try {
+ reader = createIndexReader(ALBUM_ID3);
+ Searcher searcher = new IndexSearcher(reader);
+
+ Query query = new MatchAllDocsQuery();
+ TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
+ Random random = new Random(System.currentTimeMillis());
+
+ for (int i = 0; i < Math.min(count, topDocs.totalHits); i++) {
+ int index = random.nextInt(topDocs.totalHits);
+ Document doc = searcher.doc(topDocs.scoreDocs[index].doc);
+ int id = Integer.valueOf(doc.get(FIELD_ID));
+ try {
+ addIfNotNull(albumDao.getAlbum(id), result);
+ } catch (Exception x) {
+ LOG.warn("Failed to get album file " + id, x);
+ }
+ }
+
+ } catch (Throwable x) {
+ LOG.error("Failed to search for random albums.", x);
+ } finally {
+ FileUtil.closeQuietly(reader);
+ }
+ return result;
+ }
+
+ private <T> void addIfNotNull(T value, List<T> list) {
+ if (value != null) {
+ list.add(value);
+ }
+ }
+ private IndexWriter createIndexWriter(IndexType indexType) throws IOException {
+ File dir = getIndexDirectory(indexType);
+ return new IndexWriter(FSDirectory.open(dir), new SubsonicAnalyzer(), true, new IndexWriter.MaxFieldLength(10));
+ }
+
+ private IndexReader createIndexReader(IndexType indexType) throws IOException {
+ File dir = getIndexDirectory(indexType);
+ return IndexReader.open(FSDirectory.open(dir), true);
+ }
+
+ private File getIndexRootDirectory() {
+ return new File(SettingsService.getSubsonicHome(), "lucene2");
+ }
+
+ private File getIndexDirectory(IndexType indexType) {
+ return new File(getIndexRootDirectory(), indexType.toString().toLowerCase());
+ }
+
+ private void removeLocks() {
+ for (IndexType indexType : IndexType.values()) {
+ Directory dir = null;
+ try {
+ dir = FSDirectory.open(getIndexDirectory(indexType));
+ if (IndexWriter.isLocked(dir)) {
+ IndexWriter.unlock(dir);
+ LOG.info("Removed Lucene lock file in " + dir);
+ }
+ } catch (Exception x) {
+ LOG.warn("Failed to remove Lucene lock file in " + dir, x);
+ } finally {
+ FileUtil.closeQuietly(dir);
+ }
+ }
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setArtistDao(ArtistDao artistDao) {
+ this.artistDao = artistDao;
+ }
+
+ public void setAlbumDao(AlbumDao albumDao) {
+ this.albumDao = albumDao;
+ }
+
+ public static enum IndexType {
+
+ SONG(new String[]{FIELD_TITLE, FIELD_ARTIST}, FIELD_TITLE) {
+ @Override
+ public Document createDocument(MediaFile mediaFile) {
+ Document doc = new Document();
+ doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
+ doc.add(new Field(FIELD_MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO, Field.Index.ANALYZED_NO_NORMS));
+
+ if (mediaFile.getTitle() != null) {
+ doc.add(new Field(FIELD_TITLE, mediaFile.getTitle(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+ if (mediaFile.getArtist() != null) {
+ doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+ if (mediaFile.getGenre() != null) {
+ doc.add(new Field(FIELD_GENRE, normalizeGenre(mediaFile.getGenre()), Field.Store.NO, Field.Index.ANALYZED));
+ }
+ if (mediaFile.getYear() != null) {
+ doc.add(new NumericField(FIELD_YEAR, Field.Store.NO, true).setIntValue(mediaFile.getYear()));
+ }
+ if (mediaFile.getFolder() != null) {
+ doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS));
+ }
+
+ return doc;
+ }
+ },
+
+ ALBUM(new String[]{FIELD_ALBUM, FIELD_ARTIST}, FIELD_ALBUM) {
+ @Override
+ public Document createDocument(MediaFile mediaFile) {
+ Document doc = new Document();
+ doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
+
+ if (mediaFile.getArtist() != null) {
+ doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+ if (mediaFile.getAlbumName() != null) {
+ doc.add(new Field(FIELD_ALBUM, mediaFile.getAlbumName(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+
+ return doc;
+ }
+ },
+
+ ALBUM_ID3(new String[]{FIELD_ALBUM, FIELD_ARTIST}, FIELD_ALBUM) {
+ @Override
+ public Document createDocument(Album album) {
+ Document doc = new Document();
+ doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(album.getId()));
+
+ if (album.getArtist() != null) {
+ doc.add(new Field(FIELD_ARTIST, album.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+ if (album.getName() != null) {
+ doc.add(new Field(FIELD_ALBUM, album.getName(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+
+ return doc;
+ }
+ },
+
+ ARTIST(new String[]{FIELD_ARTIST}, null) {
+ @Override
+ public Document createDocument(MediaFile mediaFile) {
+ Document doc = new Document();
+ doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
+
+ if (mediaFile.getArtist() != null) {
+ doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
+ }
+
+ return doc;
+ }
+ },
+
+ ARTIST_ID3(new String[]{FIELD_ARTIST}, null) {
+ @Override
+ public Document createDocument(Artist artist) {
+ Document doc = new Document();
+ doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(artist.getId()));
+ doc.add(new Field(FIELD_ARTIST, artist.getName(), Field.Store.YES, Field.Index.ANALYZED));
+
+ return doc;
+ }
+ };
+
+ private final String[] fields;
+ private final Map<String, Float> boosts;
+
+ private IndexType(String[] fields, String boostedField) {
+ this.fields = fields;
+ boosts = new HashMap<String, Float>();
+ if (boostedField != null) {
+ boosts.put(boostedField, 2.0F);
+ }
+ }
+
+ public String[] getFields() {
+ return fields;
+ }
+
+ protected Document createDocument(MediaFile mediaFile) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected Document createDocument(Artist artist) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected Document createDocument(Album album) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Map<String, Float> getBoosts() {
+ return boosts;
+ }
+ }
+
+ private class SubsonicAnalyzer extends StandardAnalyzer {
+ private SubsonicAnalyzer() {
+ super(LUCENE_VERSION);
+ }
+
+ @Override
+ public TokenStream tokenStream(String fieldName, Reader reader) {
+ TokenStream result = super.tokenStream(fieldName, reader);
+ return new ASCIIFoldingFilter(result);
+ }
+
+ @Override
+ public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException {
+ class SavedStreams {
+ StandardTokenizer tokenStream;
+ TokenStream filteredTokenStream;
+ }
+
+ SavedStreams streams = (SavedStreams) getPreviousTokenStream();
+ if (streams == null) {
+ streams = new SavedStreams();
+ setPreviousTokenStream(streams);
+ streams.tokenStream = new StandardTokenizer(LUCENE_VERSION, reader);
+ streams.filteredTokenStream = new StandardFilter(streams.tokenStream);
+ streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream);
+ streams.filteredTokenStream = new StopFilter(true, streams.filteredTokenStream, STOP_WORDS_SET);
+ streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream);
+ } else {
+ streams.tokenStream.reset(reader);
+ }
+ streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH);
+
+ return streams.filteredTokenStream;
+ }
+ }
+}
+
+
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java
new file mode 100644
index 00000000..d6ca871d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java
@@ -0,0 +1,303 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.File;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.acegisecurity.providers.dao.DaoAuthenticationProvider;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.UserDetailsService;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper;
+import org.springframework.dao.DataAccessException;
+
+import net.sf.ehcache.Ehcache;
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.UserDao;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.User;
+import net.sourceforge.subsonic.util.FileUtil;
+
+/**
+ * Provides security-related services for authentication and authorization.
+ *
+ * @author Sindre Mehus
+ */
+public class SecurityService implements UserDetailsService {
+
+ private static final Logger LOG = Logger.getLogger(SecurityService.class);
+
+ private UserDao userDao;
+ private SettingsService settingsService;
+ private Ehcache userCache;
+
+ /**
+ * Locates the user based on the username.
+ *
+ * @param username The username presented to the {@link DaoAuthenticationProvider}
+ * @return A fully populated user record (never <code>null</code>)
+ * @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority.
+ * @throws DataAccessException If user could not be found for a repository-specific reason.
+ */
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
+ User user = getUserByName(username);
+ if (user == null) {
+ throw new UsernameNotFoundException("User \"" + username + "\" was not found.");
+ }
+
+ String[] roles = userDao.getRolesForUser(username);
+ GrantedAuthority[] authorities = new GrantedAuthority[roles.length];
+ for (int i = 0; i < roles.length; i++) {
+ authorities[i] = new GrantedAuthorityImpl("ROLE_" + roles[i].toUpperCase());
+ }
+
+ // If user is LDAP authenticated, disable user. The proper authentication should in that case
+ // be done by SubsonicLdapBindAuthenticator.
+ boolean enabled = !user.isLdapAuthenticated();
+
+ return new org.acegisecurity.userdetails.User(username, user.getPassword(), enabled, true, true, true, authorities);
+ }
+
+ /**
+ * Returns the currently logged-in user for the given HTTP request.
+ *
+ * @param request The HTTP request.
+ * @return The logged-in user, or <code>null</code>.
+ */
+ public User getCurrentUser(HttpServletRequest request) {
+ String username = getCurrentUsername(request);
+ return username == null ? null : userDao.getUserByName(username);
+ }
+
+ /**
+ * Returns the name of the currently logged-in user.
+ *
+ * @param request The HTTP request.
+ * @return The name of the logged-in user, or <code>null</code>.
+ */
+ public String getCurrentUsername(HttpServletRequest request) {
+ return new SecurityContextHolderAwareRequestWrapper(request, null).getRemoteUser();
+ }
+
+ /**
+ * Returns the user with the given username.
+ *
+ * @param username The username used when logging in.
+ * @return The user, or <code>null</code> if not found.
+ */
+ public User getUserByName(String username) {
+ return userDao.getUserByName(username);
+ }
+
+ /**
+ * Returns the user with the given email address.
+ *
+ * @param email The email address.
+ * @return The user, or <code>null</code> if not found.
+ */
+ public User getUserByEmail(String email) {
+ return userDao.getUserByEmail(email);
+ }
+
+ /**
+ * Returns all users.
+ *
+ * @return Possibly empty array of all users.
+ */
+ public List<User> getAllUsers() {
+ return userDao.getAllUsers();
+ }
+
+ /**
+ * Returns whether the given user has administrative rights.
+ */
+ public boolean isAdmin(String username) {
+ if (User.USERNAME_ADMIN.equals(username)) {
+ return true;
+ }
+ User user = getUserByName(username);
+ return user != null && user.isAdminRole();
+ }
+
+ /**
+ * Creates a new user.
+ *
+ * @param user The user to create.
+ */
+ public void createUser(User user) {
+ userDao.createUser(user);
+ LOG.info("Created user " + user.getUsername());
+ }
+
+ /**
+ * Deletes the user with the given username.
+ *
+ * @param username The username.
+ */
+ public void deleteUser(String username) {
+ userDao.deleteUser(username);
+ LOG.info("Deleted user " + username);
+ userCache.remove(username);
+ }
+
+ /**
+ * Updates the given user.
+ *
+ * @param user The user to update.
+ */
+ public void updateUser(User user) {
+ userDao.updateUser(user);
+ userCache.remove(user.getUsername());
+ }
+
+ /**
+ * Updates the byte counts for given user.
+ *
+ * @param user The user to update, may be <code>null</code>.
+ * @param bytesStreamedDelta Increment bytes streamed count with this value.
+ * @param bytesDownloadedDelta Increment bytes downloaded count with this value.
+ * @param bytesUploadedDelta Increment bytes uploaded count with this value.
+ */
+ public void updateUserByteCounts(User user, long bytesStreamedDelta, long bytesDownloadedDelta, long bytesUploadedDelta) {
+ if (user == null) {
+ return;
+ }
+
+ user.setBytesStreamed(user.getBytesStreamed() + bytesStreamedDelta);
+ user.setBytesDownloaded(user.getBytesDownloaded() + bytesDownloadedDelta);
+ user.setBytesUploaded(user.getBytesUploaded() + bytesUploadedDelta);
+
+ userDao.updateUser(user);
+ }
+
+ /**
+ * Returns whether the given file may be read.
+ *
+ * @return Whether the given file may be read.
+ */
+ public boolean isReadAllowed(File file) {
+ // Allowed to read from both music folder and podcast folder.
+ return isInMusicFolder(file) || isInPodcastFolder(file);
+ }
+
+ /**
+ * Returns whether the given file may be written, created or deleted.
+ *
+ * @return Whether the given file may be written, created or deleted.
+ */
+ public boolean isWriteAllowed(File file) {
+ // Only allowed to write podcasts or cover art.
+ boolean isPodcast = isInPodcastFolder(file);
+ boolean isCoverArt = isInMusicFolder(file) && file.getName().startsWith("cover.");
+
+ return isPodcast || isCoverArt;
+ }
+
+ /**
+ * Returns whether the given file may be uploaded.
+ *
+ * @return Whether the given file may be uploaded.
+ */
+ public boolean isUploadAllowed(File file) {
+ return isInMusicFolder(file) && !FileUtil.exists(file);
+ }
+
+ /**
+ * Returns whether the given file is located in one of the music folders (or any of their sub-folders).
+ *
+ * @param file The file in question.
+ * @return Whether the given file is located in one of the music folders.
+ */
+ private boolean isInMusicFolder(File file) {
+ return getMusicFolderForFile(file) != null;
+ }
+
+ private MusicFolder getMusicFolderForFile(File file) {
+ List<MusicFolder> folders = settingsService.getAllMusicFolders(false, true);
+ String path = file.getPath();
+ for (MusicFolder folder : folders) {
+ if (isFileInFolder(path, folder.getPath().getPath())) {
+ return folder;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the given file is located in the Podcast folder (or any of its sub-folders).
+ *
+ * @param file The file in question.
+ * @return Whether the given file is located in the Podcast folder.
+ */
+ private boolean isInPodcastFolder(File file) {
+ String podcastFolder = settingsService.getPodcastFolder();
+ return isFileInFolder(file.getPath(), podcastFolder);
+ }
+
+ public String getRootFolderForFile(File file) {
+ MusicFolder folder = getMusicFolderForFile(file);
+ if (folder != null) {
+ return folder.getPath().getPath();
+ }
+
+ if (isInPodcastFolder(file)) {
+ return settingsService.getPodcastFolder();
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the given file is located in the given folder (or any of its sub-folders).
+ * If the given file contains the expression ".." (indicating a reference to the parent directory),
+ * this method will return <code>false</code>.
+ *
+ * @param file The file in question.
+ * @param folder The folder in question.
+ * @return Whether the given file is located in the given folder.
+ */
+ protected boolean isFileInFolder(String file, String folder) {
+ // Deny access if file contains ".." surrounded by slashes (or end of line).
+ if (file.matches(".*(/|\\\\)\\.\\.(/|\\\\|$).*")) {
+ return false;
+ }
+
+ // Convert slashes.
+ file = file.replace('\\', '/');
+ folder = folder.replace('\\', '/');
+
+ return file.toUpperCase().startsWith(folder.toUpperCase());
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setUserDao(UserDao userDao) {
+ this.userDao = userDao;
+ }
+
+ public void setUserCache(Ehcache userCache) {
+ this.userCache = userCache;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.java
new file mode 100644
index 00000000..4a7f95b4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.java
@@ -0,0 +1,46 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import javax.xml.parsers.SAXParser;
+
+import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory;
+
+/**
+ * Locates services for objects that are not part of the Spring context.
+ *
+ * @author Sindre Mehus
+ */
+@Deprecated
+public class ServiceLocator {
+
+ private static SettingsService settingsService;
+
+ private ServiceLocator() {
+ }
+
+ public static SettingsService getSettingsService() {
+ return settingsService;
+ }
+
+ public static void setSettingsService(SettingsService settingsService) {
+ ServiceLocator.settingsService = settingsService;
+ }
+}
+
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java
new file mode 100644
index 00000000..afb5cdc7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java
@@ -0,0 +1,1254 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.AvatarDao;
+import net.sourceforge.subsonic.dao.InternetRadioDao;
+import net.sourceforge.subsonic.dao.MusicFolderDao;
+import net.sourceforge.subsonic.dao.UserDao;
+import net.sourceforge.subsonic.domain.Avatar;
+import net.sourceforge.subsonic.domain.InternetRadio;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.domain.Theme;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.util.FileUtil;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.Util;
+
+/**
+ * Provides persistent storage of application settings and preferences.
+ *
+ * @author Sindre Mehus
+ */
+public class SettingsService {
+
+ // Subsonic home directory.
+ private static final File SUBSONIC_HOME_WINDOWS = new File("c:/subsonic");
+ private static final File SUBSONIC_HOME_OTHER = new File("/var/subsonic");
+
+ // Global settings.
+ private static final String KEY_INDEX_STRING = "IndexString";
+ private static final String KEY_IGNORED_ARTICLES = "IgnoredArticles";
+ private static final String KEY_SHORTCUTS = "Shortcuts";
+ private static final String KEY_PLAYLIST_FOLDER = "PlaylistFolder";
+ private static final String KEY_MUSIC_FILE_TYPES = "MusicFileTypes";
+ private static final String KEY_VIDEO_FILE_TYPES = "VideoFileTypes";
+ private static final String KEY_COVER_ART_FILE_TYPES = "CoverArtFileTypes";
+ private static final String KEY_COVER_ART_LIMIT = "CoverArtLimit";
+ private static final String KEY_WELCOME_TITLE = "WelcomeTitle";
+ private static final String KEY_WELCOME_SUBTITLE = "WelcomeSubtitle";
+ private static final String KEY_WELCOME_MESSAGE = "WelcomeMessage2";
+ private static final String KEY_LOGIN_MESSAGE = "LoginMessage";
+ private static final String KEY_LOCALE_LANGUAGE = "LocaleLanguage";
+ private static final String KEY_LOCALE_COUNTRY = "LocaleCountry";
+ private static final String KEY_LOCALE_VARIANT = "LocaleVariant";
+ private static final String KEY_THEME_ID = "Theme";
+ private static final String KEY_INDEX_CREATION_INTERVAL = "IndexCreationInterval";
+ private static final String KEY_INDEX_CREATION_HOUR = "IndexCreationHour";
+ private static final String KEY_FAST_CACHE_ENABLED = "FastCacheEnabled";
+ private static final String KEY_PODCAST_UPDATE_INTERVAL = "PodcastUpdateInterval";
+ private static final String KEY_PODCAST_FOLDER = "PodcastFolder";
+ private static final String KEY_PODCAST_EPISODE_RETENTION_COUNT = "PodcastEpisodeRetentionCount";
+ private static final String KEY_PODCAST_EPISODE_DOWNLOAD_COUNT = "PodcastEpisodeDownloadCount";
+ private static final String KEY_DOWNLOAD_BITRATE_LIMIT = "DownloadBitrateLimit";
+ private static final String KEY_UPLOAD_BITRATE_LIMIT = "UploadBitrateLimit";
+ private static final String KEY_STREAM_PORT = "StreamPort";
+ private static final String KEY_LICENSE_EMAIL = "LicenseEmail";
+ private static final String KEY_LICENSE_CODE = "LicenseCode";
+ private static final String KEY_LICENSE_DATE = "LicenseDate";
+ private static final String KEY_DOWNSAMPLING_COMMAND = "DownsamplingCommand3";
+ private static final String KEY_JUKEBOX_COMMAND = "JukeboxCommand";
+ private static final String KEY_REWRITE_URL = "RewriteUrl";
+ private static final String KEY_LDAP_ENABLED = "LdapEnabled";
+ private static final String KEY_LDAP_URL = "LdapUrl";
+ private static final String KEY_LDAP_MANAGER_DN = "LdapManagerDn";
+ private static final String KEY_LDAP_MANAGER_PASSWORD = "LdapManagerPassword";
+ private static final String KEY_LDAP_SEARCH_FILTER = "LdapSearchFilter";
+ private static final String KEY_LDAP_AUTO_SHADOWING = "LdapAutoShadowing";
+ private static final String KEY_GETTING_STARTED_ENABLED = "GettingStartedEnabled";
+ private static final String KEY_PORT_FORWARDING_ENABLED = "PortForwardingEnabled";
+ private static final String KEY_PORT = "Port";
+ private static final String KEY_HTTPS_PORT = "HttpsPort";
+ private static final String KEY_URL_REDIRECTION_ENABLED = "UrlRedirectionEnabled";
+ private static final String KEY_URL_REDIRECT_FROM = "UrlRedirectFrom";
+ private static final String KEY_URL_REDIRECT_TRIAL_EXPIRES = "UrlRedirectTrialExpires";
+ private static final String KEY_URL_REDIRECT_CONTEXT_PATH = "UrlRedirectContextPath";
+ private static final String KEY_REST_TRIAL_EXPIRES = "RestTrialExpires-";
+ private static final String KEY_VIDEO_TRIAL_EXPIRES = "VideoTrialExpires";
+ private static final String KEY_SERVER_ID = "ServerId";
+ private static final String KEY_SETTINGS_CHANGED = "SettingsChanged";
+ private static final String KEY_LAST_SCANNED = "LastScanned";
+ private static final String KEY_ORGANIZE_BY_FOLDER_STRUCTURE = "OrganizeByFolderStructure";
+ private static final String KEY_SORT_ALBUMS_BY_YEAR = "SortAlbumsByYear";
+
+ // Default values.
+ private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)";
+ private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les";
+ private static final String DEFAULT_SHORTCUTS = "New Incoming Podcast";
+ private static final String DEFAULT_PLAYLIST_FOLDER = Util.getDefaultPlaylistFolder();
+ private static final String DEFAULT_MUSIC_FILE_TYPES = "mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn";
+ private static final String DEFAULT_VIDEO_FILE_TYPES = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts";
+ private static final String DEFAULT_COVER_ART_FILE_TYPES = "cover.jpg folder.jpg jpg jpeg gif png";
+ private static final int DEFAULT_COVER_ART_LIMIT = 30;
+ private static final String DEFAULT_WELCOME_TITLE = "Welcome to Subsonic!";
+ private static final String DEFAULT_WELCOME_SUBTITLE = null;
+ private static final String DEFAULT_WELCOME_MESSAGE = "__Welcome to Subsonic!__\n" +
+ "\\\\ \\\\\n" +
+ "Subsonic is a free, web-based media streamer, providing ubiquitous access to your music. \n" +
+ "\\\\ \\\\\n" +
+ "Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple " +
+ "players simultaneously, for instance to one player in your kitchen and another in your living room.\n" +
+ "\\\\ \\\\\n" +
+ "To change or remove this message, log in with administrator rights and go to {link:Settings > General|generalSettings.view}.";
+ private static final String DEFAULT_LOGIN_MESSAGE = null;
+ private static final String DEFAULT_LOCALE_LANGUAGE = "en";
+ private static final String DEFAULT_LOCALE_COUNTRY = "";
+ private static final String DEFAULT_LOCALE_VARIANT = "";
+ private static final String DEFAULT_THEME_ID = "default";
+ private static final int DEFAULT_INDEX_CREATION_INTERVAL = 1;
+ private static final int DEFAULT_INDEX_CREATION_HOUR = 3;
+ private static final boolean DEFAULT_FAST_CACHE_ENABLED = false;
+ private static final int DEFAULT_PODCAST_UPDATE_INTERVAL = 24;
+ private static final String DEFAULT_PODCAST_FOLDER = Util.getDefaultPodcastFolder();
+ private static final int DEFAULT_PODCAST_EPISODE_RETENTION_COUNT = 10;
+ private static final int DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT = 1;
+ private static final long DEFAULT_DOWNLOAD_BITRATE_LIMIT = 0;
+ private static final long DEFAULT_UPLOAD_BITRATE_LIMIT = 0;
+ private static final long DEFAULT_STREAM_PORT = 0;
+ private static final String DEFAULT_LICENSE_EMAIL = null;
+ private static final String DEFAULT_LICENSE_CODE = null;
+ private static final String DEFAULT_LICENSE_DATE = null;
+ private static final String DEFAULT_DOWNSAMPLING_COMMAND = "ffmpeg -i %s -ab %bk -v 0 -f mp3 -";
+ private static final String DEFAULT_JUKEBOX_COMMAND = "ffmpeg -ss %o -i %s -v 0 -f au -";
+ private static final boolean DEFAULT_REWRITE_URL = true;
+ private static final boolean DEFAULT_LDAP_ENABLED = false;
+ private static final String DEFAULT_LDAP_URL = "ldap://host.domain.com:389/cn=Users,dc=domain,dc=com";
+ private static final String DEFAULT_LDAP_MANAGER_DN = null;
+ private static final String DEFAULT_LDAP_MANAGER_PASSWORD = null;
+ private static final String DEFAULT_LDAP_SEARCH_FILTER = "(sAMAccountName={0})";
+ private static final boolean DEFAULT_LDAP_AUTO_SHADOWING = false;
+ private static final boolean DEFAULT_PORT_FORWARDING_ENABLED = false;
+ private static final boolean DEFAULT_GETTING_STARTED_ENABLED = true;
+ private static final int DEFAULT_PORT = 80;
+ private static final int DEFAULT_HTTPS_PORT = 0;
+ private static final boolean DEFAULT_URL_REDIRECTION_ENABLED = false;
+ private static final String DEFAULT_URL_REDIRECT_FROM = "yourname";
+ private static final String DEFAULT_URL_REDIRECT_TRIAL_EXPIRES = null;
+ private static final String DEFAULT_URL_REDIRECT_CONTEXT_PATH = null;
+ private static final String DEFAULT_REST_TRIAL_EXPIRES = null;
+ private static final String DEFAULT_VIDEO_TRIAL_EXPIRES = null;
+ private static final String DEFAULT_SERVER_ID = null;
+ private static final long DEFAULT_SETTINGS_CHANGED = 0L;
+ private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true;
+ private static final boolean DEFAULT_SORT_ALBUMS_BY_YEAR = true;
+
+ // Array of obsolete keys. Used to clean property file.
+ private static final List<String> OBSOLETE_KEYS = Arrays.asList("PortForwardingPublicPort", "PortForwardingLocalPort",
+ "DownsamplingCommand", "DownsamplingCommand2", "AutoCoverBatch", "MusicMask", "VideoMask", "CoverArtMask");
+
+ private static final String LOCALES_FILE = "/net/sourceforge/subsonic/i18n/locales.txt";
+ private static final String THEMES_FILE = "/net/sourceforge/subsonic/theme/themes.txt";
+
+ private static final Logger LOG = Logger.getLogger(SettingsService.class);
+
+ private Properties properties = new Properties();
+ private List<Theme> themes;
+ private List<Locale> locales;
+ private InternetRadioDao internetRadioDao;
+ private MusicFolderDao musicFolderDao;
+ private UserDao userDao;
+ private AvatarDao avatarDao;
+ private VersionService versionService;
+
+ private String[] cachedCoverArtFileTypesArray;
+ private String[] cachedMusicFileTypesArray;
+ private String[] cachedVideoFileTypesArray;
+ private List<MusicFolder> cachedMusicFolders;
+
+ private static File subsonicHome;
+
+ private boolean licenseValidated = true;
+
+ public SettingsService() {
+ File propertyFile = getPropertyFile();
+
+ if (propertyFile.exists()) {
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(propertyFile);
+ properties.load(in);
+ } catch (Exception x) {
+ LOG.error("Unable to read from property file.", x);
+ } finally {
+ IOUtils.closeQuietly(in);
+ }
+
+ // Remove obsolete properties.
+ for (Iterator<Object> iterator = properties.keySet().iterator(); iterator.hasNext();) {
+ String key = (String) iterator.next();
+ if (OBSOLETE_KEYS.contains(key)) {
+ LOG.debug("Removing obsolete property [" + key + ']');
+ iterator.remove();
+ }
+ }
+ }
+
+ save(false);
+ }
+
+ /**
+ * Register in service locator so that non-Spring objects can access me.
+ * This method is invoked automatically by Spring.
+ */
+ public void init() {
+ ServiceLocator.setSettingsService(this);
+ validateLicenseAsync();
+ }
+
+ public void save() {
+ save(true);
+ }
+
+ public void save(boolean updateChangedDate) {
+ if (updateChangedDate) {
+ setProperty(KEY_SETTINGS_CHANGED, String.valueOf(System.currentTimeMillis()));
+ }
+
+ OutputStream out = null;
+ try {
+ out = new FileOutputStream(getPropertyFile());
+ properties.store(out, "Subsonic preferences. NOTE: This file is automatically generated.");
+ } catch (Exception x) {
+ LOG.error("Unable to write to property file.", x);
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
+ private File getPropertyFile() {
+ return new File(getSubsonicHome(), "subsonic.properties");
+ }
+
+ /**
+ * Returns the Subsonic home directory.
+ *
+ * @return The Subsonic home directory, if it exists.
+ * @throws RuntimeException If directory doesn't exist.
+ */
+ public static synchronized File getSubsonicHome() {
+
+ if (subsonicHome != null) {
+ return subsonicHome;
+ }
+
+ File home;
+
+ String overrideHome = System.getProperty("subsonic.home");
+ if (overrideHome != null) {
+ home = new File(overrideHome);
+ } else {
+ boolean isWindows = System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows");
+ home = isWindows ? SUBSONIC_HOME_WINDOWS : SUBSONIC_HOME_OTHER;
+ }
+
+ // Attempt to create home directory if it doesn't exist.
+ if (!home.exists() || !home.isDirectory()) {
+ boolean success = home.mkdirs();
+ if (success) {
+ subsonicHome = home;
+ } else {
+ String message = "The directory " + home + " does not exist. Please create it and make it writable. " +
+ "(You can override the directory location by specifying -Dsubsonic.home=... when " +
+ "starting the servlet container.)";
+ System.err.println("ERROR: " + message);
+ }
+ } else {
+ subsonicHome = home;
+ }
+
+ return home;
+ }
+
+ private boolean getBoolean(String key, boolean defaultValue) {
+ return Boolean.valueOf(properties.getProperty(key, String.valueOf(defaultValue)));
+ }
+
+ private void setBoolean(String key, boolean value) {
+ setProperty(key, String.valueOf(value));
+ }
+
+ public String getIndexString() {
+ return properties.getProperty(KEY_INDEX_STRING, DEFAULT_INDEX_STRING);
+ }
+
+ public void setIndexString(String indexString) {
+ setProperty(KEY_INDEX_STRING, indexString);
+ }
+
+ public String getIgnoredArticles() {
+ return properties.getProperty(KEY_IGNORED_ARTICLES, DEFAULT_IGNORED_ARTICLES);
+ }
+
+ public String[] getIgnoredArticlesAsArray() {
+ return getIgnoredArticles().split("\\s+");
+ }
+
+ public void setIgnoredArticles(String ignoredArticles) {
+ setProperty(KEY_IGNORED_ARTICLES, ignoredArticles);
+ }
+
+ public String getShortcuts() {
+ return properties.getProperty(KEY_SHORTCUTS, DEFAULT_SHORTCUTS);
+ }
+
+ public String[] getShortcutsAsArray() {
+ return StringUtil.split(getShortcuts());
+ }
+
+ public void setShortcuts(String shortcuts) {
+ setProperty(KEY_SHORTCUTS, shortcuts);
+ }
+
+ public String getPlaylistFolder() {
+ return properties.getProperty(KEY_PLAYLIST_FOLDER, DEFAULT_PLAYLIST_FOLDER);
+ }
+
+ public String getMusicFileTypes() {
+ return properties.getProperty(KEY_MUSIC_FILE_TYPES, DEFAULT_MUSIC_FILE_TYPES);
+ }
+
+ public synchronized void setMusicFileTypes(String fileTypes) {
+ setProperty(KEY_MUSIC_FILE_TYPES, fileTypes);
+ cachedMusicFileTypesArray = null;
+ }
+
+ public synchronized String[] getMusicFileTypesAsArray() {
+ if (cachedMusicFileTypesArray == null) {
+ cachedMusicFileTypesArray = toStringArray(getMusicFileTypes());
+ }
+ return cachedMusicFileTypesArray;
+ }
+
+ public String getVideoFileTypes() {
+ return properties.getProperty(KEY_VIDEO_FILE_TYPES, DEFAULT_VIDEO_FILE_TYPES);
+ }
+
+ public synchronized void setVideoFileTypes(String fileTypes) {
+ setProperty(KEY_VIDEO_FILE_TYPES, fileTypes);
+ cachedVideoFileTypesArray = null;
+ }
+
+ public synchronized String[] getVideoFileTypesAsArray() {
+ if (cachedVideoFileTypesArray == null) {
+ cachedVideoFileTypesArray = toStringArray(getVideoFileTypes());
+ }
+ return cachedVideoFileTypesArray;
+ }
+
+ public String getCoverArtFileTypes() {
+ return properties.getProperty(KEY_COVER_ART_FILE_TYPES, DEFAULT_COVER_ART_FILE_TYPES);
+ }
+
+ public synchronized void setCoverArtFileTypes(String fileTypes) {
+ setProperty(KEY_COVER_ART_FILE_TYPES, fileTypes);
+ cachedCoverArtFileTypesArray = null;
+ }
+
+ public synchronized String[] getCoverArtFileTypesAsArray() {
+ if (cachedCoverArtFileTypesArray == null) {
+ cachedCoverArtFileTypesArray = toStringArray(getCoverArtFileTypes());
+ }
+ return cachedCoverArtFileTypesArray;
+ }
+
+ public int getCoverArtLimit() {
+ return Integer.parseInt(properties.getProperty(KEY_COVER_ART_LIMIT, "" + DEFAULT_COVER_ART_LIMIT));
+ }
+
+ public void setCoverArtLimit(int limit) {
+ setProperty(KEY_COVER_ART_LIMIT, "" + limit);
+ }
+
+ public String getWelcomeTitle() {
+ return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_TITLE, DEFAULT_WELCOME_TITLE));
+ }
+
+ public void setWelcomeTitle(String title) {
+ setProperty(KEY_WELCOME_TITLE, title);
+ }
+
+ public String getWelcomeSubtitle() {
+ return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_SUBTITLE, DEFAULT_WELCOME_SUBTITLE));
+ }
+
+ public void setWelcomeSubtitle(String subtitle) {
+ setProperty(KEY_WELCOME_SUBTITLE, subtitle);
+ }
+
+ public String getWelcomeMessage() {
+ return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_MESSAGE, DEFAULT_WELCOME_MESSAGE));
+ }
+
+ public void setWelcomeMessage(String message) {
+ setProperty(KEY_WELCOME_MESSAGE, message);
+ }
+
+ public String getLoginMessage() {
+ return StringUtils.trimToNull(properties.getProperty(KEY_LOGIN_MESSAGE, DEFAULT_LOGIN_MESSAGE));
+ }
+
+ public void setLoginMessage(String message) {
+ setProperty(KEY_LOGIN_MESSAGE, message);
+ }
+
+ /**
+ * Returns the number of days between automatic index creation, of -1 if automatic index
+ * creation is disabled.
+ */
+ public int getIndexCreationInterval() {
+ return Integer.parseInt(properties.getProperty(KEY_INDEX_CREATION_INTERVAL, "" + DEFAULT_INDEX_CREATION_INTERVAL));
+ }
+
+ /**
+ * Sets the number of days between automatic index creation, of -1 if automatic index
+ * creation is disabled.
+ */
+ public void setIndexCreationInterval(int days) {
+ setProperty(KEY_INDEX_CREATION_INTERVAL, String.valueOf(days));
+ }
+
+ /**
+ * Returns the hour of day (0 - 23) when automatic index creation should run.
+ */
+ public int getIndexCreationHour() {
+ return Integer.parseInt(properties.getProperty(KEY_INDEX_CREATION_HOUR, String.valueOf(DEFAULT_INDEX_CREATION_HOUR)));
+ }
+
+ /**
+ * Sets the hour of day (0 - 23) when automatic index creation should run.
+ */
+ public void setIndexCreationHour(int hour) {
+ setProperty(KEY_INDEX_CREATION_HOUR, String.valueOf(hour));
+ }
+
+ public boolean isFastCacheEnabled() {
+ return getBoolean(KEY_FAST_CACHE_ENABLED, DEFAULT_FAST_CACHE_ENABLED);
+ }
+
+ public void setFastCacheEnabled(boolean enabled) {
+ setBoolean(KEY_FAST_CACHE_ENABLED, enabled);
+ }
+
+ /**
+ * Returns the number of hours between Podcast updates, of -1 if automatic updates
+ * are disabled.
+ */
+ public int getPodcastUpdateInterval() {
+ return Integer.parseInt(properties.getProperty(KEY_PODCAST_UPDATE_INTERVAL, String.valueOf(DEFAULT_PODCAST_UPDATE_INTERVAL)));
+ }
+
+ /**
+ * Sets the number of hours between Podcast updates, of -1 if automatic updates
+ * are disabled.
+ */
+ public void setPodcastUpdateInterval(int hours) {
+ setProperty(KEY_PODCAST_UPDATE_INTERVAL, String.valueOf(hours));
+ }
+
+ /**
+ * Returns the number of Podcast episodes to keep (-1 to keep all).
+ */
+ public int getPodcastEpisodeRetentionCount() {
+ return Integer.parseInt(properties.getProperty(KEY_PODCAST_EPISODE_RETENTION_COUNT, String.valueOf(DEFAULT_PODCAST_EPISODE_RETENTION_COUNT)));
+ }
+
+ /**
+ * Sets the number of Podcast episodes to keep (-1 to keep all).
+ */
+ public void setPodcastEpisodeRetentionCount(int count) {
+ setProperty(KEY_PODCAST_EPISODE_RETENTION_COUNT, String.valueOf(count));
+ }
+
+ /**
+ * Returns the number of Podcast episodes to download (-1 to download all).
+ */
+ public int getPodcastEpisodeDownloadCount() {
+ return Integer.parseInt(properties.getProperty(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, String.valueOf(DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT)));
+ }
+
+ /**
+ * Sets the number of Podcast episodes to download (-1 to download all).
+ */
+ public void setPodcastEpisodeDownloadCount(int count) {
+ setProperty(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, String.valueOf(count));
+ }
+
+ /**
+ * Returns the Podcast download folder.
+ */
+ public String getPodcastFolder() {
+ return properties.getProperty(KEY_PODCAST_FOLDER, DEFAULT_PODCAST_FOLDER);
+ }
+
+ /**
+ * Sets the Podcast download folder.
+ */
+ public void setPodcastFolder(String folder) {
+ setProperty(KEY_PODCAST_FOLDER, folder);
+ }
+
+ /**
+ * @return The download bitrate limit in Kbit/s. Zero if unlimited.
+ */
+ public long getDownloadBitrateLimit() {
+ return Long.parseLong(properties.getProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + DEFAULT_DOWNLOAD_BITRATE_LIMIT));
+ }
+
+ /**
+ * @param limit The download bitrate limit in Kbit/s. Zero if unlimited.
+ */
+ public void setDownloadBitrateLimit(long limit) {
+ setProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + limit);
+ }
+
+ /**
+ * @return The upload bitrate limit in Kbit/s. Zero if unlimited.
+ */
+ public long getUploadBitrateLimit() {
+ return Long.parseLong(properties.getProperty(KEY_UPLOAD_BITRATE_LIMIT, "" + DEFAULT_UPLOAD_BITRATE_LIMIT));
+ }
+
+ /**
+ * @param limit The upload bitrate limit in Kbit/s. Zero if unlimited.
+ */
+ public void setUploadBitrateLimit(long limit) {
+ setProperty(KEY_UPLOAD_BITRATE_LIMIT, "" + limit);
+ }
+
+ /**
+ * @return The non-SSL stream port. Zero if disabled.
+ */
+ public int getStreamPort() {
+ return Integer.parseInt(properties.getProperty(KEY_STREAM_PORT, "" + DEFAULT_STREAM_PORT));
+ }
+
+ /**
+ * @param port The non-SSL stream port. Zero if disabled.
+ */
+ public void setStreamPort(int port) {
+ setProperty(KEY_STREAM_PORT, "" + port);
+ }
+
+ public String getLicenseEmail() {
+ return properties.getProperty(KEY_LICENSE_EMAIL, DEFAULT_LICENSE_EMAIL);
+ }
+
+ public void setLicenseEmail(String email) {
+ setProperty(KEY_LICENSE_EMAIL, email);
+ }
+
+ public String getLicenseCode() {
+ return properties.getProperty(KEY_LICENSE_CODE, DEFAULT_LICENSE_CODE);
+ }
+
+ public void setLicenseCode(String code) {
+ setProperty(KEY_LICENSE_CODE, code);
+ }
+
+ public Date getLicenseDate() {
+ String value = properties.getProperty(KEY_LICENSE_DATE, DEFAULT_LICENSE_DATE);
+ return value == null ? null : new Date(Long.parseLong(value));
+ }
+
+ public void setLicenseDate(Date date) {
+ String value = (date == null ? null : String.valueOf(date.getTime()));
+ setProperty(KEY_LICENSE_DATE, value);
+ }
+
+ public boolean isLicenseValid() {
+ return isLicenseValid(getLicenseEmail(), getLicenseCode()) && licenseValidated;
+ }
+
+ public boolean isLicenseValid(String email, String license) {
+ if (email == null || license == null) {
+ return false;
+ }
+ return license.equalsIgnoreCase(StringUtil.md5Hex(email.toLowerCase()));
+ }
+
+ public String getDownsamplingCommand() {
+ return properties.getProperty(KEY_DOWNSAMPLING_COMMAND, DEFAULT_DOWNSAMPLING_COMMAND);
+ }
+
+ public void setDownsamplingCommand(String command) {
+ setProperty(KEY_DOWNSAMPLING_COMMAND, command);
+ }
+
+ public String getJukeboxCommand() {
+ return properties.getProperty(KEY_JUKEBOX_COMMAND, DEFAULT_JUKEBOX_COMMAND);
+ }
+
+ public boolean isRewriteUrlEnabled() {
+ return getBoolean(KEY_REWRITE_URL, DEFAULT_REWRITE_URL);
+ }
+
+ public void setRewriteUrlEnabled(boolean rewriteUrl) {
+ setBoolean(KEY_REWRITE_URL, rewriteUrl);
+ }
+
+ public boolean isLdapEnabled() {
+ return getBoolean(KEY_LDAP_ENABLED, DEFAULT_LDAP_ENABLED);
+ }
+
+ public void setLdapEnabled(boolean ldapEnabled) {
+ setBoolean(KEY_LDAP_ENABLED, ldapEnabled);
+ }
+
+ public String getLdapUrl() {
+ return properties.getProperty(KEY_LDAP_URL, DEFAULT_LDAP_URL);
+ }
+
+ public void setLdapUrl(String ldapUrl) {
+ properties.setProperty(KEY_LDAP_URL, ldapUrl);
+ }
+
+ public String getLdapSearchFilter() {
+ return properties.getProperty(KEY_LDAP_SEARCH_FILTER, DEFAULT_LDAP_SEARCH_FILTER);
+ }
+
+ public void setLdapSearchFilter(String ldapSearchFilter) {
+ properties.setProperty(KEY_LDAP_SEARCH_FILTER, ldapSearchFilter);
+ }
+
+ public String getLdapManagerDn() {
+ return properties.getProperty(KEY_LDAP_MANAGER_DN, DEFAULT_LDAP_MANAGER_DN);
+ }
+
+ public void setLdapManagerDn(String ldapManagerDn) {
+ properties.setProperty(KEY_LDAP_MANAGER_DN, ldapManagerDn);
+ }
+
+ public String getLdapManagerPassword() {
+ String s = properties.getProperty(KEY_LDAP_MANAGER_PASSWORD, DEFAULT_LDAP_MANAGER_PASSWORD);
+ try {
+ return StringUtil.utf8HexDecode(s);
+ } catch (Exception x) {
+ LOG.warn("Failed to decode LDAP manager password.", x);
+ return s;
+ }
+ }
+
+ public void setLdapManagerPassword(String ldapManagerPassword) {
+ try {
+ ldapManagerPassword = StringUtil.utf8HexEncode(ldapManagerPassword);
+ } catch (Exception x) {
+ LOG.warn("Failed to encode LDAP manager password.", x);
+ }
+ properties.setProperty(KEY_LDAP_MANAGER_PASSWORD, ldapManagerPassword);
+ }
+
+ public boolean isLdapAutoShadowing() {
+ return getBoolean(KEY_LDAP_AUTO_SHADOWING, DEFAULT_LDAP_AUTO_SHADOWING);
+ }
+
+ public void setLdapAutoShadowing(boolean ldapAutoShadowing) {
+ setBoolean(KEY_LDAP_AUTO_SHADOWING, ldapAutoShadowing);
+ }
+
+ public boolean isGettingStartedEnabled() {
+ return getBoolean(KEY_GETTING_STARTED_ENABLED, DEFAULT_GETTING_STARTED_ENABLED);
+ }
+
+ public void setGettingStartedEnabled(boolean isGettingStartedEnabled) {
+ setBoolean(KEY_GETTING_STARTED_ENABLED, isGettingStartedEnabled);
+ }
+
+ public boolean isPortForwardingEnabled() {
+ return getBoolean(KEY_PORT_FORWARDING_ENABLED, DEFAULT_PORT_FORWARDING_ENABLED);
+ }
+
+ public void setPortForwardingEnabled(boolean isPortForwardingEnabled) {
+ setBoolean(KEY_PORT_FORWARDING_ENABLED, isPortForwardingEnabled);
+ }
+
+ public int getPort() {
+ return Integer.valueOf(properties.getProperty(KEY_PORT, String.valueOf(DEFAULT_PORT)));
+ }
+
+ public void setPort(int port) {
+ setProperty(KEY_PORT, String.valueOf(port));
+ }
+
+ public int getHttpsPort() {
+ return Integer.valueOf(properties.getProperty(KEY_HTTPS_PORT, String.valueOf(DEFAULT_HTTPS_PORT)));
+ }
+
+ public void setHttpsPort(int httpsPort) {
+ setProperty(KEY_HTTPS_PORT, String.valueOf(httpsPort));
+ }
+
+ public boolean isUrlRedirectionEnabled() {
+ return getBoolean(KEY_URL_REDIRECTION_ENABLED, DEFAULT_URL_REDIRECTION_ENABLED);
+ }
+
+ public void setUrlRedirectionEnabled(boolean isUrlRedirectionEnabled) {
+ setBoolean(KEY_URL_REDIRECTION_ENABLED, isUrlRedirectionEnabled);
+ }
+
+ public String getUrlRedirectFrom() {
+ return properties.getProperty(KEY_URL_REDIRECT_FROM, DEFAULT_URL_REDIRECT_FROM);
+ }
+
+ public void setUrlRedirectFrom(String urlRedirectFrom) {
+ properties.setProperty(KEY_URL_REDIRECT_FROM, urlRedirectFrom);
+ }
+
+ public Date getUrlRedirectTrialExpires() {
+ String value = properties.getProperty(KEY_URL_REDIRECT_TRIAL_EXPIRES, DEFAULT_URL_REDIRECT_TRIAL_EXPIRES);
+ return value == null ? null : new Date(Long.parseLong(value));
+ }
+
+ public void setUrlRedirectTrialExpires(Date date) {
+ String value = (date == null ? null : String.valueOf(date.getTime()));
+ setProperty(KEY_URL_REDIRECT_TRIAL_EXPIRES, value);
+ }
+
+ public Date getVideoTrialExpires() {
+ String value = properties.getProperty(KEY_VIDEO_TRIAL_EXPIRES, DEFAULT_VIDEO_TRIAL_EXPIRES);
+ return value == null ? null : new Date(Long.parseLong(value));
+ }
+
+ public void setVideoTrialExpires(Date date) {
+ String value = (date == null ? null : String.valueOf(date.getTime()));
+ setProperty(KEY_VIDEO_TRIAL_EXPIRES, value);
+ }
+
+ public String getUrlRedirectContextPath() {
+ return properties.getProperty(KEY_URL_REDIRECT_CONTEXT_PATH, DEFAULT_URL_REDIRECT_CONTEXT_PATH);
+ }
+
+ public void setUrlRedirectContextPath(String contextPath) {
+ properties.setProperty(KEY_URL_REDIRECT_CONTEXT_PATH, contextPath);
+ }
+
+ public Date getRESTTrialExpires(String client) {
+ String value = properties.getProperty(KEY_REST_TRIAL_EXPIRES + client, DEFAULT_REST_TRIAL_EXPIRES);
+ return value == null ? null : new Date(Long.parseLong(value));
+ }
+
+ public void setRESTTrialExpires(String client, Date date) {
+ String value = (date == null ? null : String.valueOf(date.getTime()));
+ setProperty(KEY_REST_TRIAL_EXPIRES + client, value);
+ }
+
+ public String getServerId() {
+ return properties.getProperty(KEY_SERVER_ID, DEFAULT_SERVER_ID);
+ }
+
+ public void setServerId(String serverId) {
+ properties.setProperty(KEY_SERVER_ID, serverId);
+ }
+
+ public long getSettingsChanged() {
+ return Long.parseLong(properties.getProperty(KEY_SETTINGS_CHANGED, String.valueOf(DEFAULT_SETTINGS_CHANGED)));
+ }
+
+ public Date getLastScanned() {
+ String lastScanned = properties.getProperty(KEY_LAST_SCANNED);
+ return lastScanned == null ? null : new Date(Long.parseLong(lastScanned));
+ }
+
+ public void setLastScanned(Date date) {
+ if (date == null) {
+ properties.remove(KEY_LAST_SCANNED);
+ } else {
+ properties.setProperty(KEY_LAST_SCANNED, String.valueOf(date.getTime()));
+ }
+ }
+
+ public boolean isOrganizeByFolderStructure() {
+ return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE);
+ }
+
+ public void setOrganizeByFolderStructure(boolean b) {
+ setBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, b);
+ }
+
+ public boolean isSortAlbumsByYear() {
+ return getBoolean(KEY_SORT_ALBUMS_BY_YEAR, DEFAULT_SORT_ALBUMS_BY_YEAR);
+ }
+
+ public void setSortAlbumsByYear(boolean b) {
+ setBoolean(KEY_SORT_ALBUMS_BY_YEAR, b);
+ }
+
+ /**
+ * Returns the locale (for language, date format etc).
+ *
+ * @return The locale.
+ */
+ public Locale getLocale() {
+ String language = properties.getProperty(KEY_LOCALE_LANGUAGE, DEFAULT_LOCALE_LANGUAGE);
+ String country = properties.getProperty(KEY_LOCALE_COUNTRY, DEFAULT_LOCALE_COUNTRY);
+ String variant = properties.getProperty(KEY_LOCALE_VARIANT, DEFAULT_LOCALE_VARIANT);
+
+ return new Locale(language, country, variant);
+ }
+
+ /**
+ * Sets the locale (for language, date format etc.)
+ *
+ * @param locale The locale.
+ */
+ public void setLocale(Locale locale) {
+ setProperty(KEY_LOCALE_LANGUAGE, locale.getLanguage());
+ setProperty(KEY_LOCALE_COUNTRY, locale.getCountry());
+ setProperty(KEY_LOCALE_VARIANT, locale.getVariant());
+ }
+
+ /**
+ * Returns the ID of the theme to use.
+ *
+ * @return The theme ID.
+ */
+ public String getThemeId() {
+ return properties.getProperty(KEY_THEME_ID, DEFAULT_THEME_ID);
+ }
+
+ /**
+ * Sets the ID of the theme to use.
+ *
+ * @param themeId The theme ID
+ */
+ public void setThemeId(String themeId) {
+ setProperty(KEY_THEME_ID, themeId);
+ }
+
+ /**
+ * Returns a list of available themes.
+ *
+ * @return A list of available themes.
+ */
+ public synchronized Theme[] getAvailableThemes() {
+ if (themes == null) {
+ themes = new ArrayList<Theme>();
+ try {
+ InputStream in = SettingsService.class.getResourceAsStream(THEMES_FILE);
+ String[] lines = StringUtil.readLines(in);
+ for (String line : lines) {
+ String[] elements = StringUtil.split(line);
+ if (elements.length == 2) {
+ themes.add(new Theme(elements[0], elements[1]));
+ } else {
+ LOG.warn("Failed to parse theme from line: [" + line + "].");
+ }
+ }
+ } catch (IOException x) {
+ LOG.error("Failed to resolve list of themes.", x);
+ themes.add(new Theme("default", "Subsonic default"));
+ }
+ }
+ return themes.toArray(new Theme[themes.size()]);
+ }
+
+ /**
+ * Returns a list of available locales.
+ *
+ * @return A list of available locales.
+ */
+ public synchronized Locale[] getAvailableLocales() {
+ if (locales == null) {
+ locales = new ArrayList<Locale>();
+ try {
+ InputStream in = SettingsService.class.getResourceAsStream(LOCALES_FILE);
+ String[] lines = StringUtil.readLines(in);
+
+ for (String line : lines) {
+ locales.add(parseLocale(line));
+ }
+
+ } catch (IOException x) {
+ LOG.error("Failed to resolve list of locales.", x);
+ locales.add(Locale.ENGLISH);
+ }
+ }
+ return locales.toArray(new Locale[locales.size()]);
+ }
+
+ private Locale parseLocale(String line) {
+ String[] s = line.split("_");
+ String language = s[0];
+ String country = "";
+ String variant = "";
+
+ if (s.length > 1) {
+ country = s[1];
+ }
+ if (s.length > 2) {
+ variant = s[2];
+ }
+ return new Locale(language, country, variant);
+ }
+
+ /**
+ * Returns the "brand" name. Normally, this is just "Subsonic".
+ *
+ * @return The brand name.
+ */
+ public String getBrand() {
+ return "Subsonic";
+ }
+
+ /**
+ * Returns all music folders. Non-existing and disabled folders are not included.
+ *
+ * @return Possibly empty list of all music folders.
+ */
+ public List<MusicFolder> getAllMusicFolders() {
+ return getAllMusicFolders(false, false);
+ }
+
+ /**
+ * Returns all music folders.
+ *
+ * @param includeDisabled Whether to include disabled folders.
+ * @param includeNonExisting Whether to include non-existing folders.
+ * @return Possibly empty list of all music folders.
+ */
+ public List<MusicFolder> getAllMusicFolders(boolean includeDisabled, boolean includeNonExisting) {
+ if (cachedMusicFolders == null) {
+ cachedMusicFolders = musicFolderDao.getAllMusicFolders();
+ }
+
+ List<MusicFolder> result = new ArrayList<MusicFolder>(cachedMusicFolders.size());
+ for (MusicFolder folder : cachedMusicFolders) {
+ if ((includeDisabled || folder.isEnabled()) && (includeNonExisting || FileUtil.exists(folder.getPath()))) {
+ result.add(folder);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the music folder with the given ID.
+ *
+ * @param id The ID.
+ * @return The music folder with the given ID, or <code>null</code> if not found.
+ */
+ public MusicFolder getMusicFolderById(Integer id) {
+ List<MusicFolder> all = getAllMusicFolders();
+ for (MusicFolder folder : all) {
+ if (id.equals(folder.getId())) {
+ return folder;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new music folder.
+ *
+ * @param musicFolder The music folder to create.
+ */
+ public void createMusicFolder(MusicFolder musicFolder) {
+ musicFolderDao.createMusicFolder(musicFolder);
+ cachedMusicFolders = null;
+ }
+
+ /**
+ * Deletes the music folder with the given ID.
+ *
+ * @param id The ID of the music folder to delete.
+ */
+ public void deleteMusicFolder(Integer id) {
+ musicFolderDao.deleteMusicFolder(id);
+ cachedMusicFolders = null;
+ }
+
+ /**
+ * Updates the given music folder.
+ *
+ * @param musicFolder The music folder to update.
+ */
+ public void updateMusicFolder(MusicFolder musicFolder) {
+ musicFolderDao.updateMusicFolder(musicFolder);
+ cachedMusicFolders = null;
+ }
+
+ /**
+ * Returns all internet radio stations. Disabled stations are not returned.
+ *
+ * @return Possibly empty list of all internet radio stations.
+ */
+ public List<InternetRadio> getAllInternetRadios() {
+ return getAllInternetRadios(false);
+ }
+
+ /**
+ * Returns the internet radio station with the given ID.
+ *
+ * @param id The ID.
+ * @return The internet radio station with the given ID, or <code>null</code> if not found.
+ */
+ public InternetRadio getInternetRadioById(Integer id) {
+ for (InternetRadio radio : getAllInternetRadios()) {
+ if (id.equals(radio.getId())) {
+ return radio;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all internet radio stations.
+ *
+ * @param includeAll Whether disabled stations should be included.
+ * @return Possibly empty list of all internet radio stations.
+ */
+ public List<InternetRadio> getAllInternetRadios(boolean includeAll) {
+ List<InternetRadio> all = internetRadioDao.getAllInternetRadios();
+ List<InternetRadio> result = new ArrayList<InternetRadio>(all.size());
+ for (InternetRadio folder : all) {
+ if (includeAll || folder.isEnabled()) {
+ result.add(folder);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new internet radio station.
+ *
+ * @param radio The internet radio station to create.
+ */
+ public void createInternetRadio(InternetRadio radio) {
+ internetRadioDao.createInternetRadio(radio);
+ }
+
+ /**
+ * Deletes the internet radio station with the given ID.
+ *
+ * @param id The internet radio station ID.
+ */
+ public void deleteInternetRadio(Integer id) {
+ internetRadioDao.deleteInternetRadio(id);
+ }
+
+ /**
+ * Updates the given internet radio station.
+ *
+ * @param radio The internet radio station to update.
+ */
+ public void updateInternetRadio(InternetRadio radio) {
+ internetRadioDao.updateInternetRadio(radio);
+ }
+
+ /**
+ * Returns settings for the given user.
+ *
+ * @param username The username.
+ * @return User-specific settings. Never <code>null</code>.
+ */
+ public UserSettings getUserSettings(String username) {
+ UserSettings settings = userDao.getUserSettings(username);
+ return settings == null ? createDefaultUserSettings(username) : settings;
+ }
+
+ private UserSettings createDefaultUserSettings(String username) {
+ UserSettings settings = new UserSettings(username);
+ settings.setFinalVersionNotificationEnabled(true);
+ settings.setBetaVersionNotificationEnabled(false);
+ settings.setShowNowPlayingEnabled(true);
+ settings.setShowChatEnabled(true);
+ settings.setPartyModeEnabled(false);
+ settings.setNowPlayingAllowed(true);
+ settings.setLastFmEnabled(false);
+ settings.setLastFmUsername(null);
+ settings.setLastFmPassword(null);
+ settings.setChanged(new Date());
+
+ UserSettings.Visibility playlist = settings.getPlaylistVisibility();
+ playlist.setCaptionCutoff(35);
+ playlist.setArtistVisible(true);
+ playlist.setAlbumVisible(true);
+ playlist.setYearVisible(true);
+ playlist.setDurationVisible(true);
+ playlist.setBitRateVisible(true);
+ playlist.setFormatVisible(true);
+ playlist.setFileSizeVisible(true);
+
+ UserSettings.Visibility main = settings.getMainVisibility();
+ main.setCaptionCutoff(35);
+ main.setTrackNumberVisible(true);
+ main.setArtistVisible(true);
+ main.setDurationVisible(true);
+
+ return settings;
+ }
+
+ /**
+ * Updates settings for the given username.
+ *
+ * @param settings The user-specific settings.
+ */
+ public void updateUserSettings(UserSettings settings) {
+ userDao.updateUserSettings(settings);
+ }
+
+ /**
+ * Returns all system avatars.
+ *
+ * @return All system avatars.
+ */
+ public List<Avatar> getAllSystemAvatars() {
+ return avatarDao.getAllSystemAvatars();
+ }
+
+ /**
+ * Returns the system avatar with the given ID.
+ *
+ * @param id The system avatar ID.
+ * @return The avatar or <code>null</code> if not found.
+ */
+ public Avatar getSystemAvatar(int id) {
+ return avatarDao.getSystemAvatar(id);
+ }
+
+ /**
+ * Returns the custom avatar for the given user.
+ *
+ * @param username The username.
+ * @return The avatar or <code>null</code> if not found.
+ */
+ public Avatar getCustomAvatar(String username) {
+ return avatarDao.getCustomAvatar(username);
+ }
+
+ /**
+ * Sets the custom avatar for the given user.
+ *
+ * @param avatar The avatar, or <code>null</code> to remove the avatar.
+ * @param username The username.
+ */
+ public void setCustomAvatar(Avatar avatar, String username) {
+ avatarDao.setCustomAvatar(avatar, username);
+ }
+
+ private void setProperty(String key, String value) {
+ if (value == null) {
+ properties.remove(key);
+ } else {
+ properties.setProperty(key, value);
+ }
+ }
+
+ private String[] toStringArray(String s) {
+ List<String> result = new ArrayList<String>();
+ StringTokenizer tokenizer = new StringTokenizer(s, " ");
+ while (tokenizer.hasMoreTokens()) {
+ result.add(tokenizer.nextToken());
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ private void validateLicense() {
+ String email = getLicenseEmail();
+ Date date = getLicenseDate();
+
+ if (email == null || date == null) {
+ licenseValidated = false;
+ return;
+ }
+
+ licenseValidated = true;
+
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 120000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 120000);
+ HttpGet method = new HttpGet("http://subsonic.org/backend/validateLicense.view" + "?email=" + StringUtil.urlEncode(email) +
+ "&date=" + date.getTime() + "&version=" + versionService.getLocalVersion());
+ try {
+ ResponseHandler<String> responseHandler = new BasicResponseHandler();
+ String content = client.execute(method, responseHandler);
+ licenseValidated = content != null && content.contains("true");
+ if (!licenseValidated) {
+ LOG.warn("License key is not valid.");
+ }
+ } catch (Throwable x) {
+ LOG.warn("Failed to validate license.", x);
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+
+ public void validateLicenseAsync() {
+ new Thread() {
+ @Override
+ public void run() {
+ validateLicense();
+ }
+ }.start();
+ }
+
+ public void setInternetRadioDao(InternetRadioDao internetRadioDao) {
+ this.internetRadioDao = internetRadioDao;
+ }
+
+ public void setMusicFolderDao(MusicFolderDao musicFolderDao) {
+ this.musicFolderDao = musicFolderDao;
+ }
+
+ public void setUserDao(UserDao userDao) {
+ this.userDao = userDao;
+ }
+
+ public void setAvatarDao(AvatarDao avatarDao) {
+ this.avatarDao = avatarDao;
+ }
+
+ public void setVersionService(VersionService versionService) {
+ this.versionService = versionService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java
new file mode 100644
index 00000000..cf5860e6
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java
@@ -0,0 +1,133 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import org.apache.commons.lang.ObjectUtils;
+import org.apache.commons.lang.RandomStringUtils;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.dao.ShareDao;
+import net.sourceforge.subsonic.domain.Share;
+import net.sourceforge.subsonic.domain.User;
+
+/**
+ * Provides services for sharing media.
+ *
+ * @author Sindre Mehus
+ * @see Share
+ */
+public class ShareService {
+
+ private static final Logger LOG = Logger.getLogger(ShareService.class);
+
+ private ShareDao shareDao;
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private MediaFileService mediaFileService;
+
+ public List<Share> getAllShares() {
+ return shareDao.getAllShares();
+ }
+
+ public List<Share> getSharesForUser(User user) {
+ List<Share> result = new ArrayList<Share>();
+ for (Share share : getAllShares()) {
+ if (user.isAdminRole() || ObjectUtils.equals(user.getUsername(), share.getUsername())) {
+ result.add(share);
+ }
+ }
+ return result;
+ }
+
+ public Share getShareById(int id) {
+ return shareDao.getShareById(id);
+ }
+
+ public List<MediaFile> getSharedFiles(int id) {
+ List<MediaFile> result = new ArrayList<MediaFile>();
+ for (String path : shareDao.getSharedFiles(id)) {
+ try {
+ result.add(mediaFileService.getMediaFile(path));
+ } catch (Exception x) {
+ // Ignored
+ }
+ }
+ return result;
+ }
+
+ public Share createShare(HttpServletRequest request, List<MediaFile> files) throws Exception {
+
+ Share share = new Share();
+ share.setName(RandomStringUtils.random(5, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ share.setCreated(new Date());
+ share.setUsername(securityService.getCurrentUsername(request));
+
+ Calendar expires = Calendar.getInstance();
+ expires.add(Calendar.YEAR, 1);
+ share.setExpires(expires.getTime());
+
+ shareDao.createShare(share);
+ for (MediaFile file : files) {
+ shareDao.createSharedFiles(share.getId(), file.getPath());
+ }
+ LOG.info("Created share '" + share.getName() + "' with " + files.size() + " file(s).");
+
+ return share;
+ }
+
+ public void updateShare(Share share) {
+ shareDao.updateShare(share);
+ }
+
+ public void deleteShare(int id) {
+ shareDao.deleteShare(id);
+ }
+
+ public String getShareBaseUrl() {
+ return "http://" + settingsService.getUrlRedirectFrom() + ".subsonic.org/share/";
+ }
+
+ public String getShareUrl(Share share) {
+ return getShareBaseUrl() + share.getName();
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setShareDao(ShareDao shareDao) {
+ this.shareDao = shareDao;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setMediaFileService(MediaFileService mediaFileService) {
+ this.mediaFileService = mediaFileService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.java
new file mode 100644
index 00000000..a893166a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.java
@@ -0,0 +1,134 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TransferStatus;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides services for maintaining the list of stream, download and upload statuses.
+ * <p/>
+ * Note that for stream statuses, the last inactive status is also stored.
+ *
+ * @author Sindre Mehus
+ * @see TransferStatus
+ */
+public class StatusService {
+
+ private final List<TransferStatus> streamStatuses = new ArrayList<TransferStatus>();
+ private final List<TransferStatus> downloadStatuses = new ArrayList<TransferStatus>();
+ private final List<TransferStatus> uploadStatuses = new ArrayList<TransferStatus>();
+
+ // Maps from player ID to latest inactive stream status.
+ private final Map<String, TransferStatus> inactiveStreamStatuses = new LinkedHashMap<String, TransferStatus>();
+
+ public synchronized TransferStatus createStreamStatus(Player player) {
+ // Reuse existing status, if possible.
+ TransferStatus status = inactiveStreamStatuses.get(player.getId());
+ if (status != null) {
+ status.setActive(true);
+ } else {
+ status = createStatus(player, streamStatuses);
+ }
+ return status;
+ }
+
+ public synchronized void removeStreamStatus(TransferStatus status) {
+ // Move it to the map of inactive statuses.
+ status.setActive(false);
+ inactiveStreamStatuses.put(status.getPlayer().getId(), status);
+ streamStatuses.remove(status);
+ }
+
+ public synchronized List<TransferStatus> getAllStreamStatuses() {
+
+ List<TransferStatus> result = new ArrayList<TransferStatus>(streamStatuses);
+
+ // Add inactive status for those players that have no active status.
+ Set<String> activePlayers = new HashSet<String>();
+ for (TransferStatus status : streamStatuses) {
+ activePlayers.add(status.getPlayer().getId());
+ }
+
+ for (Map.Entry<String, TransferStatus> entry : inactiveStreamStatuses.entrySet()) {
+ if (!activePlayers.contains(entry.getKey())) {
+ result.add(entry.getValue());
+ }
+ }
+ return result;
+ }
+
+ public synchronized List<TransferStatus> getStreamStatusesForPlayer(Player player) {
+ List<TransferStatus> result = new ArrayList<TransferStatus>();
+ for (TransferStatus status : streamStatuses) {
+ if (status.getPlayer().getId().equals(player.getId())) {
+ result.add(status);
+ }
+ }
+
+ // If no active statuses exists, add the inactive one.
+ if (result.isEmpty()) {
+ TransferStatus inactiveStatus = inactiveStreamStatuses.get(player.getId());
+ if (inactiveStatus != null) {
+ result.add(inactiveStatus);
+ }
+ }
+
+ return result;
+ }
+
+ public synchronized TransferStatus createDownloadStatus(Player player) {
+ return createStatus(player, downloadStatuses);
+ }
+
+ public synchronized void removeDownloadStatus(TransferStatus status) {
+ downloadStatuses.remove(status);
+ }
+
+ public synchronized List<TransferStatus> getAllDownloadStatuses() {
+ return new ArrayList<TransferStatus>(downloadStatuses);
+ }
+
+ public synchronized TransferStatus createUploadStatus(Player player) {
+ return createStatus(player, uploadStatuses);
+ }
+
+ public synchronized void removeUploadStatus(TransferStatus status) {
+ uploadStatuses.remove(status);
+ }
+
+ public synchronized List<TransferStatus> getAllUploadStatuses() {
+ return new ArrayList<TransferStatus>(uploadStatuses);
+ }
+
+ private synchronized TransferStatus createStatus(Player player, List<TransferStatus> statusList) {
+ TransferStatus status = new TransferStatus();
+ status.setPlayer(player);
+ statusList.add(status);
+ return status;
+ }
+
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java
new file mode 100644
index 00000000..2c8b9c5e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java
@@ -0,0 +1,530 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.controller.VideoPlayerController;
+import net.sourceforge.subsonic.dao.TranscodingDao;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.Player;
+import net.sourceforge.subsonic.domain.TranscodeScheme;
+import net.sourceforge.subsonic.domain.Transcoding;
+import net.sourceforge.subsonic.domain.UserSettings;
+import net.sourceforge.subsonic.domain.VideoTranscodingSettings;
+import net.sourceforge.subsonic.io.TranscodeInputStream;
+import net.sourceforge.subsonic.util.StringUtil;
+import net.sourceforge.subsonic.util.Util;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.filefilter.PrefixFileFilter;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Provides services for transcoding media. Transcoding is the process of
+ * converting an audio stream to a different format and/or bit rate. The latter is
+ * also called downsampling.
+ *
+ * @author Sindre Mehus
+ * @see TranscodeInputStream
+ */
+public class TranscodingService {
+
+ private static final Logger LOG = Logger.getLogger(TranscodingService.class);
+
+ private TranscodingDao transcodingDao;
+ private SettingsService settingsService;
+ private PlayerService playerService;
+
+ /**
+ * Returns all transcodings.
+ *
+ * @return Possibly empty list of all transcodings.
+ */
+ public List<Transcoding> getAllTranscodings() {
+ return transcodingDao.getAllTranscodings();
+ }
+
+ /**
+ * Returns all active transcodings for the given player. Only enabled transcodings are returned.
+ *
+ * @param player The player.
+ * @return All active transcodings for the player.
+ */
+ public List<Transcoding> getTranscodingsForPlayer(Player player) {
+ return transcodingDao.getTranscodingsForPlayer(player.getId());
+ }
+
+ /**
+ * Sets the list of active transcodings for the given player.
+ *
+ * @param player The player.
+ * @param transcodingIds ID's of the active transcodings.
+ */
+ public void setTranscodingsForPlayer(Player player, int[] transcodingIds) {
+ transcodingDao.setTranscodingsForPlayer(player.getId(), transcodingIds);
+ }
+
+ /**
+ * Sets the list of active transcodings for the given player.
+ *
+ * @param player The player.
+ * @param transcodings The active transcodings.
+ */
+ public void setTranscodingsForPlayer(Player player, List<Transcoding> transcodings) {
+ int[] transcodingIds = new int[transcodings.size()];
+ for (int i = 0; i < transcodingIds.length; i++) {
+ transcodingIds[i] = transcodings.get(i).getId();
+ }
+ setTranscodingsForPlayer(player, transcodingIds);
+ }
+
+
+ /**
+ * Creates a new transcoding.
+ *
+ * @param transcoding The transcoding to create.
+ */
+ public void createTranscoding(Transcoding transcoding) {
+ transcodingDao.createTranscoding(transcoding);
+
+ // Activate this transcoding for all players?
+ if (transcoding.isDefaultActive()) {
+ for (Player player : playerService.getAllPlayers()) {
+ List<Transcoding> transcodings = getTranscodingsForPlayer(player);
+ transcodings.add(transcoding);
+ setTranscodingsForPlayer(player, transcodings);
+ }
+ }
+ }
+
+ /**
+ * Deletes the transcoding with the given ID.
+ *
+ * @param id The transcoding ID.
+ */
+ public void deleteTranscoding(Integer id) {
+ transcodingDao.deleteTranscoding(id);
+ }
+
+ /**
+ * Updates the given transcoding.
+ *
+ * @param transcoding The transcoding to update.
+ */
+ public void updateTranscoding(Transcoding transcoding) {
+ transcodingDao.updateTranscoding(transcoding);
+ }
+
+ /**
+ * Returns whether transcoding is required for the given media file and player combination.
+ *
+ * @param mediaFile The media file.
+ * @param player The player.
+ * @return Whether transcoding will be performed if invoking the
+ * {@link #getTranscodedInputStream} method with the same arguments.
+ */
+ public boolean isTranscodingRequired(MediaFile mediaFile, Player player) {
+ return getTranscoding(mediaFile, player, null) != null;
+ }
+
+ /**
+ * Returns the suffix for the given player and media file, taking transcodings into account.
+ *
+ * @param player The player in question.
+ * @param file The media file.
+ * @param preferredTargetFormat Used to select among multiple applicable transcodings. May be {@code null}.
+ * @return The file suffix, e.g., "mp3".
+ */
+ public String getSuffix(Player player, MediaFile file, String preferredTargetFormat) {
+ Transcoding transcoding = getTranscoding(file, player, preferredTargetFormat);
+ return transcoding != null ? transcoding.getTargetFormat() : file.getFormat();
+ }
+
+ /**
+ * Creates parameters for a possibly transcoded or downsampled input stream for the given media file and player combination.
+ * <p/>
+ * A transcoding is applied if it is applicable for the format of the given file, and is activated for the
+ * given player.
+ * <p/>
+ * If no transcoding is applicable, the file may still be downsampled, given that the player is configured
+ * with a bit rate limit which is higher than the actual bit rate of the file.
+ * <p/>
+ * Otherwise, a normal input stream to the original file is returned.
+ *
+ * @param mediaFile The media file.
+ * @param player The player.
+ * @param maxBitRate Overrides the per-player and per-user bitrate limit. May be {@code null}.
+ * @param preferredTargetFormat Used to select among multiple applicable transcodings. May be {@code null}.
+ * @param videoTranscodingSettings Parameters used when transcoding video. May be {@code null}.
+ * @return Parameters to be used in the {@link #getTranscodedInputStream} method.
+ */
+ public Parameters getParameters(MediaFile mediaFile, Player player, Integer maxBitRate, String preferredTargetFormat,
+ VideoTranscodingSettings videoTranscodingSettings) {
+
+ Parameters parameters = new Parameters(mediaFile, videoTranscodingSettings);
+
+ TranscodeScheme transcodeScheme = getTranscodeScheme(player);
+ if (maxBitRate == null && transcodeScheme != TranscodeScheme.OFF) {
+ maxBitRate = transcodeScheme.getMaxBitRate();
+ }
+
+ Transcoding transcoding = getTranscoding(mediaFile, player, preferredTargetFormat);
+ if (transcoding != null) {
+ parameters.setTranscoding(transcoding);
+ if (maxBitRate == null) {
+ maxBitRate = mediaFile.isVideo() ? VideoPlayerController.DEFAULT_BIT_RATE : 128;
+ }
+ } else if (maxBitRate != null) {
+ boolean supported = isDownsamplingSupported(mediaFile);
+ Integer bitRate = mediaFile.getBitRate();
+ if (supported && bitRate != null && bitRate > maxBitRate) {
+ parameters.setDownsample(true);
+ }
+ }
+
+ parameters.setMaxBitRate(maxBitRate);
+ return parameters;
+ }
+
+ /**
+ * Returns a possibly transcoded or downsampled input stream for the given music file and player combination.
+ * <p/>
+ * A transcoding is applied if it is applicable for the format of the given file, and is activated for the
+ * given player.
+ * <p/>
+ * If no transcoding is applicable, the file may still be downsampled, given that the player is configured
+ * with a bit rate limit which is higher than the actual bit rate of the file.
+ * <p/>
+ * Otherwise, a normal input stream to the original file is returned.
+ *
+ * @param parameters As returned by {@link #getParameters}.
+ * @return A possible transcoded or downsampled input stream.
+ * @throws IOException If an I/O error occurs.
+ */
+ public InputStream getTranscodedInputStream(Parameters parameters) throws IOException {
+ try {
+
+ if (parameters.getTranscoding() != null) {
+ return createTranscodedInputStream(parameters);
+ }
+
+ if (parameters.downsample) {
+ return createDownsampledInputStream(parameters);
+ }
+
+ } catch (Exception x) {
+ LOG.warn("Failed to transcode " + parameters.getMediaFile() + ". Using original.", x);
+ }
+
+ return new FileInputStream(parameters.getMediaFile().getFile());
+ }
+
+
+ /**
+ * Returns the strictest transcoding scheme defined for the player and the user.
+ */
+ private TranscodeScheme getTranscodeScheme(Player player) {
+ String username = player.getUsername();
+ if (username != null) {
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ return player.getTranscodeScheme().strictest(userSettings.getTranscodeScheme());
+ }
+
+ return player.getTranscodeScheme();
+ }
+
+ /**
+ * Returns an input stream by applying the given transcoding to the given music file.
+ *
+ * @param parameters Transcoding parameters.
+ * @return The transcoded input stream.
+ * @throws IOException If an I/O error occurs.
+ */
+ private InputStream createTranscodedInputStream(Parameters parameters)
+ throws IOException {
+
+ Transcoding transcoding = parameters.getTranscoding();
+ Integer maxBitRate = parameters.getMaxBitRate();
+ VideoTranscodingSettings videoTranscodingSettings = parameters.getVideoTranscodingSettings();
+ MediaFile mediaFile = parameters.getMediaFile();
+
+ TranscodeInputStream in = createTranscodeInputStream(transcoding.getStep1(), maxBitRate, videoTranscodingSettings, mediaFile, null);
+
+ if (transcoding.getStep2() != null) {
+ in = createTranscodeInputStream(transcoding.getStep2(), maxBitRate, videoTranscodingSettings, mediaFile, in);
+ }
+
+ if (transcoding.getStep3() != null) {
+ in = createTranscodeInputStream(transcoding.getStep3(), maxBitRate, videoTranscodingSettings, mediaFile, in);
+ }
+
+ return in;
+ }
+
+ /**
+ * Creates a transcoded input stream by interpreting the given command line string.
+ * This includes the following:
+ * <ul>
+ * <li>Splitting the command line string to an array.</li>
+ * <li>Replacing occurrences of "%s" with the path of the given music file.</li>
+ * <li>Replacing occurrences of "%t" with the title of the given music file.</li>
+ * <li>Replacing occurrences of "%l" with the album name of the given music file.</li>
+ * <li>Replacing occurrences of "%a" with the artist name of the given music file.</li>
+ * <li>Replacing occurrcences of "%b" with the max bitrate.</li>
+ * <li>Replacing occurrcences of "%o" with the video time offset (used for scrubbing).</li>
+ * <li>Replacing occurrcences of "%w" with the video image width.</li>
+ * <li>Replacing occurrcences of "%h" with the video image height.</li>
+ * <li>Prepending the path of the transcoder directory if the transcoder is found there.</li>
+ * </ul>
+ *
+ * @param command The command line string.
+ * @param maxBitRate The maximum bitrate to use. May not be {@code null}.
+ * @param videoTranscodingSettings Parameters used when transcoding video. May be {@code null}.
+ * @param mediaFile The media file.
+ * @param in Data to feed to the process. May be {@code null}. @return The newly created input stream.
+ */
+ private TranscodeInputStream createTranscodeInputStream(String command, Integer maxBitRate,
+ VideoTranscodingSettings videoTranscodingSettings, MediaFile mediaFile, InputStream in) throws IOException {
+
+ String title = mediaFile.getTitle();
+ String album = mediaFile.getAlbumName();
+ String artist = mediaFile.getArtist();
+
+ if (title == null) {
+ title = "Unknown Song";
+ }
+ if (album == null) {
+ title = "Unknown Album";
+ }
+ if (artist == null) {
+ title = "Unknown Artist";
+ }
+
+ List<String> result = new LinkedList<String>(Arrays.asList(StringUtil.split(command)));
+ result.set(0, getTranscodeDirectory().getPath() + File.separatorChar + result.get(0));
+
+ File tmpFile = null;
+
+ for (int i = 1; i < result.size(); i++) {
+ String cmd = result.get(i);
+ if (cmd.contains("%b")) {
+ cmd = cmd.replace("%b", String.valueOf(maxBitRate));
+ }
+ if (cmd.contains("%t")) {
+ cmd = cmd.replace("%t", title);
+ }
+ if (cmd.contains("%l")) {
+ cmd = cmd.replace("%l", album);
+ }
+ if (cmd.contains("%a")) {
+ cmd = cmd.replace("%a", artist);
+ }
+ if (cmd.contains("%o") && videoTranscodingSettings != null) {
+ cmd = cmd.replace("%o", String.valueOf(videoTranscodingSettings.getTimeOffset()));
+ }
+ if (cmd.contains("%w") && videoTranscodingSettings != null) {
+ cmd = cmd.replace("%w", String.valueOf(videoTranscodingSettings.getWidth()));
+ }
+ if (cmd.contains("%h") && videoTranscodingSettings != null) {
+ cmd = cmd.replace("%h", String.valueOf(videoTranscodingSettings.getHeight()));
+ }
+ if (cmd.contains("%s")) {
+
+ // Work-around for filename character encoding problem on Windows.
+ // Create temporary file, and feed this to the transcoder.
+ String path = mediaFile.getFile().getAbsolutePath();
+ if (Util.isWindows() && !mediaFile.isVideo() && !StringUtils.isAsciiPrintable(path)) {
+ tmpFile = File.createTempFile("subsonic", "." + FilenameUtils.getExtension(path));
+ tmpFile.deleteOnExit();
+ FileUtils.copyFile(new File(path), tmpFile);
+ LOG.debug("Created tmp file: " + tmpFile);
+ cmd = cmd.replace("%s", tmpFile.getPath());
+ } else {
+ cmd = cmd.replace("%s", path);
+ }
+ }
+
+ result.set(i, cmd);
+ }
+ return new TranscodeInputStream(new ProcessBuilder(result), in, tmpFile);
+ }
+
+ /**
+ * Returns an applicable transcoding for the given file and player, or <code>null</code> if no
+ * transcoding should be done.
+ */
+ private Transcoding getTranscoding(MediaFile mediaFile, Player player, String preferredTargetFormat) {
+
+ List<Transcoding> applicableTranscodings = new LinkedList<Transcoding>();
+ String suffix = mediaFile.getFormat();
+
+ for (Transcoding transcoding : getTranscodingsForPlayer(player)) {
+ for (String sourceFormat : transcoding.getSourceFormatsAsArray()) {
+ if (sourceFormat.equalsIgnoreCase(suffix)) {
+ if (isTranscodingInstalled(transcoding)) {
+ applicableTranscodings.add(transcoding);
+ }
+ }
+ }
+ }
+
+ if (applicableTranscodings.isEmpty()) {
+ return null;
+ }
+
+ for (Transcoding transcoding : applicableTranscodings) {
+ if (transcoding.getTargetFormat().equalsIgnoreCase(preferredTargetFormat)) {
+ return transcoding;
+ }
+ }
+
+ return applicableTranscodings.get(0);
+ }
+
+ /**
+ * Returns a downsampled input stream to the music file.
+ *
+ * @param parameters Downsample parameters.
+ * @throws IOException If an I/O error occurs.
+ */
+ private InputStream createDownsampledInputStream(Parameters parameters) throws IOException {
+ String command = settingsService.getDownsamplingCommand();
+ return createTranscodeInputStream(command, parameters.getMaxBitRate(), parameters.getVideoTranscodingSettings(),
+ parameters.getMediaFile(), null);
+ }
+
+ /**
+ * Returns whether downsampling is supported (i.e., whether LAME is installed or not.)
+ *
+ * @param mediaFile If not null, returns whether downsampling is supported for this file.
+ * @return Whether downsampling is supported.
+ */
+ public boolean isDownsamplingSupported(MediaFile mediaFile) {
+ if (mediaFile != null) {
+ boolean isMp3 = "mp3".equalsIgnoreCase(mediaFile.getFormat());
+ if (!isMp3) {
+ return false;
+ }
+ }
+
+ String commandLine = settingsService.getDownsamplingCommand();
+ return isTranscodingStepInstalled(commandLine);
+ }
+
+ private boolean isTranscodingInstalled(Transcoding transcoding) {
+ return isTranscodingStepInstalled(transcoding.getStep1()) &&
+ isTranscodingStepInstalled(transcoding.getStep2()) &&
+ isTranscodingStepInstalled(transcoding.getStep3());
+ }
+
+ private boolean isTranscodingStepInstalled(String step) {
+ if (StringUtils.isEmpty(step)) {
+ return true;
+ }
+ String executable = StringUtil.split(step)[0];
+ PrefixFileFilter filter = new PrefixFileFilter(executable);
+ String[] matches = getTranscodeDirectory().list(filter);
+ return matches != null && matches.length > 0;
+ }
+
+ /**
+ * Returns the directory in which all transcoders are installed.
+ */
+ public File getTranscodeDirectory() {
+ File dir = new File(SettingsService.getSubsonicHome(), "transcode");
+ if (!dir.exists()) {
+ boolean ok = dir.mkdir();
+ if (ok) {
+ LOG.info("Created directory " + dir);
+ } else {
+ LOG.warn("Failed to create directory " + dir);
+ }
+ }
+ return dir;
+ }
+
+ public void setTranscodingDao(TranscodingDao transcodingDao) {
+ this.transcodingDao = transcodingDao;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+
+ public void setPlayerService(PlayerService playerService) {
+ this.playerService = playerService;
+ }
+
+ public static class Parameters {
+ private boolean downsample;
+ private final MediaFile mediaFile;
+ private final VideoTranscodingSettings videoTranscodingSettings;
+ private Integer maxBitRate;
+ private Transcoding transcoding;
+
+ public Parameters(MediaFile mediaFile, VideoTranscodingSettings videoTranscodingSettings) {
+ this.mediaFile = mediaFile;
+ this.videoTranscodingSettings = videoTranscodingSettings;
+ }
+
+ public void setMaxBitRate(Integer maxBitRate) {
+ this.maxBitRate = maxBitRate;
+ }
+
+ public boolean isDownsample() {
+ return downsample;
+ }
+
+ public void setDownsample(boolean downsample) {
+ this.downsample = downsample;
+ }
+
+ public boolean isTranscode() {
+ return transcoding != null;
+ }
+
+ public void setTranscoding(Transcoding transcoding) {
+ this.transcoding = transcoding;
+ }
+
+ public Transcoding getTranscoding() {
+ return transcoding;
+ }
+
+ public MediaFile getMediaFile() {
+ return mediaFile;
+ }
+
+ public Integer getMaxBitRate() {
+ return maxBitRate;
+ }
+
+ public VideoTranscodingSettings getVideoTranscodingSettings() {
+ return videoTranscodingSettings;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java
new file mode 100644
index 00000000..e24e6409
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java
@@ -0,0 +1,267 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.Version;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicResponseHandler;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpConnectionParams;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides version-related services, including functionality for determining whether a newer
+ * version of Subsonic is available.
+ *
+ * @author Sindre Mehus
+ */
+public class VersionService {
+
+ private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");
+ private static final Logger LOG = Logger.getLogger(VersionService.class);
+
+ private Version localVersion;
+ private Version latestFinalVersion;
+ private Version latestBetaVersion;
+ private Date localBuildDate;
+ private String localBuildNumber;
+
+ /**
+ * Time when latest version was fetched (in milliseconds).
+ */
+ private long lastVersionFetched;
+
+ /**
+ * Only fetch last version this often (in milliseconds.).
+ */
+ private static final long LAST_VERSION_FETCH_INTERVAL = 7L * 24L * 3600L * 1000L; // One week
+
+ /**
+ * URL from which to fetch latest versions.
+ */
+ private static final String VERSION_URL = "http://subsonic.org/backend/version.view";
+
+ /**
+ * Returns the version number for the locally installed Subsonic version.
+ *
+ * @return The version number for the locally installed Subsonic version.
+ */
+ public synchronized Version getLocalVersion() {
+ if (localVersion == null) {
+ try {
+ localVersion = new Version(readLineFromResource("/version.txt"));
+ LOG.info("Resolved local Subsonic version to: " + localVersion);
+ } catch (Exception x) {
+ LOG.warn("Failed to resolve local Subsonic version.", x);
+ }
+ }
+ return localVersion;
+ }
+
+ /**
+ * Returns the version number for the latest available Subsonic final version.
+ *
+ * @return The version number for the latest available Subsonic final version, or <code>null</code>
+ * if the version number can't be resolved.
+ */
+ public synchronized Version getLatestFinalVersion() {
+ refreshLatestVersion();
+ return latestFinalVersion;
+ }
+
+ /**
+ * Returns the version number for the latest available Subsonic beta version.
+ *
+ * @return The version number for the latest available Subsonic beta version, or <code>null</code>
+ * if the version number can't be resolved.
+ */
+ public synchronized Version getLatestBetaVersion() {
+ refreshLatestVersion();
+ return latestBetaVersion;
+ }
+
+ /**
+ * Returns the build date for the locally installed Subsonic version.
+ *
+ * @return The build date for the locally installed Subsonic version, or <code>null</code>
+ * if the build date can't be resolved.
+ */
+ public synchronized Date getLocalBuildDate() {
+ if (localBuildDate == null) {
+ try {
+ String date = readLineFromResource("/build_date.txt");
+ localBuildDate = DATE_FORMAT.parse(date);
+ } catch (Exception x) {
+ LOG.warn("Failed to resolve local Subsonic build date.", x);
+ }
+ }
+ return localBuildDate;
+ }
+
+ /**
+ * Returns the build number for the locally installed Subsonic version.
+ *
+ * @return The build number for the locally installed Subsonic version, or <code>null</code>
+ * if the build number can't be resolved.
+ */
+ public synchronized String getLocalBuildNumber() {
+ if (localBuildNumber == null) {
+ try {
+ localBuildNumber = readLineFromResource("/build_number.txt");
+ } catch (Exception x) {
+ LOG.warn("Failed to resolve local Subsonic build number.", x);
+ }
+ }
+ return localBuildNumber;
+ }
+
+ /**
+ * Returns whether a new final version of Subsonic is available.
+ *
+ * @return Whether a new final version of Subsonic is available.
+ */
+ public boolean isNewFinalVersionAvailable() {
+ Version latest = getLatestFinalVersion();
+ Version local = getLocalVersion();
+
+ if (latest == null || local == null) {
+ return false;
+ }
+
+ return local.compareTo(latest) < 0;
+ }
+
+ /**
+ * Returns whether a new beta version of Subsonic is available.
+ *
+ * @return Whether a new beta version of Subsonic is available.
+ */
+ public boolean isNewBetaVersionAvailable() {
+ Version latest = getLatestBetaVersion();
+ Version local = getLocalVersion();
+
+ if (latest == null || local == null) {
+ return false;
+ }
+
+ return local.compareTo(latest) < 0;
+ }
+
+ /**
+ * Reads the first line from the resource with the given name.
+ *
+ * @param resourceName The resource name.
+ * @return The first line of the resource.
+ */
+ private String readLineFromResource(String resourceName) {
+ InputStream in = VersionService.class.getResourceAsStream(resourceName);
+ if (in == null) {
+ return null;
+ }
+ BufferedReader reader = null;
+ try {
+
+ reader = new BufferedReader(new InputStreamReader(in));
+ return reader.readLine();
+
+ } catch (IOException x) {
+ return null;
+ } finally {
+ IOUtils.closeQuietly(reader);
+ IOUtils.closeQuietly(in);
+ }
+ }
+
+ /**
+ * Refreshes the latest final and beta versions.
+ */
+ private void refreshLatestVersion() {
+ long now = System.currentTimeMillis();
+ boolean isOutdated = now - lastVersionFetched > LAST_VERSION_FETCH_INTERVAL;
+
+ if (isOutdated) {
+ try {
+ lastVersionFetched = now;
+ readLatestVersion();
+ } catch (Exception x) {
+ LOG.warn("Failed to resolve latest Subsonic version.", x);
+ }
+ }
+ }
+
+ /**
+ * Resolves the latest available Subsonic version by screen-scraping a web page.
+ *
+ * @throws IOException If an I/O error occurs.
+ */
+ private void readLatestVersion() throws IOException {
+
+ HttpClient client = new DefaultHttpClient();
+ HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000);
+ HttpConnectionParams.setSoTimeout(client.getParams(), 10000);
+ HttpGet method = new HttpGet(VERSION_URL + "?v=" + getLocalVersion());
+ String content;
+ try {
+
+ ResponseHandler<String> responseHandler = new BasicResponseHandler();
+ content = client.execute(method, responseHandler);
+
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+
+ BufferedReader reader = new BufferedReader(new StringReader(content));
+ Pattern finalPattern = Pattern.compile("SUBSONIC_FULL_VERSION_BEGIN(.*)SUBSONIC_FULL_VERSION_END");
+ Pattern betaPattern = Pattern.compile("SUBSONIC_BETA_VERSION_BEGIN(.*)SUBSONIC_BETA_VERSION_END");
+
+ try {
+ String line = reader.readLine();
+ while (line != null) {
+ Matcher finalMatcher = finalPattern.matcher(line);
+ if (finalMatcher.find()) {
+ latestFinalVersion = new Version(finalMatcher.group(1));
+ LOG.info("Resolved latest Subsonic final version to: " + latestFinalVersion);
+ }
+ Matcher betaMatcher = betaPattern.matcher(line);
+ if (betaMatcher.find()) {
+ latestBetaVersion = new Version(betaMatcher.group(1));
+ LOG.info("Resolved latest Subsonic beta version to: " + latestBetaVersion);
+ }
+ line = reader.readLine();
+ }
+
+ } finally {
+ reader.close();
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java
new file mode 100644
index 00000000..902387be
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java
@@ -0,0 +1,207 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.jukebox;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.FloatControl;
+import javax.sound.sampled.SourceDataLine;
+
+import org.apache.commons.io.IOUtils;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.service.JukeboxService;
+
+import static net.sourceforge.subsonic.service.jukebox.AudioPlayer.State.*;
+
+/**
+ * A simple wrapper for playing sound from an input stream.
+ * <p/>
+ * Supports pause and resume, but not restarting.
+ *
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class AudioPlayer {
+
+ private static final Logger LOG = Logger.getLogger(JukeboxService.class);
+
+ private final InputStream in;
+ private final Listener listener;
+ private final SourceDataLine line;
+ private final AtomicReference<State> state = new AtomicReference<State>(PAUSED);
+ private FloatControl gainControl;
+
+ public AudioPlayer(InputStream in, Listener listener) throws Exception {
+ this.in = new BufferedInputStream(in);
+ this.listener = listener;
+
+ AudioFormat format = AudioSystem.getAudioFileFormat(this.in).getFormat();
+ line = AudioSystem.getSourceDataLine(format);
+ line.open(format);
+ LOG.debug("Opened line " + line);
+
+ if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
+ gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
+ setGain(0.5f);
+ }
+ new AudioDataWriter();
+ }
+
+ /**
+ * Starts (or resumes) the player. This only has effect if the current state is
+ * {@link State#PAUSED}.
+ */
+ public synchronized void play() {
+ if (state.get() == PAUSED) {
+ line.start();
+ setState(PLAYING);
+ }
+ }
+
+ /**
+ * Pauses the player. This only has effect if the current state is
+ * {@link State#PLAYING}.
+ */
+ public synchronized void pause() {
+ if (state.get() == PLAYING) {
+ setState(PAUSED);
+ line.stop();
+ line.flush();
+ }
+ }
+
+ /**
+ * Closes the player, releasing all resources. After this the player state is
+ * {@link State#CLOSED} (unless the current state is {@link State#EOM}).
+ */
+ public synchronized void close() {
+ if (state.get() != CLOSED && state.get() != EOM) {
+ setState(CLOSED);
+ }
+
+ try {
+ line.stop();
+ } catch (Throwable x) {
+ LOG.warn("Failed to stop player: " + x, x);
+ }
+ try {
+ if (line.isOpen()) {
+ line.close();
+ LOG.debug("Closed line " + line);
+ }
+ } catch (Throwable x) {
+ LOG.warn("Failed to close player: " + x, x);
+ }
+ IOUtils.closeQuietly(in);
+ }
+
+ /**
+ * Returns the player state.
+ */
+ public State getState() {
+ return state.get();
+ }
+
+ /**
+ * Sets the gain.
+ *
+ * @param gain The gain between 0.0 and 1.0.
+ */
+ public void setGain(float gain) {
+ if (gainControl != null) {
+
+ double minGainDB = gainControl.getMinimum();
+ double maxGainDB = gainControl.getMaximum();
+ double ampGainDB = 0.5f * maxGainDB - minGainDB;
+ double cste = Math.log(10.0) / 20;
+ double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * gain);
+
+ valueDB = Math.min(valueDB, maxGainDB);
+ valueDB = Math.max(valueDB, minGainDB);
+
+ gainControl.setValue((float) valueDB);
+ }
+ }
+
+ /**
+ * Returns the position in seconds.
+ */
+ public int getPosition() {
+ return (int) (line.getMicrosecondPosition() / 1000000L);
+ }
+
+ private void setState(State state) {
+ if (this.state.getAndSet(state) != state && listener != null) {
+ listener.stateChanged(this, state);
+ }
+ }
+
+ private class AudioDataWriter implements Runnable {
+
+ public AudioDataWriter() {
+ new Thread(this).start();
+ }
+
+ public void run() {
+ try {
+ byte[] buffer = new byte[8192];
+
+ while (true) {
+
+ switch (state.get()) {
+ case CLOSED:
+ case EOM:
+ return;
+ case PAUSED:
+ Thread.sleep(250);
+ break;
+ case PLAYING:
+ int n = in.read(buffer);
+ if (n == -1) {
+ setState(EOM);
+ return;
+ }
+ line.write(buffer, 0, n);
+ break;
+ }
+ }
+ } catch (Throwable x) {
+ LOG.warn("Error when copying audio data: " + x, x);
+ } finally {
+ close();
+ }
+ }
+ }
+
+ public interface Listener {
+ void stateChanged(AudioPlayer player, State state);
+ }
+
+ public static enum State {
+ PAUSED,
+ PLAYING,
+ CLOSED,
+ EOM
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java
new file mode 100644
index 00000000..30ed2847
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java
@@ -0,0 +1,75 @@
+package net.sourceforge.subsonic.service.jukebox;
+
+import java.awt.FlowLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.FileInputStream;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JSlider;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * @author Sindre Mehus
+ * @version $Id$
+ */
+public class PlayerTest implements AudioPlayer.Listener {
+
+ private AudioPlayer player;
+
+ public PlayerTest() throws Exception {
+ player = new AudioPlayer(new FileInputStream("i:\\tmp\\foo.au"), this);
+ createGUI();
+ }
+
+ private void createGUI() {
+ JFrame frame = new JFrame();
+
+ JButton startButton = new JButton("Start");
+ JButton stopButton = new JButton("Stop");
+ JButton resetButton = new JButton("Reset");
+ final JSlider gainSlider = new JSlider(0, 1000);
+
+ startButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ player.play();
+ }
+ });
+ stopButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ player.pause();
+ }
+ });
+ resetButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ player.close();
+ }
+ });
+ gainSlider.addChangeListener(new ChangeListener() {
+ public void stateChanged(ChangeEvent e) {
+ float gain = (float) gainSlider.getValue() / 1000.0F;
+ player.setGain(gain);
+ }
+ });
+
+ frame.setLayout(new FlowLayout());
+ frame.add(startButton);
+ frame.add(stopButton);
+ frame.add(resetButton);
+ frame.add(gainSlider);
+
+ frame.pack();
+ frame.setVisible(true);
+ }
+
+ public static void main(String[] args) throws Exception {
+ new PlayerTest();
+ }
+
+ public void stateChanged(AudioPlayer player, AudioPlayer.State state) {
+ System.out.println(state);
+ }
+}
+
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java
new file mode 100644
index 00000000..897f39d4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java
@@ -0,0 +1,74 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import java.io.File;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+
+/**
+ * Parses meta data by guessing artist, album and song title based on the path of the file.
+ *
+ * @author Sindre Mehus
+ */
+public class DefaultMetaDataParser extends MetaDataParser {
+
+ /**
+ * Parses meta data for the given file. No guessing or reformatting is done.
+ *
+ * @param file The file to parse.
+ * @return Meta data for the file.
+ */
+ public MetaData getRawMetaData(File file) {
+ MetaData metaData = new MetaData();
+ metaData.setArtist(guessArtist(file));
+ metaData.setAlbumName(guessAlbum(file, metaData.getArtist()));
+ metaData.setTitle(guessTitle(file));
+ return metaData;
+ }
+
+ /**
+ * Updates the given file with the given meta data.
+ * This method has no effect.
+ *
+ * @param file The file to update.
+ * @param metaData The new meta data.
+ */
+ public void setMetaData(MediaFile file, MetaData metaData) {
+ }
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Always false.
+ */
+ public boolean isEditingSupported() {
+ return false;
+ }
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ public boolean isApplicable(File file) {
+ return file.isFile();
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java
new file mode 100644
index 00000000..60ae1750
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java
@@ -0,0 +1,170 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.io.InputStreamReaderThread;
+import net.sourceforge.subsonic.service.ServiceLocator;
+import net.sourceforge.subsonic.service.TranscodingService;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.commons.io.FilenameUtils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses meta data from video files using FFmpeg (http://ffmpeg.org/).
+ * <p/>
+ * Currently duration, bitrate and dimension are supported.
+ *
+ * @author Sindre Mehus
+ */
+public class FFmpegParser extends MetaDataParser {
+
+ private static final Logger LOG = Logger.getLogger(FFmpegParser.class);
+ private static final Pattern DURATION_PATTERN = Pattern.compile("Duration: (\\d+):(\\d+):(\\d+).(\\d+)");
+ private static final Pattern BITRATE_PATTERN = Pattern.compile("bitrate: (\\d+) kb/s");
+ private static final Pattern DIMENSION_PATTERN = Pattern.compile("Video.*?, (\\d+)x(\\d+)");
+ private static final Pattern PAR_PATTERN = Pattern.compile("PAR (\\d+):(\\d+)");
+
+ private TranscodingService transcodingService;
+
+ /**
+ * Parses meta data for the given music file. No guessing or reformatting is done.
+ *
+ *
+ * @param file The music file to parse.
+ * @return Meta data for the file.
+ */
+ @Override
+ public MetaData getRawMetaData(File file) {
+
+ MetaData metaData = new MetaData();
+
+ try {
+
+ File ffmpeg = new File(transcodingService.getTranscodeDirectory(), "ffmpeg");
+
+ String[] command = new String[]{ffmpeg.getAbsolutePath(), "-i", file.getAbsolutePath()};
+ Process process = Runtime.getRuntime().exec(command);
+ InputStream stdout = process.getInputStream();
+ InputStream stderr = process.getErrorStream();
+
+ // Consume stdout, we're not interested in that.
+ new InputStreamReaderThread(stdout, "ffmpeg", true).start();
+
+ // Read everything from stderr. It will contain text similar to:
+ // Input #0, avi, from 'foo.avi':
+ // Duration: 00:00:33.90, start: 0.000000, bitrate: 2225 kb/s
+ // Stream #0.0: Video: mpeg4, yuv420p, 352x240 [PAR 1:1 DAR 22:15], 29.97 fps, 29.97 tbr, 29.97 tbn, 30k tbc
+ // Stream #0.1: Audio: pcm_s16le, 44100 Hz, 2 channels, s16, 1411 kb/s
+ String[] lines = StringUtil.readLines(stderr);
+
+ Integer width = null;
+ Integer height = null;
+ Double par = 1.0;
+ for (String line : lines) {
+
+ Matcher matcher = DURATION_PATTERN.matcher(line);
+ if (matcher.find()) {
+ int hours = Integer.parseInt(matcher.group(1));
+ int minutes = Integer.parseInt(matcher.group(2));
+ int seconds = Integer.parseInt(matcher.group(3));
+ metaData.setDurationSeconds(hours * 3600 + minutes * 60 + seconds);
+ }
+
+ matcher = BITRATE_PATTERN.matcher(line);
+ if (matcher.find()) {
+ metaData.setBitRate(Integer.valueOf(matcher.group(1)));
+ }
+
+ matcher = DIMENSION_PATTERN.matcher(line);
+ if (matcher.find()) {
+ width = Integer.valueOf(matcher.group(1));
+ height = Integer.valueOf(matcher.group(2));
+ }
+
+ // PAR = Pixel Aspect Rate
+ matcher = PAR_PATTERN.matcher(line);
+ if (matcher.find()) {
+ int a = Integer.parseInt(matcher.group(1));
+ int b = Integer.parseInt(matcher.group(2));
+ if (a > 0 && b > 0) {
+ par = (double) a / (double) b;
+ }
+ }
+ }
+
+ if (width != null && height != null) {
+ width = (int) Math.round(width.doubleValue() * par);
+ metaData.setWidth(width);
+ metaData.setHeight(height);
+ }
+
+
+ } catch (Throwable x) {
+ LOG.warn("Error when parsing metadata in " + file, x);
+ }
+
+ return metaData;
+ }
+
+ /**
+ * Not supported.
+ */
+ @Override
+ public void setMetaData(MediaFile file, MetaData metaData) {
+ throw new RuntimeException("setMetaData() not supported in " + getClass().getSimpleName());
+ }
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Always false.
+ */
+ @Override
+ public boolean isEditingSupported() {
+ return false;
+ }
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ @Override
+ public boolean isApplicable(File file) {
+ String format = FilenameUtils.getExtension(file.getName()).toLowerCase();
+
+ for (String s : ServiceLocator.getSettingsService().getVideoFileTypesAsArray()) {
+ if (format.equals(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setTranscodingService(TranscodingService transcodingService) {
+ this.transcodingService = transcodingService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java
new file mode 100644
index 00000000..8fa7659a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java
@@ -0,0 +1,296 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.domain.MediaFile;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang.StringUtils;
+import org.jaudiotagger.audio.AudioFile;
+import org.jaudiotagger.audio.AudioFileIO;
+import org.jaudiotagger.audio.AudioHeader;
+import org.jaudiotagger.tag.FieldKey;
+import org.jaudiotagger.tag.Tag;
+import org.jaudiotagger.tag.datatype.Artwork;
+import org.jaudiotagger.tag.reference.GenreTypes;
+
+import java.io.File;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.logging.LogManager;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses meta data from audio files using the Jaudiotagger library
+ * (http://www.jthink.net/jaudiotagger/)
+ *
+ * @author Sindre Mehus
+ */
+public class JaudiotaggerParser extends MetaDataParser {
+
+ private static final Logger LOG = Logger.getLogger(JaudiotaggerParser.class);
+ private static final Pattern GENRE_PATTERN = Pattern.compile("\\((\\d+)\\).*");
+ private static final Pattern TRACK_NUMBER_PATTERN = Pattern.compile("(\\d+)/\\d+");
+
+ static {
+ try {
+ LogManager.getLogManager().reset();
+ } catch (Throwable x) {
+ LOG.warn("Failed to turn off logging from Jaudiotagger.", x);
+ }
+ }
+
+ /**
+ * Parses meta data for the given music file. No guessing or reformatting is done.
+ *
+ *
+ * @param file The music file to parse.
+ * @return Meta data for the file.
+ */
+ @Override
+ public MetaData getRawMetaData(File file) {
+
+ MetaData metaData = new MetaData();
+
+ try {
+ AudioFile audioFile = AudioFileIO.read(file);
+ Tag tag = audioFile.getTag();
+ if (tag != null) {
+ metaData.setAlbumName(getTagField(tag, FieldKey.ALBUM));
+ metaData.setTitle(getTagField(tag, FieldKey.TITLE));
+ metaData.setYear(parseInteger(getTagField(tag, FieldKey.YEAR)));
+ metaData.setGenre(mapGenre(getTagField(tag, FieldKey.GENRE)));
+ metaData.setDiscNumber(parseInteger(getTagField(tag, FieldKey.DISC_NO)));
+ metaData.setTrackNumber(parseTrackNumber(getTagField(tag, FieldKey.TRACK)));
+
+ String songArtist = getTagField(tag, FieldKey.ARTIST);
+ String albumArtist = getTagField(tag, FieldKey.ALBUM_ARTIST);
+ metaData.setArtist(StringUtils.isBlank(albumArtist) ? songArtist : albumArtist);
+ }
+
+ AudioHeader audioHeader = audioFile.getAudioHeader();
+ if (audioHeader != null) {
+ metaData.setVariableBitRate(audioHeader.isVariableBitRate());
+ metaData.setBitRate((int) audioHeader.getBitRateAsNumber());
+ metaData.setDurationSeconds(audioHeader.getTrackLength());
+ }
+
+
+ } catch (Throwable x) {
+ LOG.warn("Error when parsing tags in " + file, x);
+ }
+
+ return metaData;
+ }
+
+ private String getTagField(Tag tag, FieldKey fieldKey) {
+ try {
+ return StringUtils.trimToNull(tag.getFirst(fieldKey));
+ } catch (Exception x) {
+ // Ignored.
+ return null;
+ }
+ }
+
+ /**
+ * Returns all tags supported by id3v1.
+ */
+ public static SortedSet<String> getID3V1Genres() {
+ return new TreeSet<String>(GenreTypes.getInstanceOf().getAlphabeticalValueList());
+ }
+
+ /**
+ * Sometimes the genre is returned as "(17)" or "(17)Rock", instead of "Rock". This method
+ * maps the genre ID to the corresponding text.
+ */
+ private String mapGenre(String genre) {
+ if (genre == null) {
+ return null;
+ }
+ Matcher matcher = GENRE_PATTERN.matcher(genre);
+ if (matcher.matches()) {
+ int genreId = Integer.parseInt(matcher.group(1));
+ if (genreId >= 0 && genreId < GenreTypes.getInstanceOf().getSize()) {
+ return GenreTypes.getInstanceOf().getValueForId(genreId);
+ }
+ }
+ return genre;
+ }
+
+ /**
+ * Parses the track number from the given string. Also supports
+ * track numbers on the form "4/12".
+ */
+ private Integer parseTrackNumber(String trackNumber) {
+ if (trackNumber == null) {
+ return null;
+ }
+
+ Integer result = null;
+
+ try {
+ result = new Integer(trackNumber);
+ } catch (NumberFormatException x) {
+ Matcher matcher = TRACK_NUMBER_PATTERN.matcher(trackNumber);
+ if (matcher.matches()) {
+ try {
+ result = Integer.valueOf(matcher.group(1));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ }
+
+ if (Integer.valueOf(0).equals(result)) {
+ return null;
+ }
+ return result;
+ }
+
+ private Integer parseInteger(String s) {
+ s = StringUtils.trimToNull(s);
+ if (s == null) {
+ return null;
+ }
+ try {
+ Integer result = Integer.valueOf(s);
+ if (Integer.valueOf(0).equals(result)) {
+ return null;
+ }
+ return result;
+ } catch (NumberFormatException x) {
+ return null;
+ }
+ }
+
+ /**
+ * Updates the given file with the given meta data.
+ *
+ * @param file The music file to update.
+ * @param metaData The new meta data.
+ */
+ @Override
+ public void setMetaData(MediaFile file, MetaData metaData) {
+
+ try {
+ AudioFile audioFile = AudioFileIO.read(file.getFile());
+ Tag tag = audioFile.getTagOrCreateAndSetDefault();
+
+ tag.setField(FieldKey.ARTIST, StringUtils.trimToEmpty(metaData.getArtist()));
+ tag.setField(FieldKey.ALBUM_ARTIST, StringUtils.trimToEmpty(metaData.getArtist()));
+ tag.setField(FieldKey.ALBUM, StringUtils.trimToEmpty(metaData.getAlbumName()));
+ tag.setField(FieldKey.TITLE, StringUtils.trimToEmpty(metaData.getTitle()));
+ tag.setField(FieldKey.GENRE, StringUtils.trimToEmpty(metaData.getGenre()));
+
+ Integer track = metaData.getTrackNumber();
+ if (track == null) {
+ tag.deleteField(FieldKey.TRACK);
+ } else {
+ tag.setField(FieldKey.TRACK, String.valueOf(track));
+ }
+
+ Integer year = metaData.getYear();
+ if (year == null) {
+ tag.deleteField(FieldKey.YEAR);
+ } else {
+ tag.setField(FieldKey.YEAR, String.valueOf(year));
+ }
+
+ audioFile.commit();
+
+ } catch (Throwable x) {
+ LOG.warn("Failed to update tags for file " + file, x);
+ throw new RuntimeException("Failed to update tags for file " + file + ". " + x.getMessage(), x);
+ }
+ }
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Always true.
+ */
+ @Override
+ public boolean isEditingSupported() {
+ return true;
+ }
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The music file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ @Override
+ public boolean isApplicable(File file) {
+ if (!file.isFile()) {
+ return false;
+ }
+
+ String format = FilenameUtils.getExtension(file.getName()).toLowerCase();
+
+ return format.equals("mp3") ||
+ format.equals("m4a") ||
+ format.equals("aac") ||
+ format.equals("ogg") ||
+ format.equals("flac") ||
+ format.equals("wav") ||
+ format.equals("mpc") ||
+ format.equals("mp+") ||
+ format.equals("ape") ||
+ format.equals("wma");
+ }
+
+ /**
+ * Returns whether cover art image data is available in the given file.
+ *
+ * @param file The music file.
+ * @return Whether cover art image data is available.
+ */
+ public boolean isImageAvailable(MediaFile file) {
+ try {
+ return getArtwork(file) != null;
+ } catch (Throwable x) {
+ LOG.warn("Failed to find cover art tag in " + file, x);
+ return false;
+ }
+ }
+
+ /**
+ * Returns the cover art image data embedded in the given file.
+ *
+ * @param file The music file.
+ * @return The embedded cover art image data, or <code>null</code> if not available.
+ */
+ public byte[] getImageData(MediaFile file) {
+ try {
+ return getArtwork(file).getBinaryData();
+ } catch (Throwable x) {
+ LOG.warn("Failed to find cover art tag in " + file, x);
+ return null;
+ }
+ }
+
+ private Artwork getArtwork(MediaFile file) throws Exception {
+ AudioFile audioFile = AudioFileIO.read(file.getFile());
+ Tag tag = audioFile.getTag();
+ return tag == null ? null : tag.getFirstArtwork();
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java
new file mode 100644
index 00000000..d3fa08a0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java
@@ -0,0 +1,135 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+/**
+ * Contains meta-data (song title, artist, album etc) for a music file.
+ * @author Sindre Mehus
+ */
+public class MetaData {
+
+ private Integer discNumber;
+ private Integer trackNumber;
+ private String title;
+ private String artist;
+ private String albumName;
+ private String genre;
+ private Integer year;
+ private Integer bitRate;
+ private boolean variableBitRate;
+ private Integer durationSeconds;
+ private Integer width;
+ private Integer height;
+
+ public Integer getDiscNumber() {
+ return discNumber;
+ }
+
+ public void setDiscNumber(Integer discNumber) {
+ this.discNumber = discNumber;
+ }
+
+ public Integer getTrackNumber() {
+ return trackNumber;
+ }
+
+ public void setTrackNumber(Integer trackNumber) {
+ this.trackNumber = trackNumber;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public void setArtist(String artist) {
+ this.artist = artist;
+ }
+
+ public String getAlbumName() {
+ return albumName;
+ }
+
+ public void setAlbumName(String albumName) {
+ this.albumName = albumName;
+ }
+
+ public String getGenre() {
+ return genre;
+ }
+
+ public void setGenre(String genre) {
+ this.genre = genre;
+ }
+
+ public Integer getYear() {
+ return year;
+ }
+
+ public void setYear(Integer year) {
+ this.year = year;
+ }
+
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ public boolean getVariableBitRate() {
+ return variableBitRate;
+ }
+
+ public void setVariableBitRate(boolean variableBitRate) {
+ this.variableBitRate = variableBitRate;
+ }
+
+ public Integer getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ public void setDurationSeconds(Integer durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ }
+
+ public Integer getWidth() {
+ return width;
+ }
+
+ public void setWidth(Integer width) {
+ this.width = width;
+ }
+
+ public Integer getHeight() {
+ return height;
+ }
+
+ public void setHeight(Integer height) {
+ this.height = height;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java
new file mode 100644
index 00000000..2ed70acc
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java
@@ -0,0 +1,162 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import java.io.File;
+import java.util.List;
+
+import org.apache.commons.io.FilenameUtils;
+
+import net.sourceforge.subsonic.domain.MediaFile;
+import net.sourceforge.subsonic.domain.MusicFolder;
+import net.sourceforge.subsonic.service.ServiceLocator;
+import net.sourceforge.subsonic.service.SettingsService;
+
+
+/**
+ * Parses meta data from media files.
+ *
+ * @author Sindre Mehus
+ */
+public abstract class MetaDataParser {
+
+ /**
+ * Parses meta data for the given file.
+ *
+ * @param file The file to parse.
+ * @return Meta data for the file, never null.
+ */
+ public MetaData getMetaData(File file) {
+
+ MetaData metaData = getRawMetaData(file);
+ String artist = metaData.getArtist();
+ String album = metaData.getAlbumName();
+ String title = metaData.getTitle();
+
+ if (artist == null) {
+ artist = guessArtist(file);
+ }
+ if (album == null) {
+ album = guessAlbum(file, artist);
+ }
+ if (title == null) {
+ title = guessTitle(file);
+ }
+
+ title = removeTrackNumberFromTitle(title, metaData.getTrackNumber());
+ metaData.setArtist(artist);
+ metaData.setAlbumName(album);
+ metaData.setTitle(title);
+
+ return metaData;
+ }
+
+ /**
+ * Parses meta data for the given file. No guessing or reformatting is done.
+ *
+ *
+ * @param file The file to parse.
+ * @return Meta data for the file.
+ */
+ public abstract MetaData getRawMetaData(File file);
+
+ /**
+ * Updates the given file with the given meta data.
+ *
+ * @param file The file to update.
+ * @param metaData The new meta data.
+ */
+ public abstract void setMetaData(MediaFile file, MetaData metaData);
+
+ /**
+ * Returns whether this parser is applicable to the given file.
+ *
+ * @param file The file in question.
+ * @return Whether this parser is applicable to the given file.
+ */
+ public abstract boolean isApplicable(File file);
+
+ /**
+ * Returns whether this parser supports tag editing (using the {@link #setMetaData} method).
+ *
+ * @return Whether tag editing is supported.
+ */
+ public abstract boolean isEditingSupported();
+
+ /**
+ * Guesses the artist for the given file.
+ */
+ public String guessArtist(File file) {
+ File parent = file.getParentFile();
+ if (isRoot(parent)) {
+ return null;
+ }
+ File grandParent = parent.getParentFile();
+ return isRoot(grandParent) ? null : grandParent.getName();
+ }
+
+ /**
+ * Guesses the album for the given file.
+ */
+ public String guessAlbum(File file, String artist) {
+ File parent = file.getParentFile();
+ String album = isRoot(parent) ? null : parent.getName();
+ if (artist != null && album != null) {
+ album = album.replace(artist + " - ", "");
+ }
+ return album;
+ }
+
+ /**
+ * Guesses the title for the given file.
+ */
+ public String guessTitle(File file) {
+ return removeTrackNumberFromTitle(FilenameUtils.getBaseName(file.getPath()), null);
+ }
+
+ private boolean isRoot(File file) {
+ SettingsService settings = ServiceLocator.getSettingsService();
+ List<MusicFolder> folders = settings.getAllMusicFolders(false, true);
+ for (MusicFolder folder : folders) {
+ if (file.equals(folder.getPath())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes any prefixed track number from the given title string.
+ *
+ * @param title The title with or without a prefixed track number, e.g., "02 - Back In Black".
+ * @param trackNumber If specified, this is the "true" track number.
+ * @return The title with the track number removed, e.g., "Back In Black".
+ */
+ protected String removeTrackNumberFromTitle(String title, Integer trackNumber) {
+ title = title.trim();
+
+ // Don't remove numbers if true track number is given, and title does not start with it.
+ if (trackNumber != null && !title.matches("0?" + trackNumber + "[\\.\\- ].*")) {
+ return title;
+ }
+
+ String result = title.replaceFirst("^\\d{2}[\\.\\- ]+", "");
+ return result.length() == 0 ? title : result;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java
new file mode 100644
index 00000000..31b56be4
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.service.metadata;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Factory for creating meta-data parsers.
+ *
+ * @author Sindre Mehus
+ */
+public class MetaDataParserFactory {
+
+ private List<MetaDataParser> parsers;
+
+ public void setParsers(List<MetaDataParser> parsers) {
+ this.parsers = parsers;
+ }
+
+ /**
+ * Returns a meta-data parser for the given file.
+ *
+ * @param file The file in question.
+ * @return An applicable parser, or <code>null</code> if no parser is found.
+ */
+ public MetaDataParser getParser(File file) {
+ for (MetaDataParser parser : parsers) {
+ if (parser.isApplicable(file)) {
+ return parser;
+ }
+ }
+ return null;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.java
new file mode 100644
index 00000000..c7c09677
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.java
@@ -0,0 +1,77 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.taglib;
+
+import org.apache.commons.lang.StringEscapeUtils;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.JspTagException;
+import javax.servlet.jsp.tagext.BodyTagSupport;
+import java.io.IOException;
+
+/**
+ * Escapes the characters in a <code>String</code> using JavaScript String rules.
+ * <p/>
+ * Escapes any values it finds into their JavaScript String form.
+ * Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.)
+ * <p/>
+ * So a tab becomes the characters <code>'\\'</code> and
+ * <code>'t'</code>.
+ * <p/>
+ * The only difference between Java strings and JavaScript strings
+ * is that in JavaScript, a single quote must be escaped.
+ * <p/>
+ * Example:
+ * <pre>
+ * input string: He didn't say, "Stop!"
+ * output string: He didn\'t say, \"Stop!\"
+ * </pre>
+ *
+ * @author Sindre Mehus
+ */
+public class EscapeJavaScriptTag extends BodyTagSupport {
+
+ private String string;
+
+ public int doStartTag() throws JspException {
+ return EVAL_BODY_BUFFERED;
+ }
+
+ public int doEndTag() throws JspException {
+ try {
+ pageContext.getOut().print(StringEscapeUtils.escapeJavaScript(string));
+ } catch (IOException x) {
+ throw new JspTagException(x);
+ }
+ return EVAL_PAGE;
+ }
+
+ public void release() {
+ string = null;
+ super.release();
+ }
+
+ public String getString() {
+ return string;
+ }
+
+ public void setString(String string) {
+ this.string = string;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java
new file mode 100644
index 00000000..0279316b
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java
@@ -0,0 +1,76 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.taglib;
+
+import net.sourceforge.subsonic.util.*;
+import org.springframework.web.servlet.support.*;
+
+import javax.servlet.http.*;
+import javax.servlet.jsp.*;
+import javax.servlet.jsp.tagext.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * Converts a byte-count to a formatted string suitable for display to the user, with respect
+ * to the current locale.
+ * <p/>
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This class assumes that 1 KB is 1024 bytes.
+ *
+ * @author Sindre Mehus
+ */
+public class FormatBytesTag extends BodyTagSupport {
+
+ private long bytes;
+
+ public int doStartTag() throws JspException {
+ return EVAL_BODY_BUFFERED;
+ }
+
+ public int doEndTag() throws JspException {
+ Locale locale = RequestContextUtils.getLocale((HttpServletRequest) pageContext.getRequest());
+ String result = StringUtil.formatBytes(bytes, locale);
+
+ try {
+ pageContext.getOut().print(result);
+ } catch (IOException x) {
+ throw new JspTagException(x);
+ }
+ return EVAL_PAGE;
+ }
+
+ public void release() {
+ bytes = 0L;
+ super.release();
+ }
+
+ public long getBytes() {
+ return bytes;
+ }
+
+ public void setBytes(long bytes) {
+ this.bytes = bytes;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.java
new file mode 100644
index 00000000..1043902e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.java
@@ -0,0 +1,67 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.taglib;
+
+import javax.servlet.jsp.tagext.*;
+import javax.servlet.jsp.*;
+
+/**
+ * A tag representing an URL query parameter.
+ *
+ * @see ParamTag
+ * @author Sindre Mehus
+ */
+public class ParamTag extends TagSupport {
+
+ private String name;
+ private String value;
+
+ public int doEndTag() throws JspTagException {
+
+ // Add parameter name and value to surrounding 'url' tag.
+ UrlTag tag = (UrlTag) findAncestorWithClass(this, UrlTag.class);
+ if (tag == null) {
+ throw new JspTagException("'sub:param' tag used outside 'sub:url'");
+ }
+ tag.addParameter(name, value);
+ return EVAL_PAGE;
+ }
+
+ public void release() {
+ name = null;
+ value = null;
+ super.release();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java
new file mode 100644
index 00000000..141ba847
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java
@@ -0,0 +1,207 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.taglib;
+
+import net.sourceforge.subsonic.Logger;
+import net.sourceforge.subsonic.filter.ParameterDecodingFilter;
+import net.sourceforge.subsonic.util.StringUtil;
+import org.apache.taglibs.standard.tag.common.core.UrlSupport;
+import org.apache.commons.lang.CharUtils;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.JspTagException;
+import javax.servlet.jsp.PageContext;
+import javax.servlet.jsp.tagext.BodyTagSupport;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Creates a URL with optional query parameters. Similar to 'c:url', but
+ * you may specify which character encoding to use for the URL query
+ * parameters. If no encoding is specified, the following steps are performed:
+ * <ul>
+ * <li>Parameter values are encoded as the hexadecimal representation of the UTF-8 bytes of the original string.</li>
+ * <li>Parameter names are prepended with the suffix "Utf8Hex"</li>
+ * <li>Note: Nothing is done with the parameter name or value if the value only contains ASCII alphanumeric characters.</li>
+ * </ul>
+ * <p/>
+ * (The problem with c:url is that is uses the same encoding as the http response,
+ * but most(?) servlet container assumes that ISO-8859-1 is used.)
+ *
+ * @author Sindre Mehus
+ */
+public class UrlTag extends BodyTagSupport {
+
+ private String DEFAULT_ENCODING = "Utf8Hex";
+ private static final Logger LOG = Logger.getLogger(UrlTag.class);
+
+ private String var;
+ private String value;
+ private String encoding = DEFAULT_ENCODING;
+ private List<Parameter> parameters = new ArrayList<Parameter>();
+
+ public int doStartTag() throws JspException {
+ parameters.clear();
+ return EVAL_BODY_BUFFERED;
+ }
+
+ public int doEndTag() throws JspException {
+
+ // Rewrite and encode the url.
+ String result = formatUrl();
+
+ // Store or print the output
+ if (var != null)
+ pageContext.setAttribute(var, result, PageContext.PAGE_SCOPE);
+ else {
+ try {
+ pageContext.getOut().print(result);
+ } catch (IOException x) {
+ throw new JspTagException(x);
+ }
+ }
+ return EVAL_PAGE;
+ }
+
+ private String formatUrl() throws JspException {
+ String baseUrl = UrlSupport.resolveUrl(value, null, pageContext);
+
+ StringBuffer result = new StringBuffer();
+ result.append(baseUrl);
+ if (!parameters.isEmpty()) {
+ result.append('?');
+
+ for (int i = 0; i < parameters.size(); i++) {
+ Parameter parameter = parameters.get(i);
+ try {
+ result.append(parameter.getName());
+ if (isUtf8Hex() && !isAsciiAlphaNumeric(parameter.getValue())) {
+ result.append(ParameterDecodingFilter.PARAM_SUFFIX);
+ }
+
+ result.append('=');
+ if (parameter.getValue() != null) {
+ result.append(encode(parameter.getValue()));
+ }
+ if (i < parameters.size() - 1) {
+ result.append("&");
+ }
+
+ } catch (UnsupportedEncodingException x) {
+ throw new JspTagException(x);
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ private String encode(String s) throws UnsupportedEncodingException {
+ if (isUtf8Hex()) {
+ if (isAsciiAlphaNumeric(s)) {
+ return s;
+ }
+
+ try {
+ return StringUtil.utf8HexEncode(s);
+ } catch (Exception x) {
+ LOG.error("Failed to utf8hex-encode the string '" + s + "'.", x);
+ return s;
+ }
+ }
+
+ return URLEncoder.encode(s, encoding);
+ }
+
+ private boolean isUtf8Hex() {
+ return DEFAULT_ENCODING.equals(encoding);
+ }
+
+ private boolean isAsciiAlphaNumeric(String s) {
+ if (s == null) {
+ return true;
+ }
+
+ for (int i = 0; i < s.length(); i++) {
+ if (!CharUtils.isAsciiAlphanumeric(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void release() {
+ var = null;
+ value = null;
+ encoding = DEFAULT_ENCODING;
+ parameters.clear();
+ super.release();
+ }
+
+ public void addParameter(String name, String value) {
+ parameters.add(new Parameter(name, value));
+ }
+
+ public String getVar() {
+ return var;
+ }
+
+ public void setVar(String var) {
+ this.var = var;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ /**
+ * A URL query parameter.
+ */
+ private static class Parameter {
+ private String name;
+ private String value;
+
+ private Parameter(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ private String getName() {
+ return name;
+ }
+
+ private String getValue() {
+ return value;
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.java
new file mode 100644
index 00000000..e099bd1e
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.java
@@ -0,0 +1,72 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.taglib;
+
+import org.radeox.api.engine.*;
+import org.radeox.api.engine.context.*;
+import org.radeox.engine.*;
+import org.radeox.engine.context.*;
+import org.apache.commons.lang.*;
+
+import javax.servlet.jsp.*;
+import javax.servlet.jsp.tagext.*;
+import java.io.*;
+
+/**
+ * Renders a Wiki text with markup to HTML, using the Radeox render engine.
+ *
+ * @author Sindre Mehus
+ */
+public class WikiTag extends BodyTagSupport {
+
+ private static final RenderContext RENDER_CONTEXT = new BaseRenderContext();
+ private static final RenderEngine RENDER_ENGINE = new BaseRenderEngine();
+
+ private String text;
+
+ public int doStartTag() throws JspException {
+ return EVAL_BODY_BUFFERED;
+ }
+
+ public int doEndTag() throws JspException {
+ String result;
+ synchronized (RENDER_ENGINE) {
+ result = RENDER_ENGINE.render(StringEscapeUtils.unescapeXml(text), RENDER_CONTEXT);
+ }
+ try {
+ pageContext.getOut().print(result);
+ } catch (IOException x) {
+ throw new JspTagException(x);
+ }
+ return EVAL_PAGE;
+ }
+
+ public void release() {
+ text = null;
+ super.release();
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java
new file mode 100644
index 00000000..874c2e9c
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java
@@ -0,0 +1,117 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.theme;
+
+import net.sourceforge.subsonic.service.*;
+import net.sourceforge.subsonic.domain.*;
+import org.springframework.web.servlet.*;
+
+import javax.servlet.http.*;
+import java.util.*;
+
+/**
+ * Theme resolver implementation which returns the theme selected in the settings.
+ *
+ * @author Sindre Mehus
+ */
+public class SubsonicThemeResolver implements ThemeResolver {
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+ private Set<String> themeIds;
+
+ /**
+ * Resolve the current theme name via the given request.
+ *
+ * @param request Request to be used for resolution
+ * @return The current theme name
+ */
+ public String resolveThemeName(HttpServletRequest request) {
+ String themeId = (String) request.getAttribute("subsonic.theme");
+ if (themeId != null) {
+ return themeId;
+ }
+
+ // Optimization: Cache theme in the request.
+ themeId = doResolveThemeName(request);
+ request.setAttribute("subsonic.theme", themeId);
+
+ return themeId;
+ }
+
+ private String doResolveThemeName(HttpServletRequest request) {
+ String themeId = null;
+
+ // Look for user-specific theme.
+ String username = securityService.getCurrentUsername(request);
+ if (username != null) {
+ UserSettings userSettings = settingsService.getUserSettings(username);
+ if (userSettings != null) {
+ themeId = userSettings.getThemeId();
+ }
+ }
+
+ if (themeId != null && themeExists(themeId)) {
+ return themeId;
+ }
+
+ // Return system theme.
+ themeId = settingsService.getThemeId();
+ return themeExists(themeId) ? themeId : "default";
+ }
+
+ /**
+ * Returns whether the theme with the given ID exists.
+ * @param themeId The theme ID.
+ * @return Whether the theme with the given ID exists.
+ */
+ private synchronized boolean themeExists(String themeId) {
+ // Lazily create set of theme IDs.
+ if (themeIds == null) {
+ themeIds = new HashSet<String>();
+ Theme[] themes = settingsService.getAvailableThemes();
+ for (Theme theme : themes) {
+ themeIds.add(theme.getId());
+ }
+ }
+
+ return themeIds.contains(themeId);
+ }
+
+ /**
+ * Set the current theme name to the given one. This method is not supported.
+ *
+ * @param request Request to be used for theme name modification
+ * @param response Response to be used for theme name modification
+ * @param themeName The new theme name
+ * @throws UnsupportedOperationException If the ThemeResolver implementation
+ * does not support dynamic changing of the theme
+ */
+ public void setThemeName(HttpServletRequest request, HttpServletResponse response, String themeName) {
+ throw new UnsupportedOperationException("Cannot change theme - use a different theme resolution strategy");
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.java
new file mode 100644
index 00000000..61db6516
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.java
@@ -0,0 +1,49 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.theme;
+
+import org.springframework.ui.context.support.ResourceBundleThemeSource;
+import org.springframework.context.MessageSource;
+import org.springframework.context.support.ResourceBundleMessageSource;
+
+/**
+ * Theme source implementation which uses two resource bundles: the
+ * theme specific (e.g., barents.properties), and the default (default.properties).
+ *
+ * @author Sindre Mehus
+ */
+public class SubsonicThemeSource extends ResourceBundleThemeSource {
+
+ private String defaultResourceBundle;
+
+ @Override
+ protected MessageSource createMessageSource(String basename) {
+ ResourceBundleMessageSource messageSource = (ResourceBundleMessageSource) super.createMessageSource(basename);
+
+ ResourceBundleMessageSource parentMessageSource = new ResourceBundleMessageSource();
+ parentMessageSource.setBasename(defaultResourceBundle);
+ messageSource.setParentMessageSource(parentMessageSource);
+
+ return messageSource;
+ }
+
+ public void setDefaultResourceBundle(String defaultResourceBundle) {
+ this.defaultResourceBundle = defaultResourceBundle;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java
new file mode 100644
index 00000000..f9b89bb7
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.upload;
+
+import org.apache.commons.fileupload.disk.DiskFileItem;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.io.IOException;
+
+/**
+ * Extension of Commons FileUpload for monitoring the upload progress.
+ *
+ * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net
+ */
+public class MonitoredDiskFileItem extends DiskFileItem {
+ private MonitoredOutputStream mos;
+ private UploadListener listener;
+
+ public MonitoredDiskFileItem(String fieldName, String contentType, boolean isFormField, String fileName, int sizeThreshold,
+ File repository, UploadListener listener) {
+ super(fieldName, contentType, isFormField, fileName, sizeThreshold, repository);
+ this.listener = listener;
+ if (fileName != null) {
+ listener.start(fileName);
+ }
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ if (mos == null) {
+ mos = new MonitoredOutputStream(super.getOutputStream(), listener);
+ }
+ return mos;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java
new file mode 100644
index 00000000..b5d6125d
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java
@@ -0,0 +1,47 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.upload;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+
+import java.io.File;
+
+/**
+ * Extension of Commons FileUpload for monitoring the upload progress.
+ *
+ * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net
+ */
+public class MonitoredDiskFileItemFactory extends DiskFileItemFactory {
+ private UploadListener listener;
+
+ public MonitoredDiskFileItemFactory(UploadListener listener) {
+ super();
+ this.listener = listener;
+ }
+
+ public MonitoredDiskFileItemFactory(int sizeThreshold, File repository, UploadListener listener) {
+ super(sizeThreshold, repository);
+ this.listener = listener;
+ }
+
+ public FileItem createItem(String fieldName, String contentType, boolean isFormField, String fileName) {
+ return new MonitoredDiskFileItem(fieldName, contentType, isFormField, fileName, getSizeThreshold(), getRepository(), listener);
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.java
new file mode 100644
index 00000000..c7f0d525
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.java
@@ -0,0 +1,60 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.upload;
+
+import java.io.OutputStream;
+import java.io.IOException;
+
+/**
+ * Extension of Commons FileUpload for monitoring the upload progress.
+ *
+ * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net
+ */
+public class MonitoredOutputStream extends OutputStream {
+ private OutputStream target;
+ private UploadListener listener;
+
+ public MonitoredOutputStream(OutputStream target, UploadListener listener) {
+ this.target = target;
+ this.listener = listener;
+ }
+
+ public void write(byte[] b, int off, int len) throws IOException {
+ target.write(b, off, len);
+ listener.bytesRead(len);
+ }
+
+ public void write(byte[] b) throws IOException {
+ target.write(b);
+ listener.bytesRead(b.length);
+ }
+
+ public void write(int b) throws IOException {
+ target.write(b);
+ listener.bytesRead(1);
+ }
+
+ public void close() throws IOException {
+ target.close();
+ }
+
+ public void flush() throws IOException {
+ target.flush();
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java
new file mode 100644
index 00000000..7eac415a
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java
@@ -0,0 +1,29 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.upload;
+
+/**
+ * Extension of Commons FileUpload for monitoring the upload progress.
+ *
+ * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net
+ */
+public interface UploadListener {
+ void start(String fileName);
+ void bytesRead(long bytesRead);
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java
new file mode 100644
index 00000000..fb240d5f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java
@@ -0,0 +1,71 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.util;
+
+import java.util.*;
+
+/**
+ * Simple implementation of a bounded list. If the maximum size is reached, adding a new element will
+ * remove the first element in the list.
+ *
+ * @author Sindre Mehus
+ * @version $Revision: 1.1 $ $Date: 2005/05/09 20:01:25 $
+ */
+public class BoundedList<E> extends LinkedList<E> {
+ private int maxSize;
+
+ /**
+ * Creates a new bounded list with the given maximum size.
+ * @param maxSize The maximum number of elements the list may hold.
+ */
+ public BoundedList(int maxSize) {
+ this.maxSize = maxSize;
+ }
+
+ /**
+ * Adds an element to the tail of the list. If the list is full, the first element is removed.
+ * @param e The element to add.
+ * @return Always <code>true</code>.
+ */
+ public boolean add(E e) {
+ if (isFull()) {
+ removeFirst();
+ }
+ return super.add(e);
+ }
+
+ /**
+ * Adds an element to the head of list. If the list is full, the last element is removed.
+ * @param e The element to add.
+ */
+ public void addFirst(E e) {
+ if (isFull()) {
+ removeLast();
+ }
+ super.addFirst(e);
+ }
+
+ /**
+ * Returns whether the list if full.
+ * @return Whether the list is full.
+ */
+ private boolean isFull() {
+ return size() == maxSize;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java
new file mode 100644
index 00000000..e91758ef
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java
@@ -0,0 +1,186 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.util;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.Arrays;
+
+import net.sourceforge.subsonic.Logger;
+
+/**
+ * Miscellaneous file utility methods.
+ *
+ * @author Sindre Mehus
+ */
+public final class FileUtil {
+
+ private static final Logger LOG = Logger.getLogger(FileUtil.class);
+
+ /**
+ * Disallow external instantiation.
+ */
+ private FileUtil() {
+ }
+
+ public static boolean isFile(final File file) {
+ return timed(new FileTask<Boolean>("isFile", file) {
+ @Override
+ public Boolean execute() {
+ return file.isFile();
+ }
+ });
+ }
+
+ public static boolean isDirectory(final File file) {
+ return timed(new FileTask<Boolean>("isDirectory", file) {
+ @Override
+ public Boolean execute() {
+ return file.isDirectory();
+ }
+ });
+ }
+
+ public static boolean exists(final File file) {
+ return timed(new FileTask<Boolean>("exists", file) {
+ @Override
+ public Boolean execute() {
+ return file.exists();
+ }
+ });
+ }
+
+ public static long lastModified(final File file) {
+ return timed(new FileTask<Long>("lastModified", file) {
+ @Override
+ public Long execute() {
+ return file.lastModified();
+ }
+ });
+ }
+
+ public static long length(final File file) {
+ return timed(new FileTask<Long>("length", file) {
+ @Override
+ public Long execute() {
+ return file.length();
+ }
+ });
+ }
+
+ /**
+ * Similar to {@link File#listFiles()}, but never returns null.
+ * Instead a warning is logged, and an empty array is returned.
+ */
+ public static File[] listFiles(final File dir) {
+ File[] files = timed(new FileTask<File[]>("listFiles", dir) {
+ @Override
+ public File[] execute() {
+ return dir.listFiles();
+ }
+ });
+
+ if (files == null) {
+ LOG.warn("Failed to list children for " + dir.getPath());
+ return new File[0];
+ }
+ return files;
+ }
+
+ /**
+ * Similar to {@link File#listFiles(FilenameFilter)}, but never returns null.
+ * Instead a warning is logged, and an empty array is returned.
+ */
+ public static File[] listFiles(final File dir, final FilenameFilter filter, boolean sort) {
+ File[] files = timed(new FileTask<File[]>("listFiles2", dir) {
+ @Override
+ public File[] execute() {
+ return dir.listFiles(filter);
+ }
+ });
+ if (files == null) {
+ LOG.warn("Failed to list children for " + dir.getPath());
+ return new File[0];
+ }
+ if (sort) {
+ Arrays.sort(files);
+ }
+ return files;
+ }
+
+ /**
+ * Returns a short path for the given file. The path consists of the name
+ * of the parent directory and the given file.
+ */
+ public static String getShortPath(File file) {
+ if (file == null) {
+ return null;
+ }
+ File parent = file.getParentFile();
+ if (parent == null) {
+ return file.getName();
+ }
+ return parent.getName() + File.separator + file.getName();
+ }
+
+ /**
+ * Closes the "closable", ignoring any excepetions.
+ *
+ * @param closeable The Closable to close, may be {@code null}.
+ */
+ public static void closeQuietly(Closeable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ // Ignored
+ }
+ }
+ }
+
+ private static <T> T timed(FileTask<T> task) {
+// long t0 = System.nanoTime();
+// try {
+ return task.execute();
+// } finally {
+// long t1 = System.nanoTime();
+// LOG.debug((t1 - t0) / 1000L + " microsec, " + task);
+// }
+ }
+
+ private abstract static class FileTask<T> {
+
+ private final String name;
+ private final File file;
+
+ public FileTask(String name, File file) {
+ this.name = name;
+ this.file = file;
+ }
+
+ public abstract T execute();
+
+ @Override
+ public String toString() {
+ return name + ", " + file;
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java
new file mode 100644
index 00000000..7edecaa2
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java
@@ -0,0 +1,54 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.util;
+
+import java.io.Serializable;
+
+/**
+ * @author Sindre Mehus
+ */
+public class Pair<S, T> implements Serializable {
+
+ private S first;
+ private T second;
+
+ public Pair() {
+ }
+
+ public Pair(S first, T second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public S getFirst() {
+ return first;
+ }
+
+ public void setFirst(S first) {
+ this.first = first;
+ }
+
+ public T getSecond() {
+ return second;
+ }
+
+ public void setSecond(T second) {
+ this.second = second;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java
new file mode 100644
index 00000000..ebad9fbf
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java
@@ -0,0 +1,537 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.math.LongRange;
+
+/**
+ * Miscellaneous string utility methods.
+ *
+ * @author Sindre Mehus
+ */
+public final class StringUtil {
+
+ public static final String ENCODING_LATIN = "ISO-8859-1";
+ public static final String ENCODING_UTF8 = "UTF-8";
+ private static final DateFormat ISO_8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+
+ private static final String[][] HTML_SUBSTITUTIONS = {
+ {"&", "&amp;"},
+ {"<", "&lt;"},
+ {">", "&gt;"},
+ {"'", "&#39;"},
+ {"\"", "&#34;"},
+ };
+
+ private static final String[][] MIME_TYPES = {
+ {"mp3", "audio/mpeg"},
+ {"ogg", "audio/ogg"},
+ {"oga", "audio/ogg"},
+ {"ogx", "application/ogg"},
+ {"aac", "audio/mp4"},
+ {"m4a", "audio/mp4"},
+ {"flac", "audio/flac"},
+ {"wav", "audio/x-wav"},
+ {"wma", "audio/x-ms-wma"},
+ {"ape", "audio/x-monkeys-audio"},
+ {"mpc", "audio/x-musepack"},
+ {"shn", "audio/x-shn"},
+
+ {"flv", "video/x-flv"},
+ {"avi", "video/avi"},
+ {"mpg", "video/mpeg"},
+ {"mpeg", "video/mpeg"},
+ {"mp4", "video/mp4"},
+ {"m4v", "video/x-m4v"},
+ {"mkv", "video/x-matroska"},
+ {"mov", "video/quicktime"},
+ {"wmv", "video/x-ms-wmv"},
+ {"ogv", "video/ogg"},
+ {"divx", "video/divx"},
+ {"m2ts", "video/MP2T"},
+
+ {"gif", "image/gif"},
+ {"jpg", "image/jpeg"},
+ {"jpeg", "image/jpeg"},
+ {"png", "image/png"},
+ {"bmp", "image/bmp"},
+ };
+
+ private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "|"};
+
+ /**
+ * Disallow external instantiation.
+ */
+ private StringUtil() {
+ }
+
+ /**
+ * Returns the specified string converted to a format suitable for
+ * HTML. All single-quote, double-quote, greater-than, less-than and
+ * ampersand characters are replaces with their corresponding HTML
+ * Character Entity code.
+ *
+ * @param s the string to convert
+ * @return the converted string
+ */
+ public static String toHtml(String s) {
+ if (s == null) {
+ return null;
+ }
+ for (String[] substitution : HTML_SUBSTITUTIONS) {
+ if (s.contains(substitution[0])) {
+ s = s.replaceAll(substitution[0], substitution[1]);
+ }
+ }
+ return s;
+ }
+
+
+ /**
+ * Formats the given date to a ISO-8601 date/time format, and UTC timezone.
+ * <p/>
+ * The returned date uses the following format: 2007-12-17T14:57:17
+ *
+ * @param date The date to format
+ * @return The corresponding ISO-8601 formatted string.
+ */
+ public static String toISO8601(Date date) {
+ if (date == null) {
+ return null;
+ }
+
+ synchronized (ISO_8601_DATE_FORMAT) {
+ return ISO_8601_DATE_FORMAT.format(date);
+ }
+ }
+
+ /**
+ * Removes the suffix (the substring after the last dot) of the given string. The dot is
+ * also removed.
+ *
+ * @param s The string in question, e.g., "foo.mp3".
+ * @return The string without the suffix, e.g., "foo".
+ */
+ public static String removeSuffix(String s) {
+ int index = s.lastIndexOf('.');
+ return index == -1 ? s : s.substring(0, index);
+ }
+
+ /**
+ * Returns the proper MIME type for the given suffix.
+ *
+ * @param suffix The suffix, e.g., "mp3" or ".mp3".
+ * @return The corresponding MIME type, e.g., "audio/mpeg". If no MIME type is found,
+ * <code>application/octet-stream</code> is returned.
+ */
+ public static String getMimeType(String suffix) {
+ for (String[] map : MIME_TYPES) {
+ if (map[0].equalsIgnoreCase(suffix) || ('.' + map[0]).equalsIgnoreCase(suffix)) {
+ return map[1];
+ }
+ }
+ return "application/octet-stream";
+ }
+
+ /**
+ * Converts a byte-count to a formatted string suitable for display to the user.
+ * For instance:
+ * <ul>
+ * <li><code>format(918)</code> returns <em>"918 B"</em>.</li>
+ * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li>
+ * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li>
+ * </ul>
+ * This method assumes that 1 KB is 1024 bytes.
+ *
+ * @param byteCount The number of bytes.
+ * @param locale The locale used for formatting.
+ * @return The formatted string.
+ */
+ public static synchronized String formatBytes(long byteCount, Locale locale) {
+
+ // More than 1 GB?
+ if (byteCount >= 1024 * 1024 * 1024) {
+ NumberFormat gigaByteFormat = new DecimalFormat("0.00 GB", new DecimalFormatSymbols(locale));
+ return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024));
+ }
+
+ // More than 1 MB?
+ if (byteCount >= 1024 * 1024) {
+ NumberFormat megaByteFormat = new DecimalFormat("0.0 MB", new DecimalFormatSymbols(locale));
+ return megaByteFormat.format((double) byteCount / (1024 * 1024));
+ }
+
+ // More than 1 KB?
+ if (byteCount >= 1024) {
+ NumberFormat kiloByteFormat = new DecimalFormat("0 KB", new DecimalFormatSymbols(locale));
+ return kiloByteFormat.format((double) byteCount / 1024);
+ }
+
+ return byteCount + " B";
+ }
+
+ /**
+ * Formats a duration with minutes and seconds, e.g., "93:45"
+ */
+ public static String formatDuration(int seconds) {
+ int minutes = seconds / 60;
+ int secs = seconds % 60;
+
+ StringBuilder builder = new StringBuilder(6);
+ builder.append(minutes).append(":");
+ if (secs < 10) {
+ builder.append("0");
+ }
+ builder.append(secs);
+ return builder.toString();
+ }
+
+ /**
+ * Splits the input string. White space is interpreted as separator token. Double quotes
+ * are interpreted as grouping operator. <br/>
+ * For instance, the input <code>"u2 rem "greatest hits""</code> will return an array with
+ * three elements: <code>{"u2", "rem", "greatest hits"}</code>
+ *
+ * @param input The input string.
+ * @return Array of elements.
+ */
+ public static String[] split(String input) {
+ if (input == null) {
+ return new String[0];
+ }
+
+ Pattern pattern = Pattern.compile("\".*?\"|\\S+");
+ Matcher matcher = pattern.matcher(input);
+
+ List<String> result = new ArrayList<String>();
+ while (matcher.find()) {
+ String element = matcher.group();
+ if (element.startsWith("\"") && element.endsWith("\"") && element.length() > 1) {
+ element = element.substring(1, element.length() - 1);
+ }
+ result.add(element);
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ /**
+ * Reads lines from the given input stream. All lines are trimmed. Empty lines and lines starting
+ * with "#" are skipped. The input stream is always closed by this method.
+ *
+ * @param in The input stream to read from.
+ * @return Array of lines.
+ * @throws IOException If an I/O error occurs.
+ */
+ public static String[] readLines(InputStream in) throws IOException {
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new InputStreamReader(in));
+ List<String> result = new ArrayList<String>();
+ for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+ line = line.trim();
+ if (!line.startsWith("#") && line.length() > 0) {
+ result.add(line);
+ }
+ }
+ return result.toArray(new String[result.size()]);
+
+ } finally {
+ IOUtils.closeQuietly(in);
+ IOUtils.closeQuietly(reader);
+ }
+ }
+
+ /**
+ * Converts the given string of whitespace-separated integers to an <code>int</code> array.
+ *
+ * @param s String consisting of integers separated by whitespace.
+ * @return The corresponding array of ints.
+ * @throws NumberFormatException If string contains non-parseable text.
+ */
+ public static int[] parseInts(String s) {
+ if (s == null) {
+ return new int[0];
+ }
+
+ String[] strings = StringUtils.split(s);
+ int[] ints = new int[strings.length];
+ for (int i = 0; i < strings.length; i++) {
+ ints[i] = Integer.parseInt(strings[i]);
+ }
+ return ints;
+ }
+
+ /**
+ * Change protocol from "https" to "http" for the given URL. The port number is also changed,
+ * but not if the given URL is already "http".
+ *
+ * @param url The original URL.
+ * @param port The port number to use, for instance 443.
+ * @return The transformed URL.
+ * @throws MalformedURLException If the original URL is invalid.
+ */
+ public static String toHttpUrl(String url, int port) throws MalformedURLException {
+ URL u = new URL(url);
+ if ("https".equals(u.getProtocol())) {
+ return new URL("http", u.getHost(), port, u.getFile()).toString();
+ }
+ return url;
+ }
+
+ /**
+ * Determines whether a is equal to b, taking null into account.
+ *
+ * @return Whether a and b are equal, or both null.
+ */
+ public static boolean isEqual(Object a, Object b) {
+ return a == null ? b == null : a.equals(b);
+ }
+
+ /**
+ * Parses a locale from the given string.
+ *
+ * @param s The locale string. Should be formatted as per the documentation in {@link Locale#toString()}.
+ * @return The locale.
+ */
+ public static Locale parseLocale(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ String[] elements = s.split("_");
+
+ if (elements.length == 0) {
+ return new Locale(s, "", "");
+ }
+ if (elements.length == 1) {
+ return new Locale(elements[0], "", "");
+ }
+ if (elements.length == 2) {
+ return new Locale(elements[0], elements[1], "");
+ }
+ return new Locale(elements[0], elements[1], elements[2]);
+ }
+
+ /**
+ * URL-encodes the input value using UTF-8.
+ */
+ public static String urlEncode(String s) {
+ try {
+ return URLEncoder.encode(s, StringUtil.ENCODING_UTF8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ }
+
+ /**
+ * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to encode.
+ * @return The encoded string.
+ */
+ public static String utf8HexEncode(String s) {
+ if (s == null) {
+ return null;
+ }
+ byte[] utf8;
+ try {
+ utf8 = s.getBytes(ENCODING_UTF8);
+ } catch (UnsupportedEncodingException x) {
+ throw new RuntimeException(x);
+ }
+ return String.valueOf(Hex.encodeHex(utf8));
+ }
+
+ /**
+ * Decodes the given string by using the hexadecimal representation of its UTF-8 bytes.
+ *
+ * @param s The string to decode.
+ * @return The decoded string.
+ * @throws Exception If an error occurs.
+ */
+ public static String utf8HexDecode(String s) throws Exception {
+ if (s == null) {
+ return null;
+ }
+ return new String(Hex.decodeHex(s.toCharArray()), ENCODING_UTF8);
+ }
+
+ /**
+ * Calculates the MD5 digest and returns the value as a 32 character hex string.
+ *
+ * @param s Data to digest.
+ * @return MD5 digest as a hex string.
+ */
+ public static String md5Hex(String s) {
+ if (s == null) {
+ return null;
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ return new String(Hex.encodeHex(md5.digest(s.getBytes(ENCODING_UTF8))));
+ } catch (Exception x) {
+ throw new RuntimeException(x.getMessage(), x);
+ }
+ }
+
+ /**
+ * Returns the file part of an URL. For instance:
+ * <p/>
+ * <code>
+ * getUrlFile("http://archive.ncsa.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html")
+ * </code>
+ * <p/>
+ * will return "url-primer.html".
+ *
+ * @param url The URL in question.
+ * @return The file part, or <code>null</code> if no file can be resolved.
+ */
+ public static String getUrlFile(String url) {
+ try {
+ String path = new URL(url).getPath();
+ if (StringUtils.isBlank(path) || path.endsWith("/")) {
+ return null;
+ }
+
+ File file = new File(path);
+ String filename = file.getName();
+ if (StringUtils.isBlank(filename)) {
+ return null;
+ }
+ return filename;
+
+ } catch (MalformedURLException x) {
+ return null;
+ }
+ }
+
+ /**
+ * Rewrites the URL by changing the protocol, host and port.
+ *
+ * @param urlToRewrite The URL to rewrite.
+ * @param urlWithProtocolHostAndPort Use protocol, host and port from this URL.
+ * @return The rewritten URL, or an unchanged URL if either argument is not a proper URL.
+ */
+ public static String rewriteUrl(String urlToRewrite, String urlWithProtocolHostAndPort) {
+ if (urlToRewrite == null) {
+ return null;
+ }
+
+ try {
+ URL urlA = new URL(urlToRewrite);
+ URL urlB = new URL(urlWithProtocolHostAndPort);
+
+ URL result = new URL(urlB.getProtocol(), urlB.getHost(), urlB.getPort(), urlA.getFile());
+ return result.toExternalForm();
+ } catch (MalformedURLException x) {
+ return urlToRewrite;
+ }
+ }
+
+ /**
+ * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
+ * with dashes ("-").
+ *
+ * @param filename The filename in question.
+ * @return The filename with special characters replaced by underscores.
+ */
+ public static String fileSystemSafe(String filename) {
+ for (String s : FILE_SYSTEM_UNSAFE) {
+ filename = filename.replace(s, "-");
+ }
+ return filename;
+ }
+
+ /**
+ * Parses the given string as a HTTP header byte range. See chapter 14.36.1 in RFC 2068
+ * for details.
+ * <p/>
+ * Only a subset of the allowed syntaxes are supported. Only ranges which specify first-byte-pos
+ * are supported. The last-byte-pos is optional.
+ *
+ * @param range The range from the HTTP header, for instance "bytes=0-499" or "bytes=500-"
+ * @return A range object (using inclusive values). If the last-byte-pos is not given, the end of
+ * the returned range is {@link Long#MAX_VALUE}. The method returns <code>null</code> if the syntax
+ * of the given range is not supported.
+ */
+ public static LongRange parseRange(String range) {
+ if (range == null) {
+ return null;
+ }
+
+ Pattern pattern = Pattern.compile("bytes=(\\d+)-(\\d*)");
+ Matcher matcher = pattern.matcher(range);
+
+ if (matcher.matches()) {
+ String firstString = matcher.group(1);
+ String lastString = StringUtils.trimToNull(matcher.group(2));
+
+ long first = Long.parseLong(firstString);
+ long last = lastString == null ? Long.MAX_VALUE : Long.parseLong(lastString);
+
+ if (first > last) {
+ return null;
+ }
+
+ return new LongRange(first, last);
+ }
+ return null;
+ }
+
+ public static String removeMarkup(String s) {
+ if (s == null) {
+ return null;
+ }
+ return s.replaceAll("<.*?>", "");
+ }
+
+ public static String getRESTProtocolVersion() {
+ // TODO: Read from xsd.
+ return "1.8.0";
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java
new file mode 100644
index 00000000..ec7175a2
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java
@@ -0,0 +1,127 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.util;
+
+import net.sourceforge.subsonic.Logger;
+
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.Inet4Address;
+import java.util.Enumeration;
+import java.util.Random;
+
+/**
+ * Miscellaneous general utility methods.
+ *
+ * @author Sindre Mehus
+ */
+public final class Util {
+
+ private static final Logger LOG = Logger.getLogger(Util.class);
+ private static final Random RANDOM = new Random(System.currentTimeMillis());
+
+ /**
+ * Disallow external instantiation.
+ */
+ private Util() {
+ }
+
+ public static String getDefaultMusicFolder() {
+ String def = isWindows() ? "c:\\music" : "/var/music";
+ return System.getProperty("subsonic.defaultMusicFolder", def);
+ }
+
+ public static String getDefaultPodcastFolder() {
+ String def = isWindows() ? "c:\\music\\Podcast" : "/var/music/Podcast";
+ return System.getProperty("subsonic.defaultPodcastFolder", def);
+ }
+
+ public static String getDefaultPlaylistFolder() {
+ String def = isWindows() ? "c:\\playlists" : "/var/playlists";
+ return System.getProperty("subsonic.defaultPlaylistFolder", def);
+ }
+
+ public static boolean isWindows() {
+ return System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows");
+ }
+
+ public static boolean isWindowsInstall() {
+ return "true".equals(System.getProperty("subsonic.windowsInstall"));
+ }
+
+ /**
+ * Similar to {@link ServletResponse#setContentLength(int)}, but this
+ * method supports lengths bigger than 2GB.
+ * <p/>
+ * See http://blogger.ziesemer.com/2008/03/suns-version-of-640k-2gb.html
+ *
+ * @param response The HTTP response.
+ * @param length The content length.
+ */
+ public static void setContentLength(HttpServletResponse response, long length) {
+ if (length <= Integer.MAX_VALUE) {
+ response.setContentLength((int) length);
+ } else {
+ response.setHeader("Content-Length", String.valueOf(length));
+ }
+ }
+
+ /**
+ * Returns the local IP address.
+ * @return The local IP, or the loopback address (127.0.0.1) if not found.
+ */
+ public static String getLocalIpAddress() {
+ try {
+
+ // Try the simple way first.
+ InetAddress address = InetAddress.getLocalHost();
+ if (!address.isLoopbackAddress()) {
+ return address.getHostAddress();
+ }
+
+ // Iterate through all network interfaces, looking for a suitable IP.
+ Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
+ while (interfaces.hasMoreElements()) {
+ NetworkInterface iface = interfaces.nextElement();
+ Enumeration<InetAddress> addresses = iface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress addr = addresses.nextElement();
+ if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) {
+ return addr.getHostAddress();
+ }
+ }
+ }
+
+ } catch (Throwable x) {
+ LOG.warn("Failed to resolve local IP address.", x);
+ }
+
+ return "127.0.0.1";
+ }
+
+ public static int randomInt(int min, int max) {
+ if (min >= max) {
+ return 0;
+ }
+ return min + RANDOM.nextInt(max - min);
+
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java
new file mode 100644
index 00000000..a572ac0f
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java
@@ -0,0 +1,328 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.util;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.XML;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Stack;
+
+
+/**
+ * Simplifies building of XML documents.
+ * <p/>
+ * <b>Example:</b><br/>
+ * The following code:
+ * <pre>
+ * XMLBuilder builder = XMLBuilder.createXMLBuilder();
+ * builder.add("foo").add("bar");
+ * builder.add("zonk", 42);
+ * builder.end().end();
+ * System.out.println(builder.toString());
+ * </pre>
+ * produces the following XML:
+ * <pre>
+ * &lt;foo&gt;
+ * &lt;bar&gt;
+ * &lt;zonk&gt;42&lt;/zonk&gt;
+ * &lt;/bar&gt;
+ * &lt;/foo&gt;
+ * </pre>
+ * This class is <em>not</em> thread safe.
+ * <p/>
+ * Also supports JSON and JSONP formats.
+ *
+ * @author Sindre Mehus
+ */
+public class XMLBuilder {
+
+ private static final String INDENTATION = " ";
+ private static final String NEWLINE = "\n";
+
+ private final Writer writer = new StringWriter();
+ private final Stack<String> elementStack = new Stack<String>();
+ private final boolean json;
+ private final String jsonpCallback;
+
+ public static XMLBuilder createXMLBuilder() {
+ return new XMLBuilder(false, null);
+ }
+
+ public static XMLBuilder createJSONBuilder() {
+ return new XMLBuilder(true, null);
+ }
+
+ public static XMLBuilder createJSONPBuilder(String callback) {
+ return new XMLBuilder(true, callback);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param json Whether to produce JSON rather than XML.
+ * @param jsonpCallback Name of javascript callback for JSONP.
+ */
+ private XMLBuilder(boolean json, String jsonpCallback) {
+ this.json = json;
+ this.jsonpCallback = jsonpCallback;
+ }
+
+ /**
+ * Adds an XML preamble, with the given encoding. The preamble will typically
+ * look like this:
+ * <p/>
+ * <code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;</code>
+ *
+ * @param encoding The encoding to put in the preamble.
+ * @return A reference to this object.
+ */
+ public XMLBuilder preamble(String encoding) throws IOException {
+ writer.write("<?xml version=\"1.0\" encoding=\"");
+ writer.write(encoding);
+ writer.write("\"?>");
+ newline();
+ return this;
+ }
+
+ /**
+ * Adds an element with the given name and a single attribute.
+ *
+ * @param element The element name.
+ * @param attributeKey The attributes key.
+ * @param attributeValue The attributes value.
+ * @param close Whether to close the element.
+ * @return A reference to this object.
+ */
+ public XMLBuilder add(String element, String attributeKey, Object attributeValue, boolean close) throws IOException {
+ return add(element, close, new Attribute(attributeKey, attributeValue));
+ }
+
+ /**
+ * Adds an element with the given name and attributes.
+ *
+ * @param element The element name.
+ * @param close Whether to close the element.
+ * @param attributes The element attributes.
+ * @return A reference to this object.
+ */
+ public XMLBuilder add(String element, boolean close, Attribute... attributes) throws IOException {
+ return add(element, Arrays.asList(attributes), close);
+ }
+
+ /**
+ * Adds an element with the given name and attributes.
+ *
+ * @param element The element name.
+ * @param attributes The element attributes.
+ * @param close Whether to close the element.
+ * @return A reference to this object.
+ */
+ public XMLBuilder add(String element, Iterable<Attribute> attributes, boolean close) throws IOException {
+ return add(element, attributes, null, close);
+ }
+
+ /**
+ * Adds an element with the given name, attributes and character data.
+ *
+ * @param element The element name.
+ * @param attributes The element attributes.
+ * @param text The character data.
+ * @param close Whether to close the element.
+ * @return A reference to this object.
+ */
+ public XMLBuilder add(String element, Iterable<Attribute> attributes, String text, boolean close) throws IOException {
+ indent();
+ elementStack.push(element);
+ writer.write('<');
+ writer.write(element);
+
+ if (attributes == null) {
+ attributes = Collections.emptyList();
+ }
+
+ Iterator<Attribute> iterator = attributes.iterator();
+
+ if (iterator.hasNext()) {
+ writer.write(' ');
+ }
+ while (iterator.hasNext()) {
+ Attribute attribute = iterator.next();
+ attribute.append(writer);
+ if (iterator.hasNext()) {
+ writer.write(' ');
+ }
+ }
+
+ if (close && text == null) {
+ elementStack.pop();
+ writer.write("/>");
+ } else {
+ writer.write('>');
+ }
+
+ if (text != null) {
+ writer.write(text);
+
+ if (close) {
+ elementStack.pop();
+ writer.write("</");
+ writer.write(element);
+ writer.write('>');
+ }
+ }
+
+ newline();
+ return this;
+ }
+
+ /**
+ * Closes the current element.
+ *
+ * @return A reference to this object.
+ * @throws IllegalStateException If there are no unclosed elements.
+ */
+ public XMLBuilder end() throws IllegalStateException, IOException {
+ if (elementStack.isEmpty()) {
+ throw new IllegalStateException("There are no unclosed elements.");
+ }
+
+ String element = elementStack.pop();
+ indent();
+ writer.write("</");
+ writer.write(element);
+ writer.write('>');
+ newline();
+ return this;
+ }
+
+ /**
+ * Closes all unclosed elements.
+ *
+ * @return A reference to this object.
+ */
+ public XMLBuilder endAll() throws IOException {
+ while (!elementStack.isEmpty()) {
+ end();
+ }
+ return this;
+ }
+
+ /**
+ * Returns the XML document as a string.
+ */
+ @Override
+ public String toString() {
+ String xml = writer.toString();
+ if (!json) {
+ return xml;
+ }
+ try {
+ JSONObject jsonObject = XML.toJSONObject(xml);
+
+ if (jsonpCallback != null) {
+ return jsonpCallback + "(" + jsonObject.toString(1) + ");";
+ }
+
+ return jsonObject.toString(1);
+ } catch (JSONException x) {
+ throw new RuntimeException("Failed to convert from XML to JSON.", x);
+ }
+ }
+
+ private void indent() throws IOException {
+ int depth = elementStack.size();
+ for (int i = 0; i < depth; i++) {
+ writer.write(INDENTATION);
+ }
+ }
+
+ private void newline() throws IOException {
+ writer.write(NEWLINE);
+ }
+
+ /**
+ * An XML element attribute.
+ */
+ public static class Attribute {
+
+ private final String key;
+ private final Object value;
+
+ public Attribute(String key, Object value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ private void append(Writer writer) throws IOException {
+ if (key != null && value != null) {
+ writer.write(key);
+ writer.write("=\"");
+ writer.write(StringEscapeUtils.escapeXml(value.toString()));
+ writer.write("\"");
+ }
+ }
+ }
+
+ /**
+ * A set of attributes.
+ */
+ public static class AttributeSet implements Iterable<Attribute> {
+
+ private final Map<String, Attribute> attributes = new LinkedHashMap<String, Attribute>();
+
+ public void add(Attribute attribute) {
+ attributes.put(attribute.getKey(), attribute);
+ }
+
+ public void add(String key, Object value) {
+ if (key != null && value != null) {
+ add(new Attribute(key, value));
+ }
+ }
+
+ public void addAll(Iterable<Attribute> attributes) {
+ for (Attribute attribute : attributes) {
+ add(attribute);
+ }
+ }
+
+ public Iterator<Attribute> iterator() {
+ return attributes.values().iterator();
+ }
+ }
+
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java
new file mode 100644
index 00000000..276eacb0
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java
@@ -0,0 +1,51 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.validator;
+
+import org.springframework.validation.Validator;
+import org.springframework.validation.Errors;
+import net.sourceforge.subsonic.command.PasswordSettingsCommand;
+import net.sourceforge.subsonic.command.DonateCommand;
+import net.sourceforge.subsonic.controller.DonateController;
+import net.sourceforge.subsonic.service.SettingsService;
+
+/**
+ * Validator for {@link DonateController}.
+ *
+ * @author Sindre Mehus
+ */
+public class DonateValidator implements Validator {
+ private SettingsService settingsService;
+
+ public boolean supports(Class clazz) {
+ return clazz.equals(DonateCommand.class);
+ }
+
+ public void validate(Object obj, Errors errors) {
+ DonateCommand command = (DonateCommand) obj;
+
+ if (!settingsService.isLicenseValid(command.getEmailAddress(), command.getLicense())) {
+ errors.rejectValue("license", "donate.invalidlicense");
+ }
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java
new file mode 100644
index 00000000..12fb06ce
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java
@@ -0,0 +1,45 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.validator;
+
+import org.springframework.validation.*;
+import net.sourceforge.subsonic.command.*;
+import net.sourceforge.subsonic.controller.*;
+
+/**
+ * Validator for {@link PasswordSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class PasswordSettingsValidator implements Validator {
+
+ public boolean supports(Class clazz) {
+ return clazz.equals(PasswordSettingsCommand.class);
+ }
+
+ public void validate(Object obj, Errors errors) {
+ PasswordSettingsCommand command = (PasswordSettingsCommand) obj;
+
+ if (command.getPassword() == null || command.getPassword().length() == 0) {
+ errors.rejectValue("password", "usersettings.nopassword");
+ } else if (!command.getPassword().equals(command.getConfirmPassword())) {
+ errors.rejectValue("password", "usersettings.wrongpassword");
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.java
new file mode 100644
index 00000000..3445b7d8
--- /dev/null
+++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.java
@@ -0,0 +1,91 @@
+/*
+ This file is part of Subsonic.
+
+ Subsonic is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Subsonic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
+
+ Copyright 2009 (C) Sindre Mehus
+ */
+package net.sourceforge.subsonic.validator;
+
+import net.sourceforge.subsonic.command.UserSettingsCommand;
+import net.sourceforge.subsonic.controller.UserSettingsController;
+import net.sourceforge.subsonic.service.SecurityService;
+import net.sourceforge.subsonic.service.SettingsService;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.validation.Errors;
+import org.springframework.validation.Validator;
+
+/**
+ * Validator for {@link UserSettingsController}.
+ *
+ * @author Sindre Mehus
+ */
+public class UserSettingsValidator implements Validator {
+
+ private SecurityService securityService;
+ private SettingsService settingsService;
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean supports(Class clazz) {
+ return clazz.equals(UserSettingsCommand.class);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void validate(Object obj, Errors errors) {
+ UserSettingsCommand command = (UserSettingsCommand) obj;
+ String username = command.getUsername();
+ String email = StringUtils.trimToNull(command.getEmail());
+ String password = StringUtils.trimToNull(command.getPassword());
+ String confirmPassword = command.getConfirmPassword();
+
+ if (command.isNew()) {
+ if (username == null || username.length() == 0) {
+ errors.rejectValue("username", "usersettings.nousername");
+ } else if (securityService.getUserByName(username) != null) {
+ errors.rejectValue("username", "usersettings.useralreadyexists");
+ } else if (email == null) {
+ errors.rejectValue("email", "usersettings.noemail");
+ } else if (command.isLdapAuthenticated() && !settingsService.isLdapEnabled()) {
+ errors.rejectValue("password", "usersettings.ldapdisabled");
+ } else if (command.isLdapAuthenticated() && password != null) {
+ errors.rejectValue("password", "usersettings.passwordnotsupportedforldap");
+ }
+ }
+
+ if ((command.isNew() || command.isPasswordChange()) && !command.isLdapAuthenticated()) {
+ if (password == null) {
+ errors.rejectValue("password", "usersettings.nopassword");
+ } else if (!password.equals(confirmPassword)) {
+ errors.rejectValue("password", "usersettings.wrongpassword");
+ }
+ }
+
+ if (command.isPasswordChange() && command.isLdapAuthenticated()) {
+ errors.rejectValue("password", "usersettings.passwordnotsupportedforldap");
+ }
+
+ }
+
+ public void setSecurityService(SecurityService securityService) {
+ this.securityService = securityService;
+ }
+
+ public void setSettingsService(SettingsService settingsService) {
+ this.settingsService = settingsService;
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/org/json/CDL.java b/subsonic-main/src/main/java/org/json/CDL.java
new file mode 100644
index 00000000..a1885aad
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/CDL.java
@@ -0,0 +1,279 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * This provides static methods to convert comma delimited text into a
+ * JSONArray, and to covert a JSONArray into comma delimited text. Comma
+ * delimited text is a very popular format for data interchange. It is
+ * understood by most database, spreadsheet, and organizer programs.
+ * <p>
+ * Each row of text represents a row in a table or a data record. Each row
+ * ends with a NEWLINE character. Each row contains one or more values.
+ * Values are separated by commas. A value can contain any character except
+ * for comma, unless is is wrapped in single quotes or double quotes.
+ * <p>
+ * The first row usually contains the names of the columns.
+ * <p>
+ * A comma delimited list can be converted into a JSONArray of JSONObjects.
+ * The names for the elements in the JSONObjects can be taken from the names
+ * in the first row.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class CDL {
+
+ /**
+ * Get the next value. The value can be wrapped in quotes. The value can
+ * be empty.
+ * @param x A JSONTokener of the source text.
+ * @return The value string, or null if empty.
+ * @throws JSONException if the quoted string is badly formed.
+ */
+ private static String getValue(JSONTokener x) throws JSONException {
+ char c;
+ char q;
+ StringBuffer sb;
+ do {
+ c = x.next();
+ } while (c == ' ' || c == '\t');
+ switch (c) {
+ case 0:
+ return null;
+ case '"':
+ case '\'':
+ q = c;
+ sb = new StringBuffer();
+ for (;;) {
+ c = x.next();
+ if (c == q) {
+ break;
+ }
+ if (c == 0 || c == '\n' || c == '\r') {
+ throw x.syntaxError("Missing close quote '" + q + "'.");
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ case ',':
+ x.back();
+ return "";
+ default:
+ x.back();
+ return x.nextTo(',');
+ }
+ }
+
+ /**
+ * Produce a JSONArray of strings from a row of comma delimited values.
+ * @param x A JSONTokener of the source text.
+ * @return A JSONArray of strings.
+ * @throws JSONException
+ */
+ public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException {
+ JSONArray ja = new JSONArray();
+ for (;;) {
+ String value = getValue(x);
+ char c = x.next();
+ if (value == null ||
+ (ja.length() == 0 && value.length() == 0 && c != ',')) {
+ return null;
+ }
+ ja.put(value);
+ for (;;) {
+ if (c == ',') {
+ break;
+ }
+ if (c != ' ') {
+ if (c == '\n' || c == '\r' || c == 0) {
+ return ja;
+ }
+ throw x.syntaxError("Bad character '" + c + "' (" +
+ (int)c + ").");
+ }
+ c = x.next();
+ }
+ }
+ }
+
+ /**
+ * Produce a JSONObject from a row of comma delimited text, using a
+ * parallel JSONArray of strings to provides the names of the elements.
+ * @param names A JSONArray of names. This is commonly obtained from the
+ * first row of a comma delimited text file using the rowToJSONArray
+ * method.
+ * @param x A JSONTokener of the source text.
+ * @return A JSONObject combining the names and values.
+ * @throws JSONException
+ */
+ public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x)
+ throws JSONException {
+ JSONArray ja = rowToJSONArray(x);
+ return ja != null ? ja.toJSONObject(names) : null;
+ }
+
+ /**
+ * Produce a comma delimited text row from a JSONArray. Values containing
+ * the comma character will be quoted. Troublesome characters may be
+ * removed.
+ * @param ja A JSONArray of strings.
+ * @return A string ending in NEWLINE.
+ */
+ public static String rowToString(JSONArray ja) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < ja.length(); i += 1) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ Object object = ja.opt(i);
+ if (object != null) {
+ String string = object.toString();
+ if (string.length() > 0 && (string.indexOf(',') >= 0 ||
+ string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 ||
+ string.indexOf(0) >= 0 || string.charAt(0) == '"')) {
+ sb.append('"');
+ int length = string.length();
+ for (int j = 0; j < length; j += 1) {
+ char c = string.charAt(j);
+ if (c >= ' ' && c != '"') {
+ sb.append(c);
+ }
+ }
+ sb.append('"');
+ } else {
+ sb.append(string);
+ }
+ }
+ }
+ sb.append('\n');
+ return sb.toString();
+ }
+
+ /**
+ * Produce a JSONArray of JSONObjects from a comma delimited text string,
+ * using the first row as a source of names.
+ * @param string The comma delimited text.
+ * @return A JSONArray of JSONObjects.
+ * @throws JSONException
+ */
+ public static JSONArray toJSONArray(String string) throws JSONException {
+ return toJSONArray(new JSONTokener(string));
+ }
+
+ /**
+ * Produce a JSONArray of JSONObjects from a comma delimited text string,
+ * using the first row as a source of names.
+ * @param x The JSONTokener containing the comma delimited text.
+ * @return A JSONArray of JSONObjects.
+ * @throws JSONException
+ */
+ public static JSONArray toJSONArray(JSONTokener x) throws JSONException {
+ return toJSONArray(rowToJSONArray(x), x);
+ }
+
+ /**
+ * Produce a JSONArray of JSONObjects from a comma delimited text string
+ * using a supplied JSONArray as the source of element names.
+ * @param names A JSONArray of strings.
+ * @param string The comma delimited text.
+ * @return A JSONArray of JSONObjects.
+ * @throws JSONException
+ */
+ public static JSONArray toJSONArray(JSONArray names, String string)
+ throws JSONException {
+ return toJSONArray(names, new JSONTokener(string));
+ }
+
+ /**
+ * Produce a JSONArray of JSONObjects from a comma delimited text string
+ * using a supplied JSONArray as the source of element names.
+ * @param names A JSONArray of strings.
+ * @param x A JSONTokener of the source text.
+ * @return A JSONArray of JSONObjects.
+ * @throws JSONException
+ */
+ public static JSONArray toJSONArray(JSONArray names, JSONTokener x)
+ throws JSONException {
+ if (names == null || names.length() == 0) {
+ return null;
+ }
+ JSONArray ja = new JSONArray();
+ for (;;) {
+ JSONObject jo = rowToJSONObject(names, x);
+ if (jo == null) {
+ break;
+ }
+ ja.put(jo);
+ }
+ if (ja.length() == 0) {
+ return null;
+ }
+ return ja;
+ }
+
+
+ /**
+ * Produce a comma delimited text from a JSONArray of JSONObjects. The
+ * first row will be a list of names obtained by inspecting the first
+ * JSONObject.
+ * @param ja A JSONArray of JSONObjects.
+ * @return A comma delimited text.
+ * @throws JSONException
+ */
+ public static String toString(JSONArray ja) throws JSONException {
+ JSONObject jo = ja.optJSONObject(0);
+ if (jo != null) {
+ JSONArray names = jo.names();
+ if (names != null) {
+ return rowToString(names) + toString(names, ja);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Produce a comma delimited text from a JSONArray of JSONObjects using
+ * a provided list of names. The list of names is not included in the
+ * output.
+ * @param names A JSONArray of strings.
+ * @param ja A JSONArray of JSONObjects.
+ * @return A comma delimited text.
+ * @throws JSONException
+ */
+ public static String toString(JSONArray names, JSONArray ja)
+ throws JSONException {
+ if (names == null || names.length() == 0) {
+ return null;
+ }
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < ja.length(); i += 1) {
+ JSONObject jo = ja.optJSONObject(i);
+ if (jo != null) {
+ sb.append(rowToString(jo.toJSONArray(names)));
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/Cookie.java b/subsonic-main/src/main/java/org/json/Cookie.java
new file mode 100644
index 00000000..a2d9c4ed
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/Cookie.java
@@ -0,0 +1,169 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * Convert a web browser cookie specification to a JSONObject and back.
+ * JSON and Cookies are both notations for name/value pairs.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class Cookie {
+
+ /**
+ * Produce a copy of a string in which the characters '+', '%', '=', ';'
+ * and control characters are replaced with "%hh". This is a gentle form
+ * of URL encoding, attempting to cause as little distortion to the
+ * string as possible. The characters '=' and ';' are meta characters in
+ * cookies. By convention, they are escaped using the URL-encoding. This is
+ * only a convention, not a standard. Often, cookies are expected to have
+ * encoded values. We encode '=' and ';' because we must. We encode '%' and
+ * '+' because they are meta characters in URL encoding.
+ * @param string The source string.
+ * @return The escaped result.
+ */
+ public static String escape(String string) {
+ char c;
+ String s = string.trim();
+ StringBuffer sb = new StringBuffer();
+ int length = s.length();
+ for (int i = 0; i < length; i += 1) {
+ c = s.charAt(i);
+ if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') {
+ sb.append('%');
+ sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16));
+ sb.append(Character.forDigit((char)(c & 0x0f), 16));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Convert a cookie specification string into a JSONObject. The string
+ * will contain a name value pair separated by '='. The name and the value
+ * will be unescaped, possibly converting '+' and '%' sequences. The
+ * cookie properties may follow, separated by ';', also represented as
+ * name=value (except the secure property, which does not have a value).
+ * The name will be stored under the key "name", and the value will be
+ * stored under the key "value". This method does not do checking or
+ * validation of the parameters. It only converts the cookie string into
+ * a JSONObject.
+ * @param string The cookie specification string.
+ * @return A JSONObject containing "name", "value", and possibly other
+ * members.
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(String string) throws JSONException {
+ String name;
+ JSONObject jo = new JSONObject();
+ Object value;
+ JSONTokener x = new JSONTokener(string);
+ jo.put("name", x.nextTo('='));
+ x.next('=');
+ jo.put("value", x.nextTo(';'));
+ x.next();
+ while (x.more()) {
+ name = unescape(x.nextTo("=;"));
+ if (x.next() != '=') {
+ if (name.equals("secure")) {
+ value = Boolean.TRUE;
+ } else {
+ throw x.syntaxError("Missing '=' in cookie parameter.");
+ }
+ } else {
+ value = unescape(x.nextTo(';'));
+ x.next();
+ }
+ jo.put(name, value);
+ }
+ return jo;
+ }
+
+
+ /**
+ * Convert a JSONObject into a cookie specification string. The JSONObject
+ * must contain "name" and "value" members.
+ * If the JSONObject contains "expires", "domain", "path", or "secure"
+ * members, they will be appended to the cookie specification string.
+ * All other members are ignored.
+ * @param jo A JSONObject
+ * @return A cookie specification string
+ * @throws JSONException
+ */
+ public static String toString(JSONObject jo) throws JSONException {
+ StringBuffer sb = new StringBuffer();
+
+ sb.append(escape(jo.getString("name")));
+ sb.append("=");
+ sb.append(escape(jo.getString("value")));
+ if (jo.has("expires")) {
+ sb.append(";expires=");
+ sb.append(jo.getString("expires"));
+ }
+ if (jo.has("domain")) {
+ sb.append(";domain=");
+ sb.append(escape(jo.getString("domain")));
+ }
+ if (jo.has("path")) {
+ sb.append(";path=");
+ sb.append(escape(jo.getString("path")));
+ }
+ if (jo.optBoolean("secure")) {
+ sb.append(";secure");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Convert <code>%</code><i>hh</i> sequences to single characters, and
+ * convert plus to space.
+ * @param string A string that may contain
+ * <code>+</code>&nbsp;<small>(plus)</small> and
+ * <code>%</code><i>hh</i> sequences.
+ * @return The unescaped string.
+ */
+ public static String unescape(String string) {
+ int length = string.length();
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < length; ++i) {
+ char c = string.charAt(i);
+ if (c == '+') {
+ c = ' ';
+ } else if (c == '%' && i + 2 < length) {
+ int d = JSONTokener.dehexchar(string.charAt(i + 1));
+ int e = JSONTokener.dehexchar(string.charAt(i + 2));
+ if (d >= 0 && e >= 0) {
+ c = (char)(d * 16 + e);
+ i += 2;
+ }
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/CookieList.java b/subsonic-main/src/main/java/org/json/CookieList.java
new file mode 100644
index 00000000..1111135f
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/CookieList.java
@@ -0,0 +1,90 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.util.Iterator;
+
+/**
+ * Convert a web browser cookie list string to a JSONObject and back.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class CookieList {
+
+ /**
+ * Convert a cookie list into a JSONObject. A cookie list is a sequence
+ * of name/value pairs. The names are separated from the values by '='.
+ * The pairs are separated by ';'. The names and the values
+ * will be unescaped, possibly converting '+' and '%' sequences.
+ *
+ * To add a cookie to a cooklist,
+ * cookielistJSONObject.put(cookieJSONObject.getString("name"),
+ * cookieJSONObject.getString("value"));
+ * @param string A cookie list string
+ * @return A JSONObject
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(String string) throws JSONException {
+ JSONObject jo = new JSONObject();
+ JSONTokener x = new JSONTokener(string);
+ while (x.more()) {
+ String name = Cookie.unescape(x.nextTo('='));
+ x.next('=');
+ jo.put(name, Cookie.unescape(x.nextTo(';')));
+ x.next();
+ }
+ return jo;
+ }
+
+
+ /**
+ * Convert a JSONObject into a cookie list. A cookie list is a sequence
+ * of name/value pairs. The names are separated from the values by '='.
+ * The pairs are separated by ';'. The characters '%', '+', '=', and ';'
+ * in the names and values are replaced by "%hh".
+ * @param jo A JSONObject
+ * @return A cookie list string
+ * @throws JSONException
+ */
+ public static String toString(JSONObject jo) throws JSONException {
+ boolean b = false;
+ Iterator keys = jo.keys();
+ String string;
+ StringBuffer sb = new StringBuffer();
+ while (keys.hasNext()) {
+ string = keys.next().toString();
+ if (!jo.isNull(string)) {
+ if (b) {
+ sb.append(';');
+ }
+ sb.append(Cookie.escape(string));
+ sb.append("=");
+ sb.append(Cookie.escape(jo.getString(string)));
+ b = true;
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/HTTP.java b/subsonic-main/src/main/java/org/json/HTTP.java
new file mode 100644
index 00000000..cc8203d1
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/HTTP.java
@@ -0,0 +1,163 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.util.Iterator;
+
+/**
+ * Convert an HTTP header to a JSONObject and back.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class HTTP {
+
+ /** Carriage return/line feed. */
+ public static final String CRLF = "\r\n";
+
+ /**
+ * Convert an HTTP header string into a JSONObject. It can be a request
+ * header or a response header. A request header will contain
+ * <pre>{
+ * Method: "POST" (for example),
+ * "Request-URI": "/" (for example),
+ * "HTTP-Version": "HTTP/1.1" (for example)
+ * }</pre>
+ * A response header will contain
+ * <pre>{
+ * "HTTP-Version": "HTTP/1.1" (for example),
+ * "Status-Code": "200" (for example),
+ * "Reason-Phrase": "OK" (for example)
+ * }</pre>
+ * In addition, the other parameters in the header will be captured, using
+ * the HTTP field names as JSON names, so that <pre>
+ * Date: Sun, 26 May 2002 18:06:04 GMT
+ * Cookie: Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s
+ * Cache-Control: no-cache</pre>
+ * become
+ * <pre>{...
+ * Date: "Sun, 26 May 2002 18:06:04 GMT",
+ * Cookie: "Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s",
+ * "Cache-Control": "no-cache",
+ * ...}</pre>
+ * It does no further checking or conversion. It does not parse dates.
+ * It does not do '%' transforms on URLs.
+ * @param string An HTTP header string.
+ * @return A JSONObject containing the elements and attributes
+ * of the XML string.
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(String string) throws JSONException {
+ JSONObject jo = new JSONObject();
+ HTTPTokener x = new HTTPTokener(string);
+ String token;
+
+ token = x.nextToken();
+ if (token.toUpperCase().startsWith("HTTP")) {
+
+// Response
+
+ jo.put("HTTP-Version", token);
+ jo.put("Status-Code", x.nextToken());
+ jo.put("Reason-Phrase", x.nextTo('\0'));
+ x.next();
+
+ } else {
+
+// Request
+
+ jo.put("Method", token);
+ jo.put("Request-URI", x.nextToken());
+ jo.put("HTTP-Version", x.nextToken());
+ }
+
+// Fields
+
+ while (x.more()) {
+ String name = x.nextTo(':');
+ x.next(':');
+ jo.put(name, x.nextTo('\0'));
+ x.next();
+ }
+ return jo;
+ }
+
+
+ /**
+ * Convert a JSONObject into an HTTP header. A request header must contain
+ * <pre>{
+ * Method: "POST" (for example),
+ * "Request-URI": "/" (for example),
+ * "HTTP-Version": "HTTP/1.1" (for example)
+ * }</pre>
+ * A response header must contain
+ * <pre>{
+ * "HTTP-Version": "HTTP/1.1" (for example),
+ * "Status-Code": "200" (for example),
+ * "Reason-Phrase": "OK" (for example)
+ * }</pre>
+ * Any other members of the JSONObject will be output as HTTP fields.
+ * The result will end with two CRLF pairs.
+ * @param jo A JSONObject
+ * @return An HTTP header string.
+ * @throws JSONException if the object does not contain enough
+ * information.
+ */
+ public static String toString(JSONObject jo) throws JSONException {
+ Iterator keys = jo.keys();
+ String string;
+ StringBuffer sb = new StringBuffer();
+ if (jo.has("Status-Code") && jo.has("Reason-Phrase")) {
+ sb.append(jo.getString("HTTP-Version"));
+ sb.append(' ');
+ sb.append(jo.getString("Status-Code"));
+ sb.append(' ');
+ sb.append(jo.getString("Reason-Phrase"));
+ } else if (jo.has("Method") && jo.has("Request-URI")) {
+ sb.append(jo.getString("Method"));
+ sb.append(' ');
+ sb.append('"');
+ sb.append(jo.getString("Request-URI"));
+ sb.append('"');
+ sb.append(' ');
+ sb.append(jo.getString("HTTP-Version"));
+ } else {
+ throw new JSONException("Not enough material for an HTTP header.");
+ }
+ sb.append(CRLF);
+ while (keys.hasNext()) {
+ string = keys.next().toString();
+ if (!"HTTP-Version".equals(string) && !"Status-Code".equals(string) &&
+ !"Reason-Phrase".equals(string) && !"Method".equals(string) &&
+ !"Request-URI".equals(string) && !jo.isNull(string)) {
+ sb.append(string);
+ sb.append(": ");
+ sb.append(jo.getString(string));
+ sb.append(CRLF);
+ }
+ }
+ sb.append(CRLF);
+ return sb.toString();
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/HTTPTokener.java b/subsonic-main/src/main/java/org/json/HTTPTokener.java
new file mode 100644
index 00000000..86fed61d
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/HTTPTokener.java
@@ -0,0 +1,77 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * The HTTPTokener extends the JSONTokener to provide additional methods
+ * for the parsing of HTTP headers.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class HTTPTokener extends JSONTokener {
+
+ /**
+ * Construct an HTTPTokener from a string.
+ * @param string A source string.
+ */
+ public HTTPTokener(String string) {
+ super(string);
+ }
+
+
+ /**
+ * Get the next token or string. This is used in parsing HTTP headers.
+ * @throws JSONException
+ * @return A String.
+ */
+ public String nextToken() throws JSONException {
+ char c;
+ char q;
+ StringBuffer sb = new StringBuffer();
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ if (c == '"' || c == '\'') {
+ q = c;
+ for (;;) {
+ c = next();
+ if (c < ' ') {
+ throw syntaxError("Unterminated string.");
+ }
+ if (c == q) {
+ return sb.toString();
+ }
+ sb.append(c);
+ }
+ }
+ for (;;) {
+ if (c == 0 || Character.isWhitespace(c)) {
+ return sb.toString();
+ }
+ sb.append(c);
+ c = next();
+ }
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/JSONArray.java b/subsonic-main/src/main/java/org/json/JSONArray.java
new file mode 100644
index 00000000..4ae610f0
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONArray.java
@@ -0,0 +1,920 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A JSONArray is an ordered sequence of values. Its external text form is a
+ * string wrapped in square brackets with commas separating the values. The
+ * internal form is an object having <code>get</code> and <code>opt</code>
+ * methods for accessing the values by index, and <code>put</code> methods for
+ * adding or replacing values. The values can be any of these types:
+ * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>,
+ * <code>Number</code>, <code>String</code>, or the
+ * <code>JSONObject.NULL object</code>.
+ * <p>
+ * The constructor can convert a JSON text into a Java object. The
+ * <code>toString</code> method converts to JSON text.
+ * <p>
+ * A <code>get</code> method returns a value if one can be found, and throws an
+ * exception if one cannot be found. An <code>opt</code> method returns a
+ * default value instead of throwing an exception, and so is useful for
+ * obtaining optional values.
+ * <p>
+ * The generic <code>get()</code> and <code>opt()</code> methods return an
+ * object which you can cast or query for type. There are also typed
+ * <code>get</code> and <code>opt</code> methods that do type checking and type
+ * coercion for you.
+ * <p>
+ * The texts produced by the <code>toString</code> methods strictly conform to
+ * JSON syntax rules. The constructors are more forgiving in the texts they will
+ * accept:
+ * <ul>
+ * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
+ * before the closing bracket.</li>
+ * <li>The <code>null</code> value will be inserted when there
+ * is <code>,</code>&nbsp;<small>(comma)</small> elision.</li>
+ * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single
+ * quote)</small>.</li>
+ * <li>Strings do not need to be quoted at all if they do not begin with a quote
+ * or single quote, and if they do not contain leading or trailing spaces,
+ * and if they do not contain any of these characters:
+ * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers
+ * and if they are not the reserved words <code>true</code>,
+ * <code>false</code>, or <code>null</code>.</li>
+ * <li>Values can be separated by <code>;</code> <small>(semicolon)</small> as
+ * well as by <code>,</code> <small>(comma)</small>.</li>
+ * </ul>
+
+ * @author JSON.org
+ * @version 2011-12-19
+ */
+public class JSONArray {
+
+
+ /**
+ * The arrayList where the JSONArray's properties are kept.
+ */
+ private final ArrayList myArrayList;
+
+
+ /**
+ * Construct an empty JSONArray.
+ */
+ public JSONArray() {
+ this.myArrayList = new ArrayList();
+ }
+
+ /**
+ * Construct a JSONArray from a JSONTokener.
+ * @param x A JSONTokener
+ * @throws JSONException If there is a syntax error.
+ */
+ public JSONArray(JSONTokener x) throws JSONException {
+ this();
+ if (x.nextClean() != '[') {
+ throw x.syntaxError("A JSONArray text must start with '['");
+ }
+ if (x.nextClean() != ']') {
+ x.back();
+ for (;;) {
+ if (x.nextClean() == ',') {
+ x.back();
+ this.myArrayList.add(JSONObject.NULL);
+ } else {
+ x.back();
+ this.myArrayList.add(x.nextValue());
+ }
+ switch (x.nextClean()) {
+ case ';':
+ case ',':
+ if (x.nextClean() == ']') {
+ return;
+ }
+ x.back();
+ break;
+ case ']':
+ return;
+ default:
+ throw x.syntaxError("Expected a ',' or ']'");
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Construct a JSONArray from a source JSON text.
+ * @param source A string that begins with
+ * <code>[</code>&nbsp;<small>(left bracket)</small>
+ * and ends with <code>]</code>&nbsp;<small>(right bracket)</small>.
+ * @throws JSONException If there is a syntax error.
+ */
+ public JSONArray(String source) throws JSONException {
+ this(new JSONTokener(source));
+ }
+
+
+ /**
+ * Construct a JSONArray from a Collection.
+ * @param collection A Collection.
+ */
+ public JSONArray(Collection collection) {
+ this.myArrayList = new ArrayList();
+ if (collection != null) {
+ Iterator iter = collection.iterator();
+ while (iter.hasNext()) {
+ this.myArrayList.add(JSONObject.wrap(iter.next()));
+ }
+ }
+ }
+
+
+ /**
+ * Construct a JSONArray from an array
+ * @throws JSONException If not an array.
+ */
+ public JSONArray(Object array) throws JSONException {
+ this();
+ if (array.getClass().isArray()) {
+ int length = Array.getLength(array);
+ for (int i = 0; i < length; i += 1) {
+ this.put(JSONObject.wrap(Array.get(array, i)));
+ }
+ } else {
+ throw new JSONException(
+"JSONArray initial value should be a string or collection or array.");
+ }
+ }
+
+
+ /**
+ * Get the object value associated with an index.
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return An object value.
+ * @throws JSONException If there is no value for the index.
+ */
+ public Object get(int index) throws JSONException {
+ Object object = this.opt(index);
+ if (object == null) {
+ throw new JSONException("JSONArray[" + index + "] not found.");
+ }
+ return object;
+ }
+
+
+ /**
+ * Get the boolean value associated with an index.
+ * The string values "true" and "false" are converted to boolean.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The truth.
+ * @throws JSONException If there is no value for the index or if the
+ * value is not convertible to boolean.
+ */
+ public boolean getBoolean(int index) throws JSONException {
+ Object object = this.get(index);
+ if (object.equals(Boolean.FALSE) ||
+ (object instanceof String &&
+ ((String)object).equalsIgnoreCase("false"))) {
+ return false;
+ } else if (object.equals(Boolean.TRUE) ||
+ (object instanceof String &&
+ ((String)object).equalsIgnoreCase("true"))) {
+ return true;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a boolean.");
+ }
+
+
+ /**
+ * Get the double value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException If the key is not found or if the value cannot
+ * be converted to a number.
+ */
+ public double getDouble(int index) throws JSONException {
+ Object object = this.get(index);
+ try {
+ return object instanceof Number
+ ? ((Number)object).doubleValue()
+ : Double.parseDouble((String)object);
+ } catch (Exception e) {
+ throw new JSONException("JSONArray[" + index +
+ "] is not a number.");
+ }
+ }
+
+
+ /**
+ * Get the int value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException If the key is not found or if the value is not a number.
+ */
+ public int getInt(int index) throws JSONException {
+ Object object = this.get(index);
+ try {
+ return object instanceof Number
+ ? ((Number)object).intValue()
+ : Integer.parseInt((String)object);
+ } catch (Exception e) {
+ throw new JSONException("JSONArray[" + index +
+ "] is not a number.");
+ }
+ }
+
+
+ /**
+ * Get the JSONArray associated with an index.
+ * @param index The index must be between 0 and length() - 1.
+ * @return A JSONArray value.
+ * @throws JSONException If there is no value for the index. or if the
+ * value is not a JSONArray
+ */
+ public JSONArray getJSONArray(int index) throws JSONException {
+ Object object = this.get(index);
+ if (object instanceof JSONArray) {
+ return (JSONArray)object;
+ }
+ throw new JSONException("JSONArray[" + index +
+ "] is not a JSONArray.");
+ }
+
+
+ /**
+ * Get the JSONObject associated with an index.
+ * @param index subscript
+ * @return A JSONObject value.
+ * @throws JSONException If there is no value for the index or if the
+ * value is not a JSONObject
+ */
+ public JSONObject getJSONObject(int index) throws JSONException {
+ Object object = this.get(index);
+ if (object instanceof JSONObject) {
+ return (JSONObject)object;
+ }
+ throw new JSONException("JSONArray[" + index +
+ "] is not a JSONObject.");
+ }
+
+
+ /**
+ * Get the long value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException If the key is not found or if the value cannot
+ * be converted to a number.
+ */
+ public long getLong(int index) throws JSONException {
+ Object object = this.get(index);
+ try {
+ return object instanceof Number
+ ? ((Number)object).longValue()
+ : Long.parseLong((String)object);
+ } catch (Exception e) {
+ throw new JSONException("JSONArray[" + index +
+ "] is not a number.");
+ }
+ }
+
+
+ /**
+ * Get the string associated with an index.
+ * @param index The index must be between 0 and length() - 1.
+ * @return A string value.
+ * @throws JSONException If there is no string value for the index.
+ */
+ public String getString(int index) throws JSONException {
+ Object object = this.get(index);
+ if (object instanceof String) {
+ return (String)object;
+ }
+ throw new JSONException("JSONArray[" + index + "] not a string.");
+ }
+
+
+ /**
+ * Determine if the value is null.
+ * @param index The index must be between 0 and length() - 1.
+ * @return true if the value at the index is null, or if there is no value.
+ */
+ public boolean isNull(int index) {
+ return JSONObject.NULL.equals(this.opt(index));
+ }
+
+
+ /**
+ * Make a string from the contents of this JSONArray. The
+ * <code>separator</code> string is inserted between each element.
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param separator A string that will be inserted between the elements.
+ * @return a string.
+ * @throws JSONException If the array contains an invalid number.
+ */
+ public String join(String separator) throws JSONException {
+ int len = this.length();
+ StringBuffer sb = new StringBuffer();
+
+ for (int i = 0; i < len; i += 1) {
+ if (i > 0) {
+ sb.append(separator);
+ }
+ sb.append(JSONObject.valueToString(this.myArrayList.get(i)));
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Get the number of elements in the JSONArray, included nulls.
+ *
+ * @return The length (or size).
+ */
+ public int length() {
+ return this.myArrayList.size();
+ }
+
+
+ /**
+ * Get the optional object value associated with an index.
+ * @param index The index must be between 0 and length() - 1.
+ * @return An object value, or null if there is no
+ * object at that index.
+ */
+ public Object opt(int index) {
+ return (index < 0 || index >= this.length())
+ ? null
+ : this.myArrayList.get(index);
+ }
+
+
+ /**
+ * Get the optional boolean value associated with an index.
+ * It returns false if there is no value at that index,
+ * or if the value is not Boolean.TRUE or the String "true".
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The truth.
+ */
+ public boolean optBoolean(int index) {
+ return this.optBoolean(index, false);
+ }
+
+
+ /**
+ * Get the optional boolean value associated with an index.
+ * It returns the defaultValue if there is no value at that index or if
+ * it is not a Boolean or the String "true" or "false" (case insensitive).
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue A boolean default.
+ * @return The truth.
+ */
+ public boolean optBoolean(int index, boolean defaultValue) {
+ try {
+ return this.getBoolean(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get the optional double value associated with an index.
+ * NaN is returned if there is no value for the index,
+ * or if the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public double optDouble(int index) {
+ return this.optDouble(index, Double.NaN);
+ }
+
+
+ /**
+ * Get the optional double value associated with an index.
+ * The defaultValue is returned if there is no value for the index,
+ * or if the value is not a number and cannot be converted to a number.
+ *
+ * @param index subscript
+ * @param defaultValue The default value.
+ * @return The value.
+ */
+ public double optDouble(int index, double defaultValue) {
+ try {
+ return this.getDouble(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get the optional int value associated with an index.
+ * Zero is returned if there is no value for the index,
+ * or if the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public int optInt(int index) {
+ return this.optInt(index, 0);
+ }
+
+
+ /**
+ * Get the optional int value associated with an index.
+ * The defaultValue is returned if there is no value for the index,
+ * or if the value is not a number and cannot be converted to a number.
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue The default value.
+ * @return The value.
+ */
+ public int optInt(int index, int defaultValue) {
+ try {
+ return this.getInt(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get the optional JSONArray associated with an index.
+ * @param index subscript
+ * @return A JSONArray value, or null if the index has no value,
+ * or if the value is not a JSONArray.
+ */
+ public JSONArray optJSONArray(int index) {
+ Object o = this.opt(index);
+ return o instanceof JSONArray ? (JSONArray)o : null;
+ }
+
+
+ /**
+ * Get the optional JSONObject associated with an index.
+ * Null is returned if the key is not found, or null if the index has
+ * no value, or if the value is not a JSONObject.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return A JSONObject value.
+ */
+ public JSONObject optJSONObject(int index) {
+ Object o = this.opt(index);
+ return o instanceof JSONObject ? (JSONObject)o : null;
+ }
+
+
+ /**
+ * Get the optional long value associated with an index.
+ * Zero is returned if there is no value for the index,
+ * or if the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public long optLong(int index) {
+ return this.optLong(index, 0);
+ }
+
+
+ /**
+ * Get the optional long value associated with an index.
+ * The defaultValue is returned if there is no value for the index,
+ * or if the value is not a number and cannot be converted to a number.
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue The default value.
+ * @return The value.
+ */
+ public long optLong(int index, long defaultValue) {
+ try {
+ return this.getLong(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get the optional string value associated with an index. It returns an
+ * empty string if there is no value at that index. If the value
+ * is not a string and is not null, then it is coverted to a string.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return A String value.
+ */
+ public String optString(int index) {
+ return this.optString(index, "");
+ }
+
+
+ /**
+ * Get the optional string associated with an index.
+ * The defaultValue is returned if the key is not found.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue The default value.
+ * @return A String value.
+ */
+ public String optString(int index, String defaultValue) {
+ Object object = this.opt(index);
+ return JSONObject.NULL.equals(object)
+ ? defaultValue
+ : object.toString();
+ }
+
+
+ /**
+ * Append a boolean value. This increases the array's length by one.
+ *
+ * @param value A boolean value.
+ * @return this.
+ */
+ public JSONArray put(boolean value) {
+ this.put(value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+
+ /**
+ * Put a value in the JSONArray, where the value will be a
+ * JSONArray which is produced from a Collection.
+ * @param value A Collection value.
+ * @return this.
+ */
+ public JSONArray put(Collection value) {
+ this.put(new JSONArray(value));
+ return this;
+ }
+
+
+ /**
+ * Append a double value. This increases the array's length by one.
+ *
+ * @param value A double value.
+ * @throws JSONException if the value is not finite.
+ * @return this.
+ */
+ public JSONArray put(double value) throws JSONException {
+ Double d = new Double(value);
+ JSONObject.testValidity(d);
+ this.put(d);
+ return this;
+ }
+
+
+ /**
+ * Append an int value. This increases the array's length by one.
+ *
+ * @param value An int value.
+ * @return this.
+ */
+ public JSONArray put(int value) {
+ this.put(new Integer(value));
+ return this;
+ }
+
+
+ /**
+ * Append an long value. This increases the array's length by one.
+ *
+ * @param value A long value.
+ * @return this.
+ */
+ public JSONArray put(long value) {
+ this.put(new Long(value));
+ return this;
+ }
+
+
+ /**
+ * Put a value in the JSONArray, where the value will be a
+ * JSONObject which is produced from a Map.
+ * @param value A Map value.
+ * @return this.
+ */
+ public JSONArray put(Map value) {
+ this.put(new JSONObject(value));
+ return this;
+ }
+
+
+ /**
+ * Append an object value. This increases the array's length by one.
+ * @param value An object value. The value should be a
+ * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the
+ * JSONObject.NULL object.
+ * @return this.
+ */
+ public JSONArray put(Object value) {
+ this.myArrayList.add(value);
+ return this;
+ }
+
+
+ /**
+ * Put or replace a boolean value in the JSONArray. If the index is greater
+ * than the length of the JSONArray, then null elements will be added as
+ * necessary to pad it out.
+ * @param index The subscript.
+ * @param value A boolean value.
+ * @return this.
+ * @throws JSONException If the index is negative.
+ */
+ public JSONArray put(int index, boolean value) throws JSONException {
+ this.put(index, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+
+ /**
+ * Put a value in the JSONArray, where the value will be a
+ * JSONArray which is produced from a Collection.
+ * @param index The subscript.
+ * @param value A Collection value.
+ * @return this.
+ * @throws JSONException If the index is negative or if the value is
+ * not finite.
+ */
+ public JSONArray put(int index, Collection value) throws JSONException {
+ this.put(index, new JSONArray(value));
+ return this;
+ }
+
+
+ /**
+ * Put or replace a double value. If the index is greater than the length of
+ * the JSONArray, then null elements will be added as necessary to pad
+ * it out.
+ * @param index The subscript.
+ * @param value A double value.
+ * @return this.
+ * @throws JSONException If the index is negative or if the value is
+ * not finite.
+ */
+ public JSONArray put(int index, double value) throws JSONException {
+ this.put(index, new Double(value));
+ return this;
+ }
+
+
+ /**
+ * Put or replace an int value. If the index is greater than the length of
+ * the JSONArray, then null elements will be added as necessary to pad
+ * it out.
+ * @param index The subscript.
+ * @param value An int value.
+ * @return this.
+ * @throws JSONException If the index is negative.
+ */
+ public JSONArray put(int index, int value) throws JSONException {
+ this.put(index, new Integer(value));
+ return this;
+ }
+
+
+ /**
+ * Put or replace a long value. If the index is greater than the length of
+ * the JSONArray, then null elements will be added as necessary to pad
+ * it out.
+ * @param index The subscript.
+ * @param value A long value.
+ * @return this.
+ * @throws JSONException If the index is negative.
+ */
+ public JSONArray put(int index, long value) throws JSONException {
+ this.put(index, new Long(value));
+ return this;
+ }
+
+
+ /**
+ * Put a value in the JSONArray, where the value will be a
+ * JSONObject that is produced from a Map.
+ * @param index The subscript.
+ * @param value The Map value.
+ * @return this.
+ * @throws JSONException If the index is negative or if the the value is
+ * an invalid number.
+ */
+ public JSONArray put(int index, Map value) throws JSONException {
+ this.put(index, new JSONObject(value));
+ return this;
+ }
+
+
+ /**
+ * Put or replace an object value in the JSONArray. If the index is greater
+ * than the length of the JSONArray, then null elements will be added as
+ * necessary to pad it out.
+ * @param index The subscript.
+ * @param value The value to put into the array. The value should be a
+ * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the
+ * JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException If the index is negative or if the the value is
+ * an invalid number.
+ */
+ public JSONArray put(int index, Object value) throws JSONException {
+ JSONObject.testValidity(value);
+ if (index < 0) {
+ throw new JSONException("JSONArray[" + index + "] not found.");
+ }
+ if (index < this.length()) {
+ this.myArrayList.set(index, value);
+ } else {
+ while (index != this.length()) {
+ this.put(JSONObject.NULL);
+ }
+ this.put(value);
+ }
+ return this;
+ }
+
+
+ /**
+ * Remove an index and close the hole.
+ * @param index The index of the element to be removed.
+ * @return The value that was associated with the index,
+ * or null if there was no value.
+ */
+ public Object remove(int index) {
+ Object o = this.opt(index);
+ this.myArrayList.remove(index);
+ return o;
+ }
+
+
+ /**
+ * Produce a JSONObject by combining a JSONArray of names with the values
+ * of this JSONArray.
+ * @param names A JSONArray containing a list of key strings. These will be
+ * paired with the values.
+ * @return A JSONObject, or null if there are no names or if this JSONArray
+ * has no values.
+ * @throws JSONException If any of the names are null.
+ */
+ public JSONObject toJSONObject(JSONArray names) throws JSONException {
+ if (names == null || names.length() == 0 || this.length() == 0) {
+ return null;
+ }
+ JSONObject jo = new JSONObject();
+ for (int i = 0; i < names.length(); i += 1) {
+ jo.put(names.getString(i), this.opt(i));
+ }
+ return jo;
+ }
+
+
+ /**
+ * Make a JSON text of this JSONArray. For compactness, no
+ * unnecessary whitespace is added. If it is not possible to produce a
+ * syntactically correct JSON text then null will be returned instead. This
+ * could occur if the array contains an invalid number.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return a printable, displayable, transmittable
+ * representation of the array.
+ */
+ public String toString() {
+ try {
+ return '[' + this.join(",") + ']';
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Make a prettyprinted JSON text of this JSONArray.
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param indentFactor The number of spaces to add to each level of
+ * indentation.
+ * @return a printable, displayable, transmittable
+ * representation of the object, beginning
+ * with <code>[</code>&nbsp;<small>(left bracket)</small> and ending
+ * with <code>]</code>&nbsp;<small>(right bracket)</small>.
+ * @throws JSONException
+ */
+ public String toString(int indentFactor) throws JSONException {
+ return this.toString(indentFactor, 0);
+ }
+
+
+ /**
+ * Make a prettyprinted JSON text of this JSONArray.
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param indentFactor The number of spaces to add to each level of
+ * indentation.
+ * @param indent The indention of the top level.
+ * @return a printable, displayable, transmittable
+ * representation of the array.
+ * @throws JSONException
+ */
+ String toString(int indentFactor, int indent) throws JSONException {
+ int len = this.length();
+ if (len == 0) {
+ return "[]";
+ }
+ int i;
+ StringBuffer sb = new StringBuffer("[");
+ if (len == 1) {
+ sb.append(JSONObject.valueToString(this.myArrayList.get(0),
+ indentFactor, indent));
+ } else {
+ int newindent = indent + indentFactor;
+ sb.append('\n');
+ for (i = 0; i < len; i += 1) {
+ if (i > 0) {
+ sb.append(",\n");
+ }
+ for (int j = 0; j < newindent; j += 1) {
+ sb.append(' ');
+ }
+ sb.append(JSONObject.valueToString(this.myArrayList.get(i),
+ indentFactor, newindent));
+ }
+ sb.append('\n');
+ for (i = 0; i < indent; i += 1) {
+ sb.append(' ');
+ }
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+
+ /**
+ * Write the contents of the JSONArray as JSON text to a writer.
+ * For compactness, no whitespace is added.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ public Writer write(Writer writer) throws JSONException {
+ try {
+ boolean b = false;
+ int len = this.length();
+
+ writer.write('[');
+
+ for (int i = 0; i < len; i += 1) {
+ if (b) {
+ writer.write(',');
+ }
+ Object v = this.myArrayList.get(i);
+ if (v instanceof JSONObject) {
+ ((JSONObject)v).write(writer);
+ } else if (v instanceof JSONArray) {
+ ((JSONArray)v).write(writer);
+ } else {
+ writer.write(JSONObject.valueToString(v));
+ }
+ b = true;
+ }
+ writer.write(']');
+ return writer;
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/org/json/JSONException.java b/subsonic-main/src/main/java/org/json/JSONException.java
new file mode 100644
index 00000000..3ec8fb99
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONException.java
@@ -0,0 +1,28 @@
+package org.json;
+
+/**
+ * The JSONException is thrown by the JSON.org classes when things are amiss.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class JSONException extends Exception {
+ private static final long serialVersionUID = 0;
+ private Throwable cause;
+
+ /**
+ * Constructs a JSONException with an explanatory message.
+ * @param message Detail about the reason for the exception.
+ */
+ public JSONException(String message) {
+ super(message);
+ }
+
+ public JSONException(Throwable cause) {
+ super(cause.getMessage());
+ this.cause = cause;
+ }
+
+ public Throwable getCause() {
+ return this.cause;
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/JSONML.java b/subsonic-main/src/main/java/org/json/JSONML.java
new file mode 100644
index 00000000..d20a9c3c
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONML.java
@@ -0,0 +1,465 @@
+package org.json;
+
+/*
+Copyright (c) 2008 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.util.Iterator;
+
+
+/**
+ * This provides static methods to convert an XML text into a JSONArray or
+ * JSONObject, and to covert a JSONArray or JSONObject into an XML text using
+ * the JsonML transform.
+ * @author JSON.org
+ * @version 2011-11-24
+ */
+public class JSONML {
+
+ /**
+ * Parse XML values and store them in a JSONArray.
+ * @param x The XMLTokener containing the source string.
+ * @param arrayForm true if array form, false if object form.
+ * @param ja The JSONArray that is containing the current tag or null
+ * if we are at the outermost level.
+ * @return A JSONArray if the value is the outermost tag, otherwise null.
+ * @throws JSONException
+ */
+ private static Object parse(
+ XMLTokener x,
+ boolean arrayForm,
+ JSONArray ja
+ ) throws JSONException {
+ String attribute;
+ char c;
+ String closeTag = null;
+ int i;
+ JSONArray newja = null;
+ JSONObject newjo = null;
+ Object token;
+ String tagName = null;
+
+// Test for and skip past these forms:
+// <!-- ... -->
+// <![ ... ]]>
+// <! ... >
+// <? ... ?>
+
+ while (true) {
+ if (!x.more()) {
+ throw x.syntaxError("Bad XML");
+ }
+ token = x.nextContent();
+ if (token == XML.LT) {
+ token = x.nextToken();
+ if (token instanceof Character) {
+ if (token == XML.SLASH) {
+
+// Close tag </
+
+ token = x.nextToken();
+ if (!(token instanceof String)) {
+ throw new JSONException(
+ "Expected a closing name instead of '" +
+ token + "'.");
+ }
+ if (x.nextToken() != XML.GT) {
+ throw x.syntaxError("Misshaped close tag");
+ }
+ return token;
+ } else if (token == XML.BANG) {
+
+// <!
+
+ c = x.next();
+ if (c == '-') {
+ if (x.next() == '-') {
+ x.skipPast("-->");
+ }
+ x.back();
+ } else if (c == '[') {
+ token = x.nextToken();
+ if (token.equals("CDATA") && x.next() == '[') {
+ if (ja != null) {
+ ja.put(x.nextCDATA());
+ }
+ } else {
+ throw x.syntaxError("Expected 'CDATA['");
+ }
+ } else {
+ i = 1;
+ do {
+ token = x.nextMeta();
+ if (token == null) {
+ throw x.syntaxError("Missing '>' after '<!'.");
+ } else if (token == XML.LT) {
+ i += 1;
+ } else if (token == XML.GT) {
+ i -= 1;
+ }
+ } while (i > 0);
+ }
+ } else if (token == XML.QUEST) {
+
+// <?
+
+ x.skipPast("?>");
+ } else {
+ throw x.syntaxError("Misshaped tag");
+ }
+
+// Open tag <
+
+ } else {
+ if (!(token instanceof String)) {
+ throw x.syntaxError("Bad tagName '" + token + "'.");
+ }
+ tagName = (String)token;
+ newja = new JSONArray();
+ newjo = new JSONObject();
+ if (arrayForm) {
+ newja.put(tagName);
+ if (ja != null) {
+ ja.put(newja);
+ }
+ } else {
+ newjo.put("tagName", tagName);
+ if (ja != null) {
+ ja.put(newjo);
+ }
+ }
+ token = null;
+ for (;;) {
+ if (token == null) {
+ token = x.nextToken();
+ }
+ if (token == null) {
+ throw x.syntaxError("Misshaped tag");
+ }
+ if (!(token instanceof String)) {
+ break;
+ }
+
+// attribute = value
+
+ attribute = (String)token;
+ if (!arrayForm && (attribute == "tagName" || attribute == "childNode")) {
+ throw x.syntaxError("Reserved attribute.");
+ }
+ token = x.nextToken();
+ if (token == XML.EQ) {
+ token = x.nextToken();
+ if (!(token instanceof String)) {
+ throw x.syntaxError("Missing value");
+ }
+ newjo.accumulate(attribute, XML.stringToValue((String)token));
+ token = null;
+ } else {
+ newjo.accumulate(attribute, "");
+ }
+ }
+ if (arrayForm && newjo.length() > 0) {
+ newja.put(newjo);
+ }
+
+// Empty tag <.../>
+
+ if (token == XML.SLASH) {
+ if (x.nextToken() != XML.GT) {
+ throw x.syntaxError("Misshaped tag");
+ }
+ if (ja == null) {
+ if (arrayForm) {
+ return newja;
+ } else {
+ return newjo;
+ }
+ }
+
+// Content, between <...> and </...>
+
+ } else {
+ if (token != XML.GT) {
+ throw x.syntaxError("Misshaped tag");
+ }
+ closeTag = (String)parse(x, arrayForm, newja);
+ if (closeTag != null) {
+ if (!closeTag.equals(tagName)) {
+ throw x.syntaxError("Mismatched '" + tagName +
+ "' and '" + closeTag + "'");
+ }
+ tagName = null;
+ if (!arrayForm && newja.length() > 0) {
+ newjo.put("childNodes", newja);
+ }
+ if (ja == null) {
+ if (arrayForm) {
+ return newja;
+ } else {
+ return newjo;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ if (ja != null) {
+ ja.put(token instanceof String
+ ? XML.stringToValue((String)token)
+ : token);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Convert a well-formed (but not necessarily valid) XML string into a
+ * JSONArray using the JsonML transform. Each XML tag is represented as
+ * a JSONArray in which the first element is the tag name. If the tag has
+ * attributes, then the second element will be JSONObject containing the
+ * name/value pairs. If the tag contains children, then strings and
+ * JSONArrays will represent the child tags.
+ * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
+ * @param string The source string.
+ * @return A JSONArray containing the structured data from the XML string.
+ * @throws JSONException
+ */
+ public static JSONArray toJSONArray(String string) throws JSONException {
+ return toJSONArray(new XMLTokener(string));
+ }
+
+
+ /**
+ * Convert a well-formed (but not necessarily valid) XML string into a
+ * JSONArray using the JsonML transform. Each XML tag is represented as
+ * a JSONArray in which the first element is the tag name. If the tag has
+ * attributes, then the second element will be JSONObject containing the
+ * name/value pairs. If the tag contains children, then strings and
+ * JSONArrays will represent the child content and tags.
+ * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
+ * @param x An XMLTokener.
+ * @return A JSONArray containing the structured data from the XML string.
+ * @throws JSONException
+ */
+ public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
+ return (JSONArray)parse(x, true, null);
+ }
+
+
+ /**
+ * Convert a well-formed (but not necessarily valid) XML string into a
+ * JSONObject using the JsonML transform. Each XML tag is represented as
+ * a JSONObject with a "tagName" property. If the tag has attributes, then
+ * the attributes will be in the JSONObject as properties. If the tag
+ * contains children, the object will have a "childNodes" property which
+ * will be an array of strings and JsonML JSONObjects.
+
+ * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
+ * @param x An XMLTokener of the XML source text.
+ * @return A JSONObject containing the structured data from the XML string.
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
+ return (JSONObject)parse(x, false, null);
+ }
+
+
+ /**
+ * Convert a well-formed (but not necessarily valid) XML string into a
+ * JSONObject using the JsonML transform. Each XML tag is represented as
+ * a JSONObject with a "tagName" property. If the tag has attributes, then
+ * the attributes will be in the JSONObject as properties. If the tag
+ * contains children, the object will have a "childNodes" property which
+ * will be an array of strings and JsonML JSONObjects.
+
+ * Comments, prologs, DTDs, and <code>&lt;[ [ ]]></code> are ignored.
+ * @param string The XML source text.
+ * @return A JSONObject containing the structured data from the XML string.
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(String string) throws JSONException {
+ return toJSONObject(new XMLTokener(string));
+ }
+
+
+ /**
+ * Reverse the JSONML transformation, making an XML text from a JSONArray.
+ * @param ja A JSONArray.
+ * @return An XML string.
+ * @throws JSONException
+ */
+ public static String toString(JSONArray ja) throws JSONException {
+ int i;
+ JSONObject jo;
+ String key;
+ Iterator keys;
+ int length;
+ Object object;
+ StringBuffer sb = new StringBuffer();
+ String tagName;
+ String value;
+
+// Emit <tagName
+
+ tagName = ja.getString(0);
+ XML.noSpace(tagName);
+ tagName = XML.escape(tagName);
+ sb.append('<');
+ sb.append(tagName);
+
+ object = ja.opt(1);
+ if (object instanceof JSONObject) {
+ i = 2;
+ jo = (JSONObject)object;
+
+// Emit the attributes
+
+ keys = jo.keys();
+ while (keys.hasNext()) {
+ key = keys.next().toString();
+ XML.noSpace(key);
+ value = jo.optString(key);
+ if (value != null) {
+ sb.append(' ');
+ sb.append(XML.escape(key));
+ sb.append('=');
+ sb.append('"');
+ sb.append(XML.escape(value));
+ sb.append('"');
+ }
+ }
+ } else {
+ i = 1;
+ }
+
+//Emit content in body
+
+ length = ja.length();
+ if (i >= length) {
+ sb.append('/');
+ sb.append('>');
+ } else {
+ sb.append('>');
+ do {
+ object = ja.get(i);
+ i += 1;
+ if (object != null) {
+ if (object instanceof String) {
+ sb.append(XML.escape(object.toString()));
+ } else if (object instanceof JSONObject) {
+ sb.append(toString((JSONObject)object));
+ } else if (object instanceof JSONArray) {
+ sb.append(toString((JSONArray)object));
+ }
+ }
+ } while (i < length);
+ sb.append('<');
+ sb.append('/');
+ sb.append(tagName);
+ sb.append('>');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Reverse the JSONML transformation, making an XML text from a JSONObject.
+ * The JSONObject must contain a "tagName" property. If it has children,
+ * then it must have a "childNodes" property containing an array of objects.
+ * The other properties are attributes with string values.
+ * @param jo A JSONObject.
+ * @return An XML string.
+ * @throws JSONException
+ */
+ public static String toString(JSONObject jo) throws JSONException {
+ StringBuffer sb = new StringBuffer();
+ int i;
+ JSONArray ja;
+ String key;
+ Iterator keys;
+ int length;
+ Object object;
+ String tagName;
+ String value;
+
+//Emit <tagName
+
+ tagName = jo.optString("tagName");
+ if (tagName == null) {
+ return XML.escape(jo.toString());
+ }
+ XML.noSpace(tagName);
+ tagName = XML.escape(tagName);
+ sb.append('<');
+ sb.append(tagName);
+
+//Emit the attributes
+
+ keys = jo.keys();
+ while (keys.hasNext()) {
+ key = keys.next().toString();
+ if (!"tagName".equals(key) && !"childNodes".equals(key)) {
+ XML.noSpace(key);
+ value = jo.optString(key);
+ if (value != null) {
+ sb.append(' ');
+ sb.append(XML.escape(key));
+ sb.append('=');
+ sb.append('"');
+ sb.append(XML.escape(value));
+ sb.append('"');
+ }
+ }
+ }
+
+//Emit content in body
+
+ ja = jo.optJSONArray("childNodes");
+ if (ja == null) {
+ sb.append('/');
+ sb.append('>');
+ } else {
+ sb.append('>');
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ object = ja.get(i);
+ if (object != null) {
+ if (object instanceof String) {
+ sb.append(XML.escape(object.toString()));
+ } else if (object instanceof JSONObject) {
+ sb.append(toString((JSONObject)object));
+ } else if (object instanceof JSONArray) {
+ sb.append(toString((JSONArray)object));
+ } else {
+ sb.append(object.toString());
+ }
+ }
+ }
+ sb.append('<');
+ sb.append('/');
+ sb.append(tagName);
+ sb.append('>');
+ }
+ return sb.toString();
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/org/json/JSONObject.java b/subsonic-main/src/main/java/org/json/JSONObject.java
new file mode 100644
index 00000000..f8ee3590
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONObject.java
@@ -0,0 +1,1630 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+/**
+ * A JSONObject is an unordered collection of name/value pairs. Its
+ * external form is a string wrapped in curly braces with colons between the
+ * names and values, and commas between the values and names. The internal form
+ * is an object having <code>get</code> and <code>opt</code> methods for
+ * accessing the values by name, and <code>put</code> methods for adding or
+ * replacing values by name. The values can be any of these types:
+ * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>,
+ * <code>Number</code>, <code>String</code>, or the <code>JSONObject.NULL</code>
+ * object. A JSONObject constructor can be used to convert an external form
+ * JSON text into an internal form whose values can be retrieved with the
+ * <code>get</code> and <code>opt</code> methods, or to convert values into a
+ * JSON text using the <code>put</code> and <code>toString</code> methods.
+ * A <code>get</code> method returns a value if one can be found, and throws an
+ * exception if one cannot be found. An <code>opt</code> method returns a
+ * default value instead of throwing an exception, and so is useful for
+ * obtaining optional values.
+ * <p>
+ * The generic <code>get()</code> and <code>opt()</code> methods return an
+ * object, which you can cast or query for type. There are also typed
+ * <code>get</code> and <code>opt</code> methods that do type checking and type
+ * coercion for you. The opt methods differ from the get methods in that they
+ * do not throw. Instead, they return a specified value, such as null.
+ * <p>
+ * The <code>put</code> methods add or replace values in an object. For example,
+ * <pre>myString = new JSONObject().put("JSON", "Hello, World!").toString();</pre>
+ * produces the string <code>{"JSON": "Hello, World"}</code>.
+ * <p>
+ * The texts produced by the <code>toString</code> methods strictly conform to
+ * the JSON syntax rules.
+ * The constructors are more forgiving in the texts they will accept:
+ * <ul>
+ * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
+ * before the closing brace.</li>
+ * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single
+ * quote)</small>.</li>
+ * <li>Strings do not need to be quoted at all if they do not begin with a quote
+ * or single quote, and if they do not contain leading or trailing spaces,
+ * and if they do not contain any of these characters:
+ * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers
+ * and if they are not the reserved words <code>true</code>,
+ * <code>false</code>, or <code>null</code>.</li>
+ * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as
+ * by <code>:</code>.</li>
+ * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as
+ * well as by <code>,</code> <small>(comma)</small>.</li>
+ * </ul>
+ * @author JSON.org
+ * @version 2011-11-24
+ */
+public class JSONObject {
+
+ /**
+ * JSONObject.NULL is equivalent to the value that JavaScript calls null,
+ * whilst Java's null is equivalent to the value that JavaScript calls
+ * undefined.
+ */
+ private static final class Null {
+
+ /**
+ * There is only intended to be a single instance of the NULL object,
+ * so the clone method returns itself.
+ * @return NULL.
+ */
+ protected final Object clone() {
+ return this;
+ }
+
+ /**
+ * A Null object is equal to the null value and to itself.
+ * @param object An object to test for nullness.
+ * @return true if the object parameter is the JSONObject.NULL object
+ * or null.
+ */
+ public boolean equals(Object object) {
+ return object == null || object == this;
+ }
+
+ /**
+ * Get the "null" string value.
+ * @return The string "null".
+ */
+ public String toString() {
+ return "null";
+ }
+ }
+
+
+ /**
+ * The map where the JSONObject's properties are kept.
+ */
+ private final Map map;
+
+
+ /**
+ * It is sometimes more convenient and less ambiguous to have a
+ * <code>NULL</code> object than to use Java's <code>null</code> value.
+ * <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>.
+ * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>.
+ */
+ public static final Object NULL = new Null();
+
+
+ /**
+ * Construct an empty JSONObject.
+ */
+ public JSONObject() {
+ this.map = new HashMap();
+ }
+
+
+ /**
+ * Construct a JSONObject from a subset of another JSONObject.
+ * An array of strings is used to identify the keys that should be copied.
+ * Missing keys are ignored.
+ * @param jo A JSONObject.
+ * @param names An array of strings.
+ * @throws JSONException
+ * @exception JSONException If a value is a non-finite number or if a name is duplicated.
+ */
+ public JSONObject(JSONObject jo, String[] names) {
+ this();
+ for (int i = 0; i < names.length; i += 1) {
+ try {
+ this.putOnce(names[i], jo.opt(names[i]));
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+
+ /**
+ * Construct a JSONObject from a JSONTokener.
+ * @param x A JSONTokener object containing the source string.
+ * @throws JSONException If there is a syntax error in the source string
+ * or a duplicated key.
+ */
+ public JSONObject(JSONTokener x) throws JSONException {
+ this();
+ char c;
+ String key;
+
+ if (x.nextClean() != '{') {
+ throw x.syntaxError("A JSONObject text must begin with '{'");
+ }
+ for (;;) {
+ c = x.nextClean();
+ switch (c) {
+ case 0:
+ throw x.syntaxError("A JSONObject text must end with '}'");
+ case '}':
+ return;
+ default:
+ x.back();
+ key = x.nextValue().toString();
+ }
+
+// The key is followed by ':'. We will also tolerate '=' or '=>'.
+
+ c = x.nextClean();
+ if (c == '=') {
+ if (x.next() != '>') {
+ x.back();
+ }
+ } else if (c != ':') {
+ throw x.syntaxError("Expected a ':' after a key");
+ }
+ this.putOnce(key, x.nextValue());
+
+// Pairs are separated by ','. We will also tolerate ';'.
+
+ switch (x.nextClean()) {
+ case ';':
+ case ',':
+ if (x.nextClean() == '}') {
+ return;
+ }
+ x.back();
+ break;
+ case '}':
+ return;
+ default:
+ throw x.syntaxError("Expected a ',' or '}'");
+ }
+ }
+ }
+
+
+ /**
+ * Construct a JSONObject from a Map.
+ *
+ * @param map A map object that can be used to initialize the contents of
+ * the JSONObject.
+ * @throws JSONException
+ */
+ public JSONObject(Map map) {
+ this.map = new HashMap();
+ if (map != null) {
+ Iterator i = map.entrySet().iterator();
+ while (i.hasNext()) {
+ Map.Entry e = (Map.Entry)i.next();
+ Object value = e.getValue();
+ if (value != null) {
+ this.map.put(e.getKey(), wrap(value));
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Construct a JSONObject from an Object using bean getters.
+ * It reflects on all of the public methods of the object.
+ * For each of the methods with no parameters and a name starting
+ * with <code>"get"</code> or <code>"is"</code> followed by an uppercase letter,
+ * the method is invoked, and a key and the value returned from the getter method
+ * are put into the new JSONObject.
+ *
+ * The key is formed by removing the <code>"get"</code> or <code>"is"</code> prefix.
+ * If the second remaining character is not upper case, then the first
+ * character is converted to lower case.
+ *
+ * For example, if an object has a method named <code>"getName"</code>, and
+ * if the result of calling <code>object.getName()</code> is <code>"Larry Fine"</code>,
+ * then the JSONObject will contain <code>"name": "Larry Fine"</code>.
+ *
+ * @param bean An object that has getter methods that should be used
+ * to make a JSONObject.
+ */
+ public JSONObject(Object bean) {
+ this();
+ this.populateMap(bean);
+ }
+
+
+ /**
+ * Construct a JSONObject from an Object, using reflection to find the
+ * public members. The resulting JSONObject's keys will be the strings
+ * from the names array, and the values will be the field values associated
+ * with those keys in the object. If a key is not found or not visible,
+ * then it will not be copied into the new JSONObject.
+ * @param object An object that has fields that should be used to make a
+ * JSONObject.
+ * @param names An array of strings, the names of the fields to be obtained
+ * from the object.
+ */
+ public JSONObject(Object object, String names[]) {
+ this();
+ Class c = object.getClass();
+ for (int i = 0; i < names.length; i += 1) {
+ String name = names[i];
+ try {
+ this.putOpt(name, c.getField(name).get(object));
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+
+ /**
+ * Construct a JSONObject from a source JSON text string.
+ * This is the most commonly used JSONObject constructor.
+ * @param source A string beginning
+ * with <code>{</code>&nbsp;<small>(left brace)</small> and ending
+ * with <code>}</code>&nbsp;<small>(right brace)</small>.
+ * @exception JSONException If there is a syntax error in the source
+ * string or a duplicated key.
+ */
+ public JSONObject(String source) throws JSONException {
+ this(new JSONTokener(source));
+ }
+
+
+ /**
+ * Construct a JSONObject from a ResourceBundle.
+ * @param baseName The ResourceBundle base name.
+ * @param locale The Locale to load the ResourceBundle for.
+ * @throws JSONException If any JSONExceptions are detected.
+ */
+ public JSONObject(String baseName, Locale locale) throws JSONException {
+ this();
+ ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale,
+ Thread.currentThread().getContextClassLoader());
+
+// Iterate through the keys in the bundle.
+
+ Enumeration keys = bundle.getKeys();
+ while (keys.hasMoreElements()) {
+ Object key = keys.nextElement();
+ if (key instanceof String) {
+
+// Go through the path, ensuring that there is a nested JSONObject for each
+// segment except the last. Add the value using the last segment's name into
+// the deepest nested JSONObject.
+
+ String[] path = ((String)key).split("\\.");
+ int last = path.length - 1;
+ JSONObject target = this;
+ for (int i = 0; i < last; i += 1) {
+ String segment = path[i];
+ JSONObject nextTarget = target.optJSONObject(segment);
+ if (nextTarget == null) {
+ nextTarget = new JSONObject();
+ target.put(segment, nextTarget);
+ }
+ target = nextTarget;
+ }
+ target.put(path[last], bundle.getString((String)key));
+ }
+ }
+ }
+
+
+ /**
+ * Accumulate values under a key. It is similar to the put method except
+ * that if there is already an object stored under the key then a
+ * JSONArray is stored under the key to hold all of the accumulated values.
+ * If there is already a JSONArray, then the new value is appended to it.
+ * In contrast, the put method replaces the previous value.
+ *
+ * If only one value is accumulated that is not a JSONArray, then the
+ * result will be the same as using put. But if multiple values are
+ * accumulated, then the result will be like append.
+ * @param key A key string.
+ * @param value An object to be accumulated under the key.
+ * @return this.
+ * @throws JSONException If the value is an invalid number
+ * or if the key is null.
+ */
+ public JSONObject accumulate(
+ String key,
+ Object value
+ ) throws JSONException {
+ testValidity(value);
+ Object object = this.opt(key);
+ if (object == null) {
+ this.put(key, value instanceof JSONArray
+ ? new JSONArray().put(value)
+ : value);
+ } else if (object instanceof JSONArray) {
+ ((JSONArray)object).put(value);
+ } else {
+ this.put(key, new JSONArray().put(object).put(value));
+ }
+ return this;
+ }
+
+
+ /**
+ * Append values to the array under a key. If the key does not exist in the
+ * JSONObject, then the key is put in the JSONObject with its value being a
+ * JSONArray containing the value parameter. If the key was already
+ * associated with a JSONArray, then the value parameter is appended to it.
+ * @param key A key string.
+ * @param value An object to be accumulated under the key.
+ * @return this.
+ * @throws JSONException If the key is null or if the current value
+ * associated with the key is not a JSONArray.
+ */
+ public JSONObject append(String key, Object value) throws JSONException {
+ testValidity(value);
+ Object object = this.opt(key);
+ if (object == null) {
+ this.put(key, new JSONArray().put(value));
+ } else if (object instanceof JSONArray) {
+ this.put(key, ((JSONArray)object).put(value));
+ } else {
+ throw new JSONException("JSONObject[" + key +
+ "] is not a JSONArray.");
+ }
+ return this;
+ }
+
+
+ /**
+ * Produce a string from a double. The string "null" will be returned if
+ * the number is not finite.
+ * @param d A double.
+ * @return A String.
+ */
+ public static String doubleToString(double d) {
+ if (Double.isInfinite(d) || Double.isNaN(d)) {
+ return "null";
+ }
+
+// Shave off trailing zeros and decimal point, if possible.
+
+ String string = Double.toString(d);
+ if (string.indexOf('.') > 0 && string.indexOf('e') < 0 &&
+ string.indexOf('E') < 0) {
+ while (string.endsWith("0")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ if (string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ }
+ return string;
+ }
+
+
+ /**
+ * Get the value object associated with a key.
+ *
+ * @param key A key string.
+ * @return The object associated with the key.
+ * @throws JSONException if the key is not found.
+ */
+ public Object get(String key) throws JSONException {
+ if (key == null) {
+ throw new JSONException("Null key.");
+ }
+ Object object = this.opt(key);
+ if (object == null) {
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] not found.");
+ }
+ return object;
+ }
+
+
+ /**
+ * Get the boolean value associated with a key.
+ *
+ * @param key A key string.
+ * @return The truth.
+ * @throws JSONException
+ * if the value is not a Boolean or the String "true" or "false".
+ */
+ public boolean getBoolean(String key) throws JSONException {
+ Object object = this.get(key);
+ if (object.equals(Boolean.FALSE) ||
+ (object instanceof String &&
+ ((String)object).equalsIgnoreCase("false"))) {
+ return false;
+ } else if (object.equals(Boolean.TRUE) ||
+ (object instanceof String &&
+ ((String)object).equalsIgnoreCase("true"))) {
+ return true;
+ }
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] is not a Boolean.");
+ }
+
+
+ /**
+ * Get the double value associated with a key.
+ * @param key A key string.
+ * @return The numeric value.
+ * @throws JSONException if the key is not found or
+ * if the value is not a Number object and cannot be converted to a number.
+ */
+ public double getDouble(String key) throws JSONException {
+ Object object = this.get(key);
+ try {
+ return object instanceof Number
+ ? ((Number)object).doubleValue()
+ : Double.parseDouble((String)object);
+ } catch (Exception e) {
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] is not a number.");
+ }
+ }
+
+
+ /**
+ * Get the int value associated with a key.
+ *
+ * @param key A key string.
+ * @return The integer value.
+ * @throws JSONException if the key is not found or if the value cannot
+ * be converted to an integer.
+ */
+ public int getInt(String key) throws JSONException {
+ Object object = this.get(key);
+ try {
+ return object instanceof Number
+ ? ((Number)object).intValue()
+ : Integer.parseInt((String)object);
+ } catch (Exception e) {
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] is not an int.");
+ }
+ }
+
+
+ /**
+ * Get the JSONArray value associated with a key.
+ *
+ * @param key A key string.
+ * @return A JSONArray which is the value.
+ * @throws JSONException if the key is not found or
+ * if the value is not a JSONArray.
+ */
+ public JSONArray getJSONArray(String key) throws JSONException {
+ Object object = this.get(key);
+ if (object instanceof JSONArray) {
+ return (JSONArray)object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] is not a JSONArray.");
+ }
+
+
+ /**
+ * Get the JSONObject value associated with a key.
+ *
+ * @param key A key string.
+ * @return A JSONObject which is the value.
+ * @throws JSONException if the key is not found or
+ * if the value is not a JSONObject.
+ */
+ public JSONObject getJSONObject(String key) throws JSONException {
+ Object object = this.get(key);
+ if (object instanceof JSONObject) {
+ return (JSONObject)object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] is not a JSONObject.");
+ }
+
+
+ /**
+ * Get the long value associated with a key.
+ *
+ * @param key A key string.
+ * @return The long value.
+ * @throws JSONException if the key is not found or if the value cannot
+ * be converted to a long.
+ */
+ public long getLong(String key) throws JSONException {
+ Object object = this.get(key);
+ try {
+ return object instanceof Number
+ ? ((Number)object).longValue()
+ : Long.parseLong((String)object);
+ } catch (Exception e) {
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] is not a long.");
+ }
+ }
+
+
+ /**
+ * Get an array of field names from a JSONObject.
+ *
+ * @return An array of field names, or null if there are no names.
+ */
+ public static String[] getNames(JSONObject jo) {
+ int length = jo.length();
+ if (length == 0) {
+ return null;
+ }
+ Iterator iterator = jo.keys();
+ String[] names = new String[length];
+ int i = 0;
+ while (iterator.hasNext()) {
+ names[i] = (String)iterator.next();
+ i += 1;
+ }
+ return names;
+ }
+
+
+ /**
+ * Get an array of field names from an Object.
+ *
+ * @return An array of field names, or null if there are no names.
+ */
+ public static String[] getNames(Object object) {
+ if (object == null) {
+ return null;
+ }
+ Class klass = object.getClass();
+ Field[] fields = klass.getFields();
+ int length = fields.length;
+ if (length == 0) {
+ return null;
+ }
+ String[] names = new String[length];
+ for (int i = 0; i < length; i += 1) {
+ names[i] = fields[i].getName();
+ }
+ return names;
+ }
+
+
+ /**
+ * Get the string associated with a key.
+ *
+ * @param key A key string.
+ * @return A string which is the value.
+ * @throws JSONException if there is no string value for the key.
+ */
+ public String getString(String key) throws JSONException {
+ Object object = this.get(key);
+ if (object instanceof String) {
+ return (String)object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) +
+ "] not a string.");
+ }
+
+
+ /**
+ * Determine if the JSONObject contains a specific key.
+ * @param key A key string.
+ * @return true if the key exists in the JSONObject.
+ */
+ public boolean has(String key) {
+ return this.map.containsKey(key);
+ }
+
+
+ /**
+ * Increment a property of a JSONObject. If there is no such property,
+ * create one with a value of 1. If there is such a property, and if
+ * it is an Integer, Long, Double, or Float, then add one to it.
+ * @param key A key string.
+ * @return this.
+ * @throws JSONException If there is already a property with this name
+ * that is not an Integer, Long, Double, or Float.
+ */
+ public JSONObject increment(String key) throws JSONException {
+ Object value = this.opt(key);
+ if (value == null) {
+ this.put(key, 1);
+ } else if (value instanceof Integer) {
+ this.put(key, ((Integer)value).intValue() + 1);
+ } else if (value instanceof Long) {
+ this.put(key, ((Long)value).longValue() + 1);
+ } else if (value instanceof Double) {
+ this.put(key, ((Double)value).doubleValue() + 1);
+ } else if (value instanceof Float) {
+ this.put(key, ((Float)value).floatValue() + 1);
+ } else {
+ throw new JSONException("Unable to increment [" + quote(key) + "].");
+ }
+ return this;
+ }
+
+
+ /**
+ * Determine if the value associated with the key is null or if there is
+ * no value.
+ * @param key A key string.
+ * @return true if there is no value associated with the key or if
+ * the value is the JSONObject.NULL object.
+ */
+ public boolean isNull(String key) {
+ return JSONObject.NULL.equals(this.opt(key));
+ }
+
+
+ /**
+ * Get an enumeration of the keys of the JSONObject.
+ *
+ * @return An iterator of the keys.
+ */
+ public Iterator keys() {
+ return this.map.keySet().iterator();
+ }
+
+
+ /**
+ * Get the number of keys stored in the JSONObject.
+ *
+ * @return The number of keys in the JSONObject.
+ */
+ public int length() {
+ return this.map.size();
+ }
+
+
+ /**
+ * Produce a JSONArray containing the names of the elements of this
+ * JSONObject.
+ * @return A JSONArray containing the key strings, or null if the JSONObject
+ * is empty.
+ */
+ public JSONArray names() {
+ JSONArray ja = new JSONArray();
+ Iterator keys = this.keys();
+ while (keys.hasNext()) {
+ ja.put(keys.next());
+ }
+ return ja.length() == 0 ? null : ja;
+ }
+
+ /**
+ * Produce a string from a Number.
+ * @param number A Number
+ * @return A String.
+ * @throws JSONException If n is a non-finite number.
+ */
+ public static String numberToString(Number number)
+ throws JSONException {
+ if (number == null) {
+ throw new JSONException("Null pointer");
+ }
+ testValidity(number);
+
+// Shave off trailing zeros and decimal point, if possible.
+
+ String string = number.toString();
+ if (string.indexOf('.') > 0 && string.indexOf('e') < 0 &&
+ string.indexOf('E') < 0) {
+ while (string.endsWith("0")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ if (string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ }
+ return string;
+ }
+
+
+ /**
+ * Get an optional value associated with a key.
+ * @param key A key string.
+ * @return An object which is the value, or null if there is no value.
+ */
+ public Object opt(String key) {
+ return key == null ? null : this.map.get(key);
+ }
+
+
+ /**
+ * Get an optional boolean associated with a key.
+ * It returns false if there is no such key, or if the value is not
+ * Boolean.TRUE or the String "true".
+ *
+ * @param key A key string.
+ * @return The truth.
+ */
+ public boolean optBoolean(String key) {
+ return this.optBoolean(key, false);
+ }
+
+
+ /**
+ * Get an optional boolean associated with a key.
+ * It returns the defaultValue if there is no such key, or if it is not
+ * a Boolean or the String "true" or "false" (case insensitive).
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return The truth.
+ */
+ public boolean optBoolean(String key, boolean defaultValue) {
+ try {
+ return this.getBoolean(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get an optional double associated with a key,
+ * or NaN if there is no such key or if its value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as
+ * a number.
+ *
+ * @param key A string which is the key.
+ * @return An object which is the value.
+ */
+ public double optDouble(String key) {
+ return this.optDouble(key, Double.NaN);
+ }
+
+
+ /**
+ * Get an optional double associated with a key, or the
+ * defaultValue if there is no such key or if its value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as
+ * a number.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return An object which is the value.
+ */
+ public double optDouble(String key, double defaultValue) {
+ try {
+ return this.getDouble(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get an optional int value associated with a key,
+ * or zero if there is no such key or if the value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as
+ * a number.
+ *
+ * @param key A key string.
+ * @return An object which is the value.
+ */
+ public int optInt(String key) {
+ return this.optInt(key, 0);
+ }
+
+
+ /**
+ * Get an optional int value associated with a key,
+ * or the default if there is no such key or if the value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as
+ * a number.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return An object which is the value.
+ */
+ public int optInt(String key, int defaultValue) {
+ try {
+ return this.getInt(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get an optional JSONArray associated with a key.
+ * It returns null if there is no such key, or if its value is not a
+ * JSONArray.
+ *
+ * @param key A key string.
+ * @return A JSONArray which is the value.
+ */
+ public JSONArray optJSONArray(String key) {
+ Object o = this.opt(key);
+ return o instanceof JSONArray ? (JSONArray)o : null;
+ }
+
+
+ /**
+ * Get an optional JSONObject associated with a key.
+ * It returns null if there is no such key, or if its value is not a
+ * JSONObject.
+ *
+ * @param key A key string.
+ * @return A JSONObject which is the value.
+ */
+ public JSONObject optJSONObject(String key) {
+ Object object = this.opt(key);
+ return object instanceof JSONObject ? (JSONObject)object : null;
+ }
+
+
+ /**
+ * Get an optional long value associated with a key,
+ * or zero if there is no such key or if the value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as
+ * a number.
+ *
+ * @param key A key string.
+ * @return An object which is the value.
+ */
+ public long optLong(String key) {
+ return this.optLong(key, 0);
+ }
+
+
+ /**
+ * Get an optional long value associated with a key,
+ * or the default if there is no such key or if the value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as
+ * a number.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return An object which is the value.
+ */
+ public long optLong(String key, long defaultValue) {
+ try {
+ return this.getLong(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Get an optional string associated with a key.
+ * It returns an empty string if there is no such key. If the value is not
+ * a string and is not null, then it is converted to a string.
+ *
+ * @param key A key string.
+ * @return A string which is the value.
+ */
+ public String optString(String key) {
+ return this.optString(key, "");
+ }
+
+
+ /**
+ * Get an optional string associated with a key.
+ * It returns the defaultValue if there is no such key.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return A string which is the value.
+ */
+ public String optString(String key, String defaultValue) {
+ Object object = this.opt(key);
+ return NULL.equals(object) ? defaultValue : object.toString();
+ }
+
+
+ private void populateMap(Object bean) {
+ Class klass = bean.getClass();
+
+// If klass is a System class then set includeSuperClass to false.
+
+ boolean includeSuperClass = klass.getClassLoader() != null;
+
+ Method[] methods = includeSuperClass
+ ? klass.getMethods()
+ : klass.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i += 1) {
+ try {
+ Method method = methods[i];
+ if (Modifier.isPublic(method.getModifiers())) {
+ String name = method.getName();
+ String key = "";
+ if (name.startsWith("get")) {
+ if ("getClass".equals(name) ||
+ "getDeclaringClass".equals(name)) {
+ key = "";
+ } else {
+ key = name.substring(3);
+ }
+ } else if (name.startsWith("is")) {
+ key = name.substring(2);
+ }
+ if (key.length() > 0 &&
+ Character.isUpperCase(key.charAt(0)) &&
+ method.getParameterTypes().length == 0) {
+ if (key.length() == 1) {
+ key = key.toLowerCase();
+ } else if (!Character.isUpperCase(key.charAt(1))) {
+ key = key.substring(0, 1).toLowerCase() +
+ key.substring(1);
+ }
+
+ Object result = method.invoke(bean, (Object[])null);
+ if (result != null) {
+ this.map.put(key, wrap(result));
+ }
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+
+ /**
+ * Put a key/boolean pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value A boolean which is the value.
+ * @return this.
+ * @throws JSONException If the key is null.
+ */
+ public JSONObject put(String key, boolean value) throws JSONException {
+ this.put(key, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+
+ /**
+ * Put a key/value pair in the JSONObject, where the value will be a
+ * JSONArray which is produced from a Collection.
+ * @param key A key string.
+ * @param value A Collection value.
+ * @return this.
+ * @throws JSONException
+ */
+ public JSONObject put(String key, Collection value) throws JSONException {
+ this.put(key, new JSONArray(value));
+ return this;
+ }
+
+
+ /**
+ * Put a key/double pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value A double which is the value.
+ * @return this.
+ * @throws JSONException If the key is null or if the number is invalid.
+ */
+ public JSONObject put(String key, double value) throws JSONException {
+ this.put(key, new Double(value));
+ return this;
+ }
+
+
+ /**
+ * Put a key/int pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value An int which is the value.
+ * @return this.
+ * @throws JSONException If the key is null.
+ */
+ public JSONObject put(String key, int value) throws JSONException {
+ this.put(key, new Integer(value));
+ return this;
+ }
+
+
+ /**
+ * Put a key/long pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value A long which is the value.
+ * @return this.
+ * @throws JSONException If the key is null.
+ */
+ public JSONObject put(String key, long value) throws JSONException {
+ this.put(key, new Long(value));
+ return this;
+ }
+
+
+ /**
+ * Put a key/value pair in the JSONObject, where the value will be a
+ * JSONObject which is produced from a Map.
+ * @param key A key string.
+ * @param value A Map value.
+ * @return this.
+ * @throws JSONException
+ */
+ public JSONObject put(String key, Map value) throws JSONException {
+ this.put(key, new JSONObject(value));
+ return this;
+ }
+
+
+ /**
+ * Put a key/value pair in the JSONObject. If the value is null,
+ * then the key will be removed from the JSONObject if it is present.
+ * @param key A key string.
+ * @param value An object which is the value. It should be of one of these
+ * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String,
+ * or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException If the value is non-finite number
+ * or if the key is null.
+ */
+ public JSONObject put(String key, Object value) throws JSONException {
+ if (key == null) {
+ throw new JSONException("Null key.");
+ }
+ if (value != null) {
+ testValidity(value);
+ this.map.put(key, value);
+ } else {
+ this.remove(key);
+ }
+ return this;
+ }
+
+
+ /**
+ * Put a key/value pair in the JSONObject, but only if the key and the
+ * value are both non-null, and only if there is not already a member
+ * with that name.
+ * @param key
+ * @param value
+ * @return his.
+ * @throws JSONException if the key is a duplicate
+ */
+ public JSONObject putOnce(String key, Object value) throws JSONException {
+ if (key != null && value != null) {
+ if (this.opt(key) != null) {
+ throw new JSONException("Duplicate key \"" + key + "\"");
+ }
+ this.put(key, value);
+ }
+ return this;
+ }
+
+
+ /**
+ * Put a key/value pair in the JSONObject, but only if the
+ * key and the value are both non-null.
+ * @param key A key string.
+ * @param value An object which is the value. It should be of one of these
+ * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String,
+ * or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException If the value is a non-finite number.
+ */
+ public JSONObject putOpt(String key, Object value) throws JSONException {
+ if (key != null && value != null) {
+ this.put(key, value);
+ }
+ return this;
+ }
+
+
+ /**
+ * Produce a string in double quotes with backslash sequences in all the
+ * right places. A backslash will be inserted within </, producing <\/,
+ * allowing JSON text to be delivered in HTML. In JSON text, a string
+ * cannot contain a control character or an unescaped quote or backslash.
+ * @param string A String
+ * @return A String correctly formatted for insertion in a JSON text.
+ */
+ public static String quote(String string) {
+ if (string == null || string.length() == 0) {
+ return "\"\"";
+ }
+
+ char b;
+ char c = 0;
+ String hhhh;
+ int i;
+ int len = string.length();
+ StringBuffer sb = new StringBuffer(len + 4);
+
+ sb.append('"');
+ for (i = 0; i < len; i += 1) {
+ b = c;
+ c = string.charAt(i);
+ switch (c) {
+ case '\\':
+ case '"':
+ sb.append('\\');
+ sb.append(c);
+ break;
+ case '/':
+ if (b == '<') {
+ sb.append('\\');
+ }
+ sb.append(c);
+ break;
+ case '\b':
+ sb.append("\\b");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\f':
+ sb.append("\\f");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ default:
+ if (c < ' ' || (c >= '\u0080' && c < '\u00a0') ||
+ (c >= '\u2000' && c < '\u2100')) {
+ hhhh = "000" + Integer.toHexString(c);
+ sb.append("\\u" + hhhh.substring(hhhh.length() - 4));
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ sb.append('"');
+ return sb.toString();
+ }
+
+ /**
+ * Remove a name and its value, if present.
+ * @param key The name to be removed.
+ * @return The value that was associated with the name,
+ * or null if there was no value.
+ */
+ public Object remove(String key) {
+ return this.map.remove(key);
+ }
+
+ /**
+ * Try to convert a string into a number, boolean, or null. If the string
+ * can't be converted, return the string.
+ * @param string A String.
+ * @return A simple JSON value.
+ */
+ public static Object stringToValue(String string) {
+ Double d;
+ if (string.equals("")) {
+ return string;
+ }
+ if (string.equalsIgnoreCase("true")) {
+ return Boolean.TRUE;
+ }
+ if (string.equalsIgnoreCase("false")) {
+ return Boolean.FALSE;
+ }
+ if (string.equalsIgnoreCase("null")) {
+ return JSONObject.NULL;
+ }
+
+ /*
+ * If it might be a number, try converting it.
+ * If a number cannot be produced, then the value will just
+ * be a string. Note that the plus and implied string
+ * conventions are non-standard. A JSON parser may accept
+ * non-JSON forms as long as it accepts all correct JSON forms.
+ */
+
+ char b = string.charAt(0);
+ if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') {
+ try {
+ if (string.indexOf('.') > -1 ||
+ string.indexOf('e') > -1 || string.indexOf('E') > -1) {
+ d = Double.valueOf(string);
+ if (!d.isInfinite() && !d.isNaN()) {
+ return d;
+ }
+ } else {
+ Long myLong = new Long(string);
+ if (myLong.longValue() == myLong.intValue()) {
+ return new Integer(myLong.intValue());
+ } else {
+ return myLong;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ }
+ return string;
+ }
+
+
+ /**
+ * Throw an exception if the object is a NaN or infinite number.
+ * @param o The object to test.
+ * @throws JSONException If o is a non-finite number.
+ */
+ public static void testValidity(Object o) throws JSONException {
+ if (o != null) {
+ if (o instanceof Double) {
+ if (((Double)o).isInfinite() || ((Double)o).isNaN()) {
+ throw new JSONException(
+ "JSON does not allow non-finite numbers.");
+ }
+ } else if (o instanceof Float) {
+ if (((Float)o).isInfinite() || ((Float)o).isNaN()) {
+ throw new JSONException(
+ "JSON does not allow non-finite numbers.");
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Produce a JSONArray containing the values of the members of this
+ * JSONObject.
+ * @param names A JSONArray containing a list of key strings. This
+ * determines the sequence of the values in the result.
+ * @return A JSONArray of values.
+ * @throws JSONException If any of the values are non-finite numbers.
+ */
+ public JSONArray toJSONArray(JSONArray names) throws JSONException {
+ if (names == null || names.length() == 0) {
+ return null;
+ }
+ JSONArray ja = new JSONArray();
+ for (int i = 0; i < names.length(); i += 1) {
+ ja.put(this.opt(names.getString(i)));
+ }
+ return ja;
+ }
+
+ /**
+ * Make a JSON text of this JSONObject. For compactness, no whitespace
+ * is added. If this would not result in a syntactically correct JSON text,
+ * then null will be returned instead.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return a printable, displayable, portable, transmittable
+ * representation of the object, beginning
+ * with <code>{</code>&nbsp;<small>(left brace)</small> and ending
+ * with <code>}</code>&nbsp;<small>(right brace)</small>.
+ */
+ public String toString() {
+ try {
+ Iterator keys = this.keys();
+ StringBuffer sb = new StringBuffer("{");
+
+ while (keys.hasNext()) {
+ if (sb.length() > 1) {
+ sb.append(',');
+ }
+ Object o = keys.next();
+ sb.append(quote(o.toString()));
+ sb.append(':');
+ sb.append(valueToString(this.map.get(o)));
+ }
+ sb.append('}');
+ return sb.toString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Make a prettyprinted JSON text of this JSONObject.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param indentFactor The number of spaces to add to each level of
+ * indentation.
+ * @return a printable, displayable, portable, transmittable
+ * representation of the object, beginning
+ * with <code>{</code>&nbsp;<small>(left brace)</small> and ending
+ * with <code>}</code>&nbsp;<small>(right brace)</small>.
+ * @throws JSONException If the object contains an invalid number.
+ */
+ public String toString(int indentFactor) throws JSONException {
+ return this.toString(indentFactor, 0);
+ }
+
+
+ /**
+ * Make a prettyprinted JSON text of this JSONObject.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param indentFactor The number of spaces to add to each level of
+ * indentation.
+ * @param indent The indentation of the top level.
+ * @return a printable, displayable, transmittable
+ * representation of the object, beginning
+ * with <code>{</code>&nbsp;<small>(left brace)</small> and ending
+ * with <code>}</code>&nbsp;<small>(right brace)</small>.
+ * @throws JSONException If the object contains an invalid number.
+ */
+ String toString(int indentFactor, int indent) throws JSONException {
+ int i;
+ int length = this.length();
+ if (length == 0) {
+ return "{}";
+ }
+ Iterator keys = this.keys();
+ int newindent = indent + indentFactor;
+ Object object;
+ StringBuffer sb = new StringBuffer("{");
+ if (length == 1) {
+ object = keys.next();
+ sb.append(quote(object.toString()));
+ sb.append(": ");
+ sb.append(valueToString(this.map.get(object), indentFactor,
+ indent));
+ } else {
+ while (keys.hasNext()) {
+ object = keys.next();
+ if (sb.length() > 1) {
+ sb.append(",\n");
+ } else {
+ sb.append('\n');
+ }
+ for (i = 0; i < newindent; i += 1) {
+ sb.append(' ');
+ }
+ sb.append(quote(object.toString()));
+ sb.append(": ");
+ sb.append(valueToString(this.map.get(object), indentFactor,
+ newindent));
+ }
+ if (sb.length() > 1) {
+ sb.append('\n');
+ for (i = 0; i < indent; i += 1) {
+ sb.append(' ');
+ }
+ }
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+
+ /**
+ * Make a JSON text of an Object value. If the object has an
+ * value.toJSONString() method, then that method will be used to produce
+ * the JSON text. The method is required to produce a strictly
+ * conforming text. If the object does not contain a toJSONString
+ * method (which is the most common case), then a text will be
+ * produced by other means. If the value is an array or Collection,
+ * then a JSONArray will be made from it and its toJSONString method
+ * will be called. If the value is a MAP, then a JSONObject will be made
+ * from it and its toJSONString method will be called. Otherwise, the
+ * value's toString method will be called, and the result will be quoted.
+ *
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param value The value to be serialized.
+ * @return a printable, displayable, transmittable
+ * representation of the object, beginning
+ * with <code>{</code>&nbsp;<small>(left brace)</small> and ending
+ * with <code>}</code>&nbsp;<small>(right brace)</small>.
+ * @throws JSONException If the value is or contains an invalid number.
+ */
+ public static String valueToString(Object value) throws JSONException {
+ if (value == null || value.equals(null)) {
+ return "null";
+ }
+ if (value instanceof JSONString) {
+ Object object;
+ try {
+ object = ((JSONString)value).toJSONString();
+ } catch (Exception e) {
+ throw new JSONException(e);
+ }
+ if (object instanceof String) {
+ return (String)object;
+ }
+ throw new JSONException("Bad value from toJSONString: " + object);
+ }
+ if (value instanceof Number) {
+ return numberToString((Number) value);
+ }
+ if (value instanceof Boolean || value instanceof JSONObject ||
+ value instanceof JSONArray) {
+ return value.toString();
+ }
+ if (value instanceof Map) {
+ return new JSONObject((Map)value).toString();
+ }
+ if (value instanceof Collection) {
+ return new JSONArray((Collection)value).toString();
+ }
+ if (value.getClass().isArray()) {
+ return new JSONArray(value).toString();
+ }
+ return quote(value.toString());
+ }
+
+
+ /**
+ * Make a prettyprinted JSON text of an object value.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ * @param value The value to be serialized.
+ * @param indentFactor The number of spaces to add to each level of
+ * indentation.
+ * @param indent The indentation of the top level.
+ * @return a printable, displayable, transmittable
+ * representation of the object, beginning
+ * with <code>{</code>&nbsp;<small>(left brace)</small> and ending
+ * with <code>}</code>&nbsp;<small>(right brace)</small>.
+ * @throws JSONException If the object contains an invalid number.
+ */
+ static String valueToString(
+ Object value,
+ int indentFactor,
+ int indent
+ ) throws JSONException {
+ if (value == null || value.equals(null)) {
+ return "null";
+ }
+ try {
+ if (value instanceof JSONString) {
+ Object o = ((JSONString)value).toJSONString();
+ if (o instanceof String) {
+ return (String)o;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ if (value instanceof Number) {
+ return numberToString((Number) value);
+ }
+ if (value instanceof Boolean) {
+ return value.toString();
+ }
+ if (value instanceof JSONObject) {
+ return ((JSONObject)value).toString(indentFactor, indent);
+ }
+ if (value instanceof JSONArray) {
+ return ((JSONArray)value).toString(indentFactor, indent);
+ }
+ if (value instanceof Map) {
+ return new JSONObject((Map)value).toString(indentFactor, indent);
+ }
+ if (value instanceof Collection) {
+ return new JSONArray((Collection)value).toString(indentFactor, indent);
+ }
+ if (value.getClass().isArray()) {
+ return new JSONArray(value).toString(indentFactor, indent);
+ }
+ return quote(value.toString());
+ }
+
+
+ /**
+ * Wrap an object, if necessary. If the object is null, return the NULL
+ * object. If it is an array or collection, wrap it in a JSONArray. If
+ * it is a map, wrap it in a JSONObject. If it is a standard property
+ * (Double, String, et al) then it is already wrapped. Otherwise, if it
+ * comes from one of the java packages, turn it into a string. And if
+ * it doesn't, try to wrap it in a JSONObject. If the wrapping fails,
+ * then null is returned.
+ *
+ * @param object The object to wrap
+ * @return The wrapped value
+ */
+ public static Object wrap(Object object) {
+ try {
+ if (object == null) {
+ return NULL;
+ }
+ if (object instanceof JSONObject || object instanceof JSONArray ||
+ NULL.equals(object) || object instanceof JSONString ||
+ object instanceof Byte || object instanceof Character ||
+ object instanceof Short || object instanceof Integer ||
+ object instanceof Long || object instanceof Boolean ||
+ object instanceof Float || object instanceof Double ||
+ object instanceof String) {
+ return object;
+ }
+
+ if (object instanceof Collection) {
+ return new JSONArray((Collection)object);
+ }
+ if (object.getClass().isArray()) {
+ return new JSONArray(object);
+ }
+ if (object instanceof Map) {
+ return new JSONObject((Map)object);
+ }
+ Package objectPackage = object.getClass().getPackage();
+ String objectPackageName = objectPackage != null
+ ? objectPackage.getName()
+ : "";
+ if (
+ objectPackageName.startsWith("java.") ||
+ objectPackageName.startsWith("javax.") ||
+ object.getClass().getClassLoader() == null
+ ) {
+ return object.toString();
+ }
+ return new JSONObject(object);
+ } catch(Exception exception) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Write the contents of the JSONObject as JSON text to a writer.
+ * For compactness, no whitespace is added.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ public Writer write(Writer writer) throws JSONException {
+ try {
+ boolean commanate = false;
+ Iterator keys = this.keys();
+ writer.write('{');
+
+ while (keys.hasNext()) {
+ if (commanate) {
+ writer.write(',');
+ }
+ Object key = keys.next();
+ writer.write(quote(key.toString()));
+ writer.write(':');
+ Object value = this.map.get(key);
+ if (value instanceof JSONObject) {
+ ((JSONObject)value).write(writer);
+ } else if (value instanceof JSONArray) {
+ ((JSONArray)value).write(writer);
+ } else {
+ writer.write(valueToString(value));
+ }
+ commanate = true;
+ }
+ writer.write('}');
+ return writer;
+ } catch (IOException exception) {
+ throw new JSONException(exception);
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/org/json/JSONString.java b/subsonic-main/src/main/java/org/json/JSONString.java
new file mode 100644
index 00000000..6efd68e7
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONString.java
@@ -0,0 +1,18 @@
+package org.json;
+/**
+ * The <code>JSONString</code> interface allows a <code>toJSONString()</code>
+ * method so that a class can change the behavior of
+ * <code>JSONObject.toString()</code>, <code>JSONArray.toString()</code>,
+ * and <code>JSONWriter.value(</code>Object<code>)</code>. The
+ * <code>toJSONString</code> method will be used instead of the default behavior
+ * of using the Object's <code>toString()</code> method and quoting the result.
+ */
+public interface JSONString {
+ /**
+ * The <code>toJSONString</code> method allows a class to produce its own JSON
+ * serialization.
+ *
+ * @return A strictly syntactically correct JSON text.
+ */
+ public String toJSONString();
+}
diff --git a/subsonic-main/src/main/java/org/json/JSONStringer.java b/subsonic-main/src/main/java/org/json/JSONStringer.java
new file mode 100644
index 00000000..32c9f7f4
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONStringer.java
@@ -0,0 +1,78 @@
+package org.json;
+
+/*
+Copyright (c) 2006 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.io.StringWriter;
+
+/**
+ * JSONStringer provides a quick and convenient way of producing JSON text.
+ * The texts produced strictly conform to JSON syntax rules. No whitespace is
+ * added, so the results are ready for transmission or storage. Each instance of
+ * JSONStringer can produce one JSON text.
+ * <p>
+ * A JSONStringer instance provides a <code>value</code> method for appending
+ * values to the
+ * text, and a <code>key</code>
+ * method for adding keys before values in objects. There are <code>array</code>
+ * and <code>endArray</code> methods that make and bound array values, and
+ * <code>object</code> and <code>endObject</code> methods which make and bound
+ * object values. All of these methods return the JSONWriter instance,
+ * permitting cascade style. For example, <pre>
+ * myString = new JSONStringer()
+ * .object()
+ * .key("JSON")
+ * .value("Hello, World!")
+ * .endObject()
+ * .toString();</pre> which produces the string <pre>
+ * {"JSON":"Hello, World!"}</pre>
+ * <p>
+ * The first method called must be <code>array</code> or <code>object</code>.
+ * There are no methods for adding commas or colons. JSONStringer adds them for
+ * you. Objects and arrays can be nested up to 20 levels deep.
+ * <p>
+ * This can sometimes be easier than using a JSONObject to build a string.
+ * @author JSON.org
+ * @version 2008-09-18
+ */
+public class JSONStringer extends JSONWriter {
+ /**
+ * Make a fresh JSONStringer. It can be used to build one JSON text.
+ */
+ public JSONStringer() {
+ super(new StringWriter());
+ }
+
+ /**
+ * Return the JSON text. This method is used to obtain the product of the
+ * JSONStringer instance. It will return <code>null</code> if there was a
+ * problem in the construction of the JSON text (such as the calls to
+ * <code>array</code> were not properly balanced with calls to
+ * <code>endArray</code>).
+ * @return The JSON text.
+ */
+ public String toString() {
+ return this.mode == 'd' ? this.writer.toString() : null;
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/JSONTokener.java b/subsonic-main/src/main/java/org/json/JSONTokener.java
new file mode 100644
index 00000000..f323f6e6
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONTokener.java
@@ -0,0 +1,446 @@
+package org.json;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * A JSONTokener takes a source string and extracts characters and tokens from
+ * it. It is used by the JSONObject and JSONArray constructors to parse
+ * JSON source strings.
+ * @author JSON.org
+ * @version 2011-11-24
+ */
+public class JSONTokener {
+
+ private int character;
+ private boolean eof;
+ private int index;
+ private int line;
+ private char previous;
+ private final Reader reader;
+ private boolean usePrevious;
+
+
+ /**
+ * Construct a JSONTokener from a Reader.
+ *
+ * @param reader A reader.
+ */
+ public JSONTokener(Reader reader) {
+ this.reader = reader.markSupported()
+ ? reader
+ : new BufferedReader(reader);
+ this.eof = false;
+ this.usePrevious = false;
+ this.previous = 0;
+ this.index = 0;
+ this.character = 1;
+ this.line = 1;
+ }
+
+
+ /**
+ * Construct a JSONTokener from an InputStream.
+ */
+ public JSONTokener(InputStream inputStream) throws JSONException {
+ this(new InputStreamReader(inputStream));
+ }
+
+
+ /**
+ * Construct a JSONTokener from a string.
+ *
+ * @param s A source string.
+ */
+ public JSONTokener(String s) {
+ this(new StringReader(s));
+ }
+
+
+ /**
+ * Back up one character. This provides a sort of lookahead capability,
+ * so that you can test for a digit or letter before attempting to parse
+ * the next number or identifier.
+ */
+ public void back() throws JSONException {
+ if (this.usePrevious || this.index <= 0) {
+ throw new JSONException("Stepping back two steps is not supported");
+ }
+ this.index -= 1;
+ this.character -= 1;
+ this.usePrevious = true;
+ this.eof = false;
+ }
+
+
+ /**
+ * Get the hex value of a character (base16).
+ * @param c A character between '0' and '9' or between 'A' and 'F' or
+ * between 'a' and 'f'.
+ * @return An int between 0 and 15, or -1 if c was not a hex digit.
+ */
+ public static int dehexchar(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - ('A' - 10);
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - ('a' - 10);
+ }
+ return -1;
+ }
+
+ public boolean end() {
+ return this.eof && !this.usePrevious;
+ }
+
+
+ /**
+ * Determine if the source string still contains characters that next()
+ * can consume.
+ * @return true if not yet at the end of the source.
+ */
+ public boolean more() throws JSONException {
+ this.next();
+ if (this.end()) {
+ return false;
+ }
+ this.back();
+ return true;
+ }
+
+
+ /**
+ * Get the next character in the source string.
+ *
+ * @return The next character, or 0 if past the end of the source string.
+ */
+ public char next() throws JSONException {
+ int c;
+ if (this.usePrevious) {
+ this.usePrevious = false;
+ c = this.previous;
+ } else {
+ try {
+ c = this.reader.read();
+ } catch (IOException exception) {
+ throw new JSONException(exception);
+ }
+
+ if (c <= 0) { // End of stream
+ this.eof = true;
+ c = 0;
+ }
+ }
+ this.index += 1;
+ if (this.previous == '\r') {
+ this.line += 1;
+ this.character = c == '\n' ? 0 : 1;
+ } else if (c == '\n') {
+ this.line += 1;
+ this.character = 0;
+ } else {
+ this.character += 1;
+ }
+ this.previous = (char) c;
+ return this.previous;
+ }
+
+
+ /**
+ * Consume the next character, and check that it matches a specified
+ * character.
+ * @param c The character to match.
+ * @return The character.
+ * @throws JSONException if the character does not match.
+ */
+ public char next(char c) throws JSONException {
+ char n = this.next();
+ if (n != c) {
+ throw this.syntaxError("Expected '" + c + "' and instead saw '" +
+ n + "'");
+ }
+ return n;
+ }
+
+
+ /**
+ * Get the next n characters.
+ *
+ * @param n The number of characters to take.
+ * @return A string of n characters.
+ * @throws JSONException
+ * Substring bounds error if there are not
+ * n characters remaining in the source string.
+ */
+ public String next(int n) throws JSONException {
+ if (n == 0) {
+ return "";
+ }
+
+ char[] chars = new char[n];
+ int pos = 0;
+
+ while (pos < n) {
+ chars[pos] = this.next();
+ if (this.end()) {
+ throw this.syntaxError("Substring bounds error");
+ }
+ pos += 1;
+ }
+ return new String(chars);
+ }
+
+
+ /**
+ * Get the next char in the string, skipping whitespace.
+ * @throws JSONException
+ * @return A character, or 0 if there are no more characters.
+ */
+ public char nextClean() throws JSONException {
+ for (;;) {
+ char c = this.next();
+ if (c == 0 || c > ' ') {
+ return c;
+ }
+ }
+ }
+
+
+ /**
+ * Return the characters up to the next close quote character.
+ * Backslash processing is done. The formal JSON format does not
+ * allow strings in single quotes, but an implementation is allowed to
+ * accept them.
+ * @param quote The quoting character, either
+ * <code>"</code>&nbsp;<small>(double quote)</small> or
+ * <code>'</code>&nbsp;<small>(single quote)</small>.
+ * @return A String.
+ * @throws JSONException Unterminated string.
+ */
+ public String nextString(char quote) throws JSONException {
+ char c;
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ c = this.next();
+ switch (c) {
+ case 0:
+ case '\n':
+ case '\r':
+ throw this.syntaxError("Unterminated string");
+ case '\\':
+ c = this.next();
+ switch (c) {
+ case 'b':
+ sb.append('\b');
+ break;
+ case 't':
+ sb.append('\t');
+ break;
+ case 'n':
+ sb.append('\n');
+ break;
+ case 'f':
+ sb.append('\f');
+ break;
+ case 'r':
+ sb.append('\r');
+ break;
+ case 'u':
+ sb.append((char)Integer.parseInt(this.next(4), 16));
+ break;
+ case '"':
+ case '\'':
+ case '\\':
+ case '/':
+ sb.append(c);
+ break;
+ default:
+ throw this.syntaxError("Illegal escape.");
+ }
+ break;
+ default:
+ if (c == quote) {
+ return sb.toString();
+ }
+ sb.append(c);
+ }
+ }
+ }
+
+
+ /**
+ * Get the text up but not including the specified character or the
+ * end of line, whichever comes first.
+ * @param delimiter A delimiter character.
+ * @return A string.
+ */
+ public String nextTo(char delimiter) throws JSONException {
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ char c = this.next();
+ if (c == delimiter || c == 0 || c == '\n' || c == '\r') {
+ if (c != 0) {
+ this.back();
+ }
+ return sb.toString().trim();
+ }
+ sb.append(c);
+ }
+ }
+
+
+ /**
+ * Get the text up but not including one of the specified delimiter
+ * characters or the end of line, whichever comes first.
+ * @param delimiters A set of delimiter characters.
+ * @return A string, trimmed.
+ */
+ public String nextTo(String delimiters) throws JSONException {
+ char c;
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ c = this.next();
+ if (delimiters.indexOf(c) >= 0 || c == 0 ||
+ c == '\n' || c == '\r') {
+ if (c != 0) {
+ this.back();
+ }
+ return sb.toString().trim();
+ }
+ sb.append(c);
+ }
+ }
+
+
+ /**
+ * Get the next value. The value can be a Boolean, Double, Integer,
+ * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
+ * @throws JSONException If syntax error.
+ *
+ * @return An object.
+ */
+ public Object nextValue() throws JSONException {
+ char c = this.nextClean();
+ String string;
+
+ switch (c) {
+ case '"':
+ case '\'':
+ return this.nextString(c);
+ case '{':
+ this.back();
+ return new JSONObject(this);
+ case '[':
+ this.back();
+ return new JSONArray(this);
+ }
+
+ /*
+ * Handle unquoted text. This could be the values true, false, or
+ * null, or it can be a number. An implementation (such as this one)
+ * is allowed to also accept non-standard forms.
+ *
+ * Accumulate characters until we reach the end of the text or a
+ * formatting character.
+ */
+
+ StringBuffer sb = new StringBuffer();
+ while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) {
+ sb.append(c);
+ c = this.next();
+ }
+ this.back();
+
+ string = sb.toString().trim();
+ if ("".equals(string)) {
+ throw this.syntaxError("Missing value");
+ }
+ return JSONObject.stringToValue(string);
+ }
+
+
+ /**
+ * Skip characters until the next character is the requested character.
+ * If the requested character is not found, no characters are skipped.
+ * @param to A character to skip to.
+ * @return The requested character, or zero if the requested character
+ * is not found.
+ */
+ public char skipTo(char to) throws JSONException {
+ char c;
+ try {
+ int startIndex = this.index;
+ int startCharacter = this.character;
+ int startLine = this.line;
+ this.reader.mark(Integer.MAX_VALUE);
+ do {
+ c = this.next();
+ if (c == 0) {
+ this.reader.reset();
+ this.index = startIndex;
+ this.character = startCharacter;
+ this.line = startLine;
+ return c;
+ }
+ } while (c != to);
+ } catch (IOException exc) {
+ throw new JSONException(exc);
+ }
+
+ this.back();
+ return c;
+ }
+
+
+ /**
+ * Make a JSONException to signal a syntax error.
+ *
+ * @param message The error message.
+ * @return A JSONException object, suitable for throwing
+ */
+ public JSONException syntaxError(String message) {
+ return new JSONException(message + this.toString());
+ }
+
+
+ /**
+ * Make a printable string of this JSONTokener.
+ *
+ * @return " at {index} [character {character} line {line}]"
+ */
+ public String toString() {
+ return " at " + this.index + " [character " + this.character + " line " +
+ this.line + "]";
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/org/json/JSONWriter.java b/subsonic-main/src/main/java/org/json/JSONWriter.java
new file mode 100644
index 00000000..35b60d90
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/JSONWriter.java
@@ -0,0 +1,327 @@
+package org.json;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/*
+Copyright (c) 2006 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * JSONWriter provides a quick and convenient way of producing JSON text.
+ * The texts produced strictly conform to JSON syntax rules. No whitespace is
+ * added, so the results are ready for transmission or storage. Each instance of
+ * JSONWriter can produce one JSON text.
+ * <p>
+ * A JSONWriter instance provides a <code>value</code> method for appending
+ * values to the
+ * text, and a <code>key</code>
+ * method for adding keys before values in objects. There are <code>array</code>
+ * and <code>endArray</code> methods that make and bound array values, and
+ * <code>object</code> and <code>endObject</code> methods which make and bound
+ * object values. All of these methods return the JSONWriter instance,
+ * permitting a cascade style. For example, <pre>
+ * new JSONWriter(myWriter)
+ * .object()
+ * .key("JSON")
+ * .value("Hello, World!")
+ * .endObject();</pre> which writes <pre>
+ * {"JSON":"Hello, World!"}</pre>
+ * <p>
+ * The first method called must be <code>array</code> or <code>object</code>.
+ * There are no methods for adding commas or colons. JSONWriter adds them for
+ * you. Objects and arrays can be nested up to 20 levels deep.
+ * <p>
+ * This can sometimes be easier than using a JSONObject to build a string.
+ * @author JSON.org
+ * @version 2011-11-24
+ */
+public class JSONWriter {
+ private static final int maxdepth = 200;
+
+ /**
+ * The comma flag determines if a comma should be output before the next
+ * value.
+ */
+ private boolean comma;
+
+ /**
+ * The current mode. Values:
+ * 'a' (array),
+ * 'd' (done),
+ * 'i' (initial),
+ * 'k' (key),
+ * 'o' (object).
+ */
+ protected char mode;
+
+ /**
+ * The object/array stack.
+ */
+ private final JSONObject stack[];
+
+ /**
+ * The stack top index. A value of 0 indicates that the stack is empty.
+ */
+ private int top;
+
+ /**
+ * The writer that will receive the output.
+ */
+ protected Writer writer;
+
+ /**
+ * Make a fresh JSONWriter. It can be used to build one JSON text.
+ */
+ public JSONWriter(Writer w) {
+ this.comma = false;
+ this.mode = 'i';
+ this.stack = new JSONObject[maxdepth];
+ this.top = 0;
+ this.writer = w;
+ }
+
+ /**
+ * Append a value.
+ * @param string A string value.
+ * @return this
+ * @throws JSONException If the value is out of sequence.
+ */
+ private JSONWriter append(String string) throws JSONException {
+ if (string == null) {
+ throw new JSONException("Null pointer");
+ }
+ if (this.mode == 'o' || this.mode == 'a') {
+ try {
+ if (this.comma && this.mode == 'a') {
+ this.writer.write(',');
+ }
+ this.writer.write(string);
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ if (this.mode == 'o') {
+ this.mode = 'k';
+ }
+ this.comma = true;
+ return this;
+ }
+ throw new JSONException("Value out of sequence.");
+ }
+
+ /**
+ * Begin appending a new array. All values until the balancing
+ * <code>endArray</code> will be appended to this array. The
+ * <code>endArray</code> method must be called to mark the array's end.
+ * @return this
+ * @throws JSONException If the nesting is too deep, or if the object is
+ * started in the wrong place (for example as a key or after the end of the
+ * outermost array or object).
+ */
+ public JSONWriter array() throws JSONException {
+ if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') {
+ this.push(null);
+ this.append("[");
+ this.comma = false;
+ return this;
+ }
+ throw new JSONException("Misplaced array.");
+ }
+
+ /**
+ * End something.
+ * @param mode Mode
+ * @param c Closing character
+ * @return this
+ * @throws JSONException If unbalanced.
+ */
+ private JSONWriter end(char mode, char c) throws JSONException {
+ if (this.mode != mode) {
+ throw new JSONException(mode == 'a'
+ ? "Misplaced endArray."
+ : "Misplaced endObject.");
+ }
+ this.pop(mode);
+ try {
+ this.writer.write(c);
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ this.comma = true;
+ return this;
+ }
+
+ /**
+ * End an array. This method most be called to balance calls to
+ * <code>array</code>.
+ * @return this
+ * @throws JSONException If incorrectly nested.
+ */
+ public JSONWriter endArray() throws JSONException {
+ return this.end('a', ']');
+ }
+
+ /**
+ * End an object. This method most be called to balance calls to
+ * <code>object</code>.
+ * @return this
+ * @throws JSONException If incorrectly nested.
+ */
+ public JSONWriter endObject() throws JSONException {
+ return this.end('k', '}');
+ }
+
+ /**
+ * Append a key. The key will be associated with the next value. In an
+ * object, every value must be preceded by a key.
+ * @param string A key string.
+ * @return this
+ * @throws JSONException If the key is out of place. For example, keys
+ * do not belong in arrays or if the key is null.
+ */
+ public JSONWriter key(String string) throws JSONException {
+ if (string == null) {
+ throw new JSONException("Null key.");
+ }
+ if (this.mode == 'k') {
+ try {
+ this.stack[this.top - 1].putOnce(string, Boolean.TRUE);
+ if (this.comma) {
+ this.writer.write(',');
+ }
+ this.writer.write(JSONObject.quote(string));
+ this.writer.write(':');
+ this.comma = false;
+ this.mode = 'o';
+ return this;
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ }
+ throw new JSONException("Misplaced key.");
+ }
+
+
+ /**
+ * Begin appending a new object. All keys and values until the balancing
+ * <code>endObject</code> will be appended to this object. The
+ * <code>endObject</code> method must be called to mark the object's end.
+ * @return this
+ * @throws JSONException If the nesting is too deep, or if the object is
+ * started in the wrong place (for example as a key or after the end of the
+ * outermost array or object).
+ */
+ public JSONWriter object() throws JSONException {
+ if (this.mode == 'i') {
+ this.mode = 'o';
+ }
+ if (this.mode == 'o' || this.mode == 'a') {
+ this.append("{");
+ this.push(new JSONObject());
+ this.comma = false;
+ return this;
+ }
+ throw new JSONException("Misplaced object.");
+
+ }
+
+
+ /**
+ * Pop an array or object scope.
+ * @param c The scope to close.
+ * @throws JSONException If nesting is wrong.
+ */
+ private void pop(char c) throws JSONException {
+ if (this.top <= 0) {
+ throw new JSONException("Nesting error.");
+ }
+ char m = this.stack[this.top - 1] == null ? 'a' : 'k';
+ if (m != c) {
+ throw new JSONException("Nesting error.");
+ }
+ this.top -= 1;
+ this.mode = this.top == 0
+ ? 'd'
+ : this.stack[this.top - 1] == null
+ ? 'a'
+ : 'k';
+ }
+
+ /**
+ * Push an array or object scope.
+ * @param c The scope to open.
+ * @throws JSONException If nesting is too deep.
+ */
+ private void push(JSONObject jo) throws JSONException {
+ if (this.top >= maxdepth) {
+ throw new JSONException("Nesting too deep.");
+ }
+ this.stack[this.top] = jo;
+ this.mode = jo == null ? 'a' : 'k';
+ this.top += 1;
+ }
+
+
+ /**
+ * Append either the value <code>true</code> or the value
+ * <code>false</code>.
+ * @param b A boolean.
+ * @return this
+ * @throws JSONException
+ */
+ public JSONWriter value(boolean b) throws JSONException {
+ return this.append(b ? "true" : "false");
+ }
+
+ /**
+ * Append a double value.
+ * @param d A double.
+ * @return this
+ * @throws JSONException If the number is not finite.
+ */
+ public JSONWriter value(double d) throws JSONException {
+ return this.value(new Double(d));
+ }
+
+ /**
+ * Append a long value.
+ * @param l A long.
+ * @return this
+ * @throws JSONException
+ */
+ public JSONWriter value(long l) throws JSONException {
+ return this.append(Long.toString(l));
+ }
+
+
+ /**
+ * Append an object value.
+ * @param object The object to append. It can be null, or a Boolean, Number,
+ * String, JSONObject, or JSONArray, or an object that implements JSONString.
+ * @return this
+ * @throws JSONException If the value is out of sequence.
+ */
+ public JSONWriter value(Object object) throws JSONException {
+ return this.append(JSONObject.valueToString(object));
+ }
+}
diff --git a/subsonic-main/src/main/java/org/json/XML.java b/subsonic-main/src/main/java/org/json/XML.java
new file mode 100644
index 00000000..82455b33
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/XML.java
@@ -0,0 +1,508 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import java.util.Iterator;
+
+
+/**
+ * This provides static methods to convert an XML text into a JSONObject,
+ * and to covert a JSONObject into an XML text.
+ * @author JSON.org
+ * @version 2011-02-11
+ */
+public class XML {
+
+ /** The Character '&'. */
+ public static final Character AMP = new Character('&');
+
+ /** The Character '''. */
+ public static final Character APOS = new Character('\'');
+
+ /** The Character '!'. */
+ public static final Character BANG = new Character('!');
+
+ /** The Character '='. */
+ public static final Character EQ = new Character('=');
+
+ /** The Character '>'. */
+ public static final Character GT = new Character('>');
+
+ /** The Character '<'. */
+ public static final Character LT = new Character('<');
+
+ /** The Character '?'. */
+ public static final Character QUEST = new Character('?');
+
+ /** The Character '"'. */
+ public static final Character QUOT = new Character('"');
+
+ /** The Character '/'. */
+ public static final Character SLASH = new Character('/');
+
+ /**
+ * Replace special characters with XML escapes:
+ * <pre>
+ * &amp; <small>(ampersand)</small> is replaced by &amp;amp;
+ * &lt; <small>(less than)</small> is replaced by &amp;lt;
+ * &gt; <small>(greater than)</small> is replaced by &amp;gt;
+ * &quot; <small>(double quote)</small> is replaced by &amp;quot;
+ * </pre>
+ * @param string The string to be escaped.
+ * @return The escaped string.
+ */
+ public static String escape(String string) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0, length = string.length(); i < length; i++) {
+ char c = string.charAt(i);
+ switch (c) {
+ case '&':
+ sb.append("&amp;");
+ break;
+ case '<':
+ sb.append("&lt;");
+ break;
+ case '>':
+ sb.append("&gt;");
+ break;
+ case '"':
+ sb.append("&quot;");
+ break;
+ case '\'':
+ sb.append("&apos;");
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Throw an exception if the string contains whitespace.
+ * Whitespace is not allowed in tagNames and attributes.
+ * @param string
+ * @throws JSONException
+ */
+ public static void noSpace(String string) throws JSONException {
+ int i, length = string.length();
+ if (length == 0) {
+ throw new JSONException("Empty string.");
+ }
+ for (i = 0; i < length; i += 1) {
+ if (Character.isWhitespace(string.charAt(i))) {
+ throw new JSONException("'" + string +
+ "' contains a space character.");
+ }
+ }
+ }
+
+ /**
+ * Scan the content following the named tag, attaching it to the context.
+ * @param x The XMLTokener containing the source string.
+ * @param context The JSONObject that will include the new material.
+ * @param name The tag name.
+ * @return true if the close tag is processed.
+ * @throws JSONException
+ */
+ private static boolean parse(XMLTokener x, JSONObject context,
+ String name) throws JSONException {
+ char c;
+ int i;
+ JSONObject jsonobject = null;
+ String string;
+ String tagName;
+ Object token;
+
+// Test for and skip past these forms:
+// <!-- ... -->
+// <! ... >
+// <![ ... ]]>
+// <? ... ?>
+// Report errors for these forms:
+// <>
+// <=
+// <<
+
+ token = x.nextToken();
+
+// <!
+
+ if (token == BANG) {
+ c = x.next();
+ if (c == '-') {
+ if (x.next() == '-') {
+ x.skipPast("-->");
+ return false;
+ }
+ x.back();
+ } else if (c == '[') {
+ token = x.nextToken();
+ if ("CDATA".equals(token)) {
+ if (x.next() == '[') {
+ string = x.nextCDATA();
+ if (string.length() > 0) {
+ context.accumulate("content", string);
+ }
+ return false;
+ }
+ }
+ throw x.syntaxError("Expected 'CDATA['");
+ }
+ i = 1;
+ do {
+ token = x.nextMeta();
+ if (token == null) {
+ throw x.syntaxError("Missing '>' after '<!'.");
+ } else if (token == LT) {
+ i += 1;
+ } else if (token == GT) {
+ i -= 1;
+ }
+ } while (i > 0);
+ return false;
+ } else if (token == QUEST) {
+
+// <?
+
+ x.skipPast("?>");
+ return false;
+ } else if (token == SLASH) {
+
+// Close tag </
+
+ token = x.nextToken();
+ if (name == null) {
+ throw x.syntaxError("Mismatched close tag " + token);
+ }
+ if (!token.equals(name)) {
+ throw x.syntaxError("Mismatched " + name + " and " + token);
+ }
+ if (x.nextToken() != GT) {
+ throw x.syntaxError("Misshaped close tag");
+ }
+ return true;
+
+ } else if (token instanceof Character) {
+ throw x.syntaxError("Misshaped tag");
+
+// Open tag <
+
+ } else {
+ tagName = (String)token;
+ token = null;
+ jsonobject = new JSONObject();
+ for (;;) {
+ if (token == null) {
+ token = x.nextToken();
+ }
+
+// attribute = value
+
+ if (token instanceof String) {
+ string = (String)token;
+ token = x.nextToken();
+ if (token == EQ) {
+ token = x.nextToken();
+ if (!(token instanceof String)) {
+ throw x.syntaxError("Missing value");
+ }
+ jsonobject.accumulate(string,
+ XML.stringToValue((String)token));
+ token = null;
+ } else {
+ jsonobject.accumulate(string, "");
+ }
+
+// Empty tag <.../>
+
+ } else if (token == SLASH) {
+ if (x.nextToken() != GT) {
+ throw x.syntaxError("Misshaped tag");
+ }
+ if (jsonobject.length() > 0) {
+ context.accumulate(tagName, jsonobject);
+ } else {
+ context.accumulate(tagName, "");
+ }
+ return false;
+
+// Content, between <...> and </...>
+
+ } else if (token == GT) {
+ for (;;) {
+ token = x.nextContent();
+ if (token == null) {
+ if (tagName != null) {
+ throw x.syntaxError("Unclosed tag " + tagName);
+ }
+ return false;
+ } else if (token instanceof String) {
+ string = (String)token;
+ if (string.length() > 0) {
+ jsonobject.accumulate("content",
+ XML.stringToValue(string));
+ }
+
+// Nested element
+
+ } else if (token == LT) {
+ if (parse(x, jsonobject, tagName)) {
+ if (jsonobject.length() == 0) {
+ context.accumulate(tagName, "");
+ } else if (jsonobject.length() == 1 &&
+ jsonobject.opt("content") != null) {
+ context.accumulate(tagName,
+ jsonobject.opt("content"));
+ } else {
+ context.accumulate(tagName, jsonobject);
+ }
+ return false;
+ }
+ }
+ }
+ } else {
+ throw x.syntaxError("Misshaped tag");
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Try to convert a string into a number, boolean, or null. If the string
+ * can't be converted, return the string. This is much less ambitious than
+ * JSONObject.stringToValue, especially because it does not attempt to
+ * convert plus forms, octal forms, hex forms, or E forms lacking decimal
+ * points.
+ * @param string A String.
+ * @return A simple JSON value.
+ */
+ public static Object stringToValue(String string) {
+ if ("".equals(string)) {
+ return string;
+ }
+ if ("true".equalsIgnoreCase(string)) {
+ return Boolean.TRUE;
+ }
+ if ("false".equalsIgnoreCase(string)) {
+ return Boolean.FALSE;
+ }
+ if ("null".equalsIgnoreCase(string)) {
+ return JSONObject.NULL;
+ }
+ if ("0".equals(string)) {
+ return new Integer(0);
+ }
+
+// If it might be a number, try converting it. If that doesn't work,
+// return the string.
+
+ try {
+ char initial = string.charAt(0);
+ boolean negative = false;
+ if (initial == '-') {
+ initial = string.charAt(1);
+ negative = true;
+ }
+ if (initial == '0' && string.charAt(negative ? 2 : 1) == '0') {
+ return string;
+ }
+ if ((initial >= '0' && initial <= '9')) {
+ if (string.indexOf('.') >= 0) {
+ return Double.valueOf(string);
+ } else if (string.indexOf('e') < 0 && string.indexOf('E') < 0) {
+ Long myLong = new Long(string);
+ if (myLong.longValue() == myLong.intValue()) {
+ return new Integer(myLong.intValue());
+ } else {
+ return myLong;
+ }
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ return string;
+ }
+
+
+ /**
+ * Convert a well-formed (but not necessarily valid) XML string into a
+ * JSONObject. Some information may be lost in this transformation
+ * because JSON is a data format and XML is a document format. XML uses
+ * elements, attributes, and content text, while JSON uses unordered
+ * collections of name/value pairs and arrays of values. JSON does not
+ * does not like to distinguish between elements and attributes.
+ * Sequences of similar elements are represented as JSONArrays. Content
+ * text may be placed in a "content" member. Comments, prologs, DTDs, and
+ * <code>&lt;[ [ ]]></code> are ignored.
+ * @param string The source string.
+ * @return A JSONObject containing the structured data from the XML string.
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(String string) throws JSONException {
+ JSONObject jo = new JSONObject();
+ XMLTokener x = new XMLTokener(string);
+ while (x.more() && x.skipPast("<")) {
+ parse(x, jo, null);
+ }
+ return jo;
+ }
+
+
+ /**
+ * Convert a JSONObject into a well-formed, element-normal XML string.
+ * @param object A JSONObject.
+ * @return A string.
+ * @throws JSONException
+ */
+ public static String toString(Object object) throws JSONException {
+ return toString(object, null);
+ }
+
+
+ /**
+ * Convert a JSONObject into a well-formed, element-normal XML string.
+ * @param object A JSONObject.
+ * @param tagName The optional name of the enclosing tag.
+ * @return A string.
+ * @throws JSONException
+ */
+ public static String toString(Object object, String tagName)
+ throws JSONException {
+ StringBuffer sb = new StringBuffer();
+ int i;
+ JSONArray ja;
+ JSONObject jo;
+ String key;
+ Iterator keys;
+ int length;
+ String string;
+ Object value;
+ if (object instanceof JSONObject) {
+
+// Emit <tagName>
+
+ if (tagName != null) {
+ sb.append('<');
+ sb.append(tagName);
+ sb.append('>');
+ }
+
+// Loop thru the keys.
+
+ jo = (JSONObject)object;
+ keys = jo.keys();
+ while (keys.hasNext()) {
+ key = keys.next().toString();
+ value = jo.opt(key);
+ if (value == null) {
+ value = "";
+ }
+ if (value instanceof String) {
+ string = (String)value;
+ } else {
+ string = null;
+ }
+
+// Emit content in body
+
+ if ("content".equals(key)) {
+ if (value instanceof JSONArray) {
+ ja = (JSONArray)value;
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ if (i > 0) {
+ sb.append('\n');
+ }
+ sb.append(escape(ja.get(i).toString()));
+ }
+ } else {
+ sb.append(escape(value.toString()));
+ }
+
+// Emit an array of similar keys
+
+ } else if (value instanceof JSONArray) {
+ ja = (JSONArray)value;
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ value = ja.get(i);
+ if (value instanceof JSONArray) {
+ sb.append('<');
+ sb.append(key);
+ sb.append('>');
+ sb.append(toString(value));
+ sb.append("</");
+ sb.append(key);
+ sb.append('>');
+ } else {
+ sb.append(toString(value, key));
+ }
+ }
+ } else if ("".equals(value)) {
+ sb.append('<');
+ sb.append(key);
+ sb.append("/>");
+
+// Emit a new tag <k>
+
+ } else {
+ sb.append(toString(value, key));
+ }
+ }
+ if (tagName != null) {
+
+// Emit the </tagname> close tag
+
+ sb.append("</");
+ sb.append(tagName);
+ sb.append('>');
+ }
+ return sb.toString();
+
+// XML does not have good support for arrays. If an array appears in a place
+// where XML is lacking, synthesize an <array> element.
+
+ } else {
+ if (object.getClass().isArray()) {
+ object = new JSONArray(object);
+ }
+ if (object instanceof JSONArray) {
+ ja = (JSONArray)object;
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ sb.append(toString(ja.opt(i), tagName == null ? "array" : tagName));
+ }
+ return sb.toString();
+ } else {
+ string = (object == null) ? "null" : escape(object.toString());
+ return (tagName == null) ? "\"" + string + "\"" :
+ (string.length() == 0) ? "<" + tagName + "/>" :
+ "<" + tagName + ">" + string + "</" + tagName + ">";
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/java/org/json/XMLTokener.java b/subsonic-main/src/main/java/org/json/XMLTokener.java
new file mode 100644
index 00000000..c7ca95f2
--- /dev/null
+++ b/subsonic-main/src/main/java/org/json/XMLTokener.java
@@ -0,0 +1,365 @@
+package org.json;
+
+/*
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/**
+ * The XMLTokener extends the JSONTokener to provide additional methods
+ * for the parsing of XML texts.
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class XMLTokener extends JSONTokener {
+
+
+ /** The table of entity values. It initially contains Character values for
+ * amp, apos, gt, lt, quot.
+ */
+ public static final java.util.HashMap entity;
+
+ static {
+ entity = new java.util.HashMap(8);
+ entity.put("amp", XML.AMP);
+ entity.put("apos", XML.APOS);
+ entity.put("gt", XML.GT);
+ entity.put("lt", XML.LT);
+ entity.put("quot", XML.QUOT);
+ }
+
+ /**
+ * Construct an XMLTokener from a string.
+ * @param s A source string.
+ */
+ public XMLTokener(String s) {
+ super(s);
+ }
+
+ /**
+ * Get the text in the CDATA block.
+ * @return The string up to the <code>]]&gt;</code>.
+ * @throws JSONException If the <code>]]&gt;</code> is not found.
+ */
+ public String nextCDATA() throws JSONException {
+ char c;
+ int i;
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ c = next();
+ if (end()) {
+ throw syntaxError("Unclosed CDATA");
+ }
+ sb.append(c);
+ i = sb.length() - 3;
+ if (i >= 0 && sb.charAt(i) == ']' &&
+ sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') {
+ sb.setLength(i);
+ return sb.toString();
+ }
+ }
+ }
+
+
+ /**
+ * Get the next XML outer token, trimming whitespace. There are two kinds
+ * of tokens: the '<' character which begins a markup tag, and the content
+ * text between markup tags.
+ *
+ * @return A string, or a '<' Character, or null if there is no more
+ * source text.
+ * @throws JSONException
+ */
+ public Object nextContent() throws JSONException {
+ char c;
+ StringBuffer sb;
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ if (c == 0) {
+ return null;
+ }
+ if (c == '<') {
+ return XML.LT;
+ }
+ sb = new StringBuffer();
+ for (;;) {
+ if (c == '<' || c == 0) {
+ back();
+ return sb.toString().trim();
+ }
+ if (c == '&') {
+ sb.append(nextEntity(c));
+ } else {
+ sb.append(c);
+ }
+ c = next();
+ }
+ }
+
+
+ /**
+ * Return the next entity. These entities are translated to Characters:
+ * <code>&amp; &apos; &gt; &lt; &quot;</code>.
+ * @param ampersand An ampersand character.
+ * @return A Character or an entity String if the entity is not recognized.
+ * @throws JSONException If missing ';' in XML entity.
+ */
+ public Object nextEntity(char ampersand) throws JSONException {
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ char c = next();
+ if (Character.isLetterOrDigit(c) || c == '#') {
+ sb.append(Character.toLowerCase(c));
+ } else if (c == ';') {
+ break;
+ } else {
+ throw syntaxError("Missing ';' in XML entity: &" + sb);
+ }
+ }
+ String string = sb.toString();
+ Object object = entity.get(string);
+ return object != null ? object : ampersand + string + ";";
+ }
+
+
+ /**
+ * Returns the next XML meta token. This is used for skipping over <!...>
+ * and <?...?> structures.
+ * @return Syntax characters (<code>< > / = ! ?</code>) are returned as
+ * Character, and strings and names are returned as Boolean. We don't care
+ * what the values actually are.
+ * @throws JSONException If a string is not properly closed or if the XML
+ * is badly structured.
+ */
+ public Object nextMeta() throws JSONException {
+ char c;
+ char q;
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ switch (c) {
+ case 0:
+ throw syntaxError("Misshaped meta tag");
+ case '<':
+ return XML.LT;
+ case '>':
+ return XML.GT;
+ case '/':
+ return XML.SLASH;
+ case '=':
+ return XML.EQ;
+ case '!':
+ return XML.BANG;
+ case '?':
+ return XML.QUEST;
+ case '"':
+ case '\'':
+ q = c;
+ for (;;) {
+ c = next();
+ if (c == 0) {
+ throw syntaxError("Unterminated string");
+ }
+ if (c == q) {
+ return Boolean.TRUE;
+ }
+ }
+ default:
+ for (;;) {
+ c = next();
+ if (Character.isWhitespace(c)) {
+ return Boolean.TRUE;
+ }
+ switch (c) {
+ case 0:
+ case '<':
+ case '>':
+ case '/':
+ case '=':
+ case '!':
+ case '?':
+ case '"':
+ case '\'':
+ back();
+ return Boolean.TRUE;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Get the next XML Token. These tokens are found inside of angle
+ * brackets. It may be one of these characters: <code>/ > = ! ?</code> or it
+ * may be a string wrapped in single quotes or double quotes, or it may be a
+ * name.
+ * @return a String or a Character.
+ * @throws JSONException If the XML is not well formed.
+ */
+ public Object nextToken() throws JSONException {
+ char c;
+ char q;
+ StringBuffer sb;
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ switch (c) {
+ case 0:
+ throw syntaxError("Misshaped element");
+ case '<':
+ throw syntaxError("Misplaced '<'");
+ case '>':
+ return XML.GT;
+ case '/':
+ return XML.SLASH;
+ case '=':
+ return XML.EQ;
+ case '!':
+ return XML.BANG;
+ case '?':
+ return XML.QUEST;
+
+// Quoted string
+
+ case '"':
+ case '\'':
+ q = c;
+ sb = new StringBuffer();
+ for (;;) {
+ c = next();
+ if (c == 0) {
+ throw syntaxError("Unterminated string");
+ }
+ if (c == q) {
+ return sb.toString();
+ }
+ if (c == '&') {
+ sb.append(nextEntity(c));
+ } else {
+ sb.append(c);
+ }
+ }
+ default:
+
+// Name
+
+ sb = new StringBuffer();
+ for (;;) {
+ sb.append(c);
+ c = next();
+ if (Character.isWhitespace(c)) {
+ return sb.toString();
+ }
+ switch (c) {
+ case 0:
+ return sb.toString();
+ case '>':
+ case '/':
+ case '=':
+ case '!':
+ case '?':
+ case '[':
+ case ']':
+ back();
+ return sb.toString();
+ case '<':
+ case '"':
+ case '\'':
+ throw syntaxError("Bad character in a name");
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Skip characters until past the requested string.
+ * If it is not found, we are left at the end of the source with a result of false.
+ * @param to A string to skip past.
+ * @throws JSONException
+ */
+ public boolean skipPast(String to) throws JSONException {
+ boolean b;
+ char c;
+ int i;
+ int j;
+ int offset = 0;
+ int length = to.length();
+ char[] circle = new char[length];
+
+ /*
+ * First fill the circle buffer with as many characters as are in the
+ * to string. If we reach an early end, bail.
+ */
+
+ for (i = 0; i < length; i += 1) {
+ c = next();
+ if (c == 0) {
+ return false;
+ }
+ circle[i] = c;
+ }
+ /*
+ * We will loop, possibly for all of the remaining characters.
+ */
+ for (;;) {
+ j = offset;
+ b = true;
+ /*
+ * Compare the circle buffer with the to string.
+ */
+ for (i = 0; i < length; i += 1) {
+ if (circle[j] != to.charAt(i)) {
+ b = false;
+ break;
+ }
+ j += 1;
+ if (j >= length) {
+ j -= length;
+ }
+ }
+ /*
+ * If we exit the loop with b intact, then victory is ours.
+ */
+ if (b) {
+ return true;
+ }
+ /*
+ * Get the next character. If there isn't one, then defeat is ours.
+ */
+ c = next();
+ if (c == 0) {
+ return false;
+ }
+ /*
+ * Shove the character in the circle buffer and advance the
+ * circle offset. The offset is mod n.
+ */
+ circle[offset] = c;
+ offset += 1;
+ if (offset >= length) {
+ offset -= length;
+ }
+ }
+ }
+}