aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main
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
parentb61d787706979e7e20f4c3c4f93c1f129d92273f (diff)
downloaddsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.gz
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.bz2
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.zip
Initial Commit
Diffstat (limited to 'subsonic-main/src/main')
-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
-rw-r--r--subsonic-main/src/main/resources/ehcache.xml297
-rw-r--r--subsonic-main/src/main/resources/log4j.properties9
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpgbin0 -> 23091 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.pngbin0 -> 5440 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.pngbin0 -> 4480 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.pngbin0 -> 6082 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.pngbin0 -> 4175 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.pngbin0 -> 6041 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.pngbin0 -> 4845 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.pngbin0 -> 4713 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.pngbin0 -> 4406 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.pngbin0 -> 2746 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.pngbin0 -> 5474 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.pngbin0 -> 3897 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.pngbin0 -> 5909 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.pngbin0 -> 5787 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.pngbin0 -> 3423 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.pngbin0 -> 4197 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.pngbin0 -> 5274 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.pngbin0 -> 6144 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.pngbin0 -> 5896 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.pngbin0 -> 5220 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.pngbin0 -> 4636 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.pngbin0 -> 6065 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.pngbin0 -> 4671 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.pngbin0 -> 5304 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.pngbin0 -> 4245 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.pngbin0 -> 5190 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.pngbin0 -> 5257 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.pngbin0 -> 3666 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.pngbin0 -> 5046 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.pngbin0 -> 5446 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.pngbin0 -> 2680 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.pngbin0 -> 3031 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.pngbin0 -> 3084 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.pngbin0 -> 2416 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.pngbin0 -> 4842 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.pngbin0 -> 5015 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.pngbin0 -> 4982 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.pngbin0 -> 5148 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.pngbin0 -> 5371 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.pngbin0 -> 4941 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.pngbin0 -> 5002 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.pngbin0 -> 4993 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.pngbin0 -> 6379 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.pngbin0 -> 2596 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.pngbin0 -> 5010 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.pngbin0 -> 3253 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.pngbin0 -> 4975 bytes
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties702
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties753
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties749
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties704
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_de.properties686
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_el.properties698
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en.properties785
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en_GB.properties51
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_es.properties427
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_et.properties771
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fi.properties675
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fr.properties682
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_is.properties635
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_it.properties684
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ja_JP.properties641
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ko.properties679
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_mk.properties302
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_nl.properties759
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_no.properties640
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pl.properties729
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pt.properties686
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ru.properties664
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sl.properties785
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sv.properties720
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_CN.properties94
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_TW.properties677
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/locales.txt83
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/2010.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/barents.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/black.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/buuftheme.properties48
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/coolandclean.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/default.properties53
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/denim.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove_simple.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd1080.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd720.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd768.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hicon.properties20
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hiconi.properties20
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hitech.properties20
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnight.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnightfun.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome.properties11
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome_black.properties12
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/pinkpanther.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/ripserver.properties43
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sandstorm.properties10
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/simplify.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/slick.properties20
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic.properties44
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_blue.properties45
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_white.properties44
-rw-r--r--subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/themes.txt56
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/applicationContext-cache.xml22
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/applicationContext-security.xml234
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/applicationContext-service.xml245
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/dwr.xml62
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/accessDenied.jsp22
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp142
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/allmusic.jsp16
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/avatarUploadResult.jsp35
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/changeCoverArt.jsp206
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/coverArt.jsp86
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/createShare.jsp51
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/db.jsp45
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/donate.jsp147
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/editTags.jsp164
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/externalPlayer.jsp99
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/generalSettings.jsp165
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/gettingStarted.jsp53
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/head.jsp11
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp70
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/helpToolTip.jsp18
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/home.jsp189
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/importPlaylist.jsp37
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/include.jsp8
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/index.jsp26
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/internetRadioSettings.jsp62
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/jquery.jsp3
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp168
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/login.jsp64
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/lyrics.jsp79
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/main.jsp479
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/more.jsp159
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/musicFolderSettings.jsp114
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/networkSettings.jsp107
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/notFound.jsp21
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/passwordSettings.jsp45
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp228
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/playAddDownload.jsp64
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp617
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/playerSettings.jsp177
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/playlist.jsp235
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/podcast.jsp26
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/podcastReceiver.jsp269
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/podcastSettings.jsp88
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/rating.jsp51
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/recover.jsp34
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/reload.jsp11
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/rest/videoPlayer.jsp142
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/right.jsp191
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/search.jsp150
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/settingsHeader.jsp32
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/shareSettings.jsp72
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/starred.jsp131
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/status.jsp93
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/test.jsp20
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/top.jsp98
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/transcodingSettings.jsp70
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/upload.jsp29
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/userSettings.jsp201
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/videoPlayer.jsp190
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/browse.jsp56
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/head.jsp10
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/index.jsp62
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/loadPlaylist.jsp23
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/playlist.jsp56
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/search.jsp19
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/searchResult.jsp30
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/wap/settings.jsp47
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/jsp/xspfPlaylist.jsp30
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/sub.tld114
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/subsonic-servlet.xml479
-rw-r--r--subsonic-main/src/main/webapp/WEB-INF/web.xml207
-rw-r--r--subsonic-main/src/main/webapp/ad/omakasa.html5
-rw-r--r--subsonic-main/src/main/webapp/crossdomain.xml6
-rw-r--r--subsonic-main/src/main/webapp/error.jsp48
-rw-r--r--subsonic-main/src/main/webapp/flash/jw-player-5.6.swfbin0 -> 104032 bytes
-rw-r--r--subsonic-main/src/main/webapp/flash/whotube.zipbin0 -> 21255 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/add.gifbin0 -> 69 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/android.pngbin0 -> 3154 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/back.gifbin0 -> 890 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/add.pngbin0 -> 1048 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/android.pngbin0 -> 988 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/back.pngbin0 -> 794 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/background_main.pngbin0 -> 158474 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/clear_rating.pngbin0 -> 976 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/donate.pngbin0 -> 2541 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/donate_small.pngbin0 -> 715 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/down.pngbin0 -> 1009 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/download.pngbin0 -> 1020 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/error.pngbin0 -> 2276 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/forward.pngbin0 -> 788 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/gpl.pngbin0 -> 4555 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/help.pngbin0 -> 2850 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/help_small.pngbin0 -> 1107 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/home.pngbin0 -> 2711 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/home_hover.pngbin0 -> 633 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/list_heading.pngbin0 -> 330 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/log.pngbin0 -> 1090 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/logo.pngbin0 -> 14018 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/more.pngbin0 -> 2629 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/now_playing.pngbin0 -> 1038 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/paypal.gifbin0 -> 7515 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/phone.pngbin0 -> 884 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/play.pngbin0 -> 1038 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/playing.pngbin0 -> 2372 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/podcast.pngbin0 -> 2902 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/podcast_small.pngbin0 -> 1064 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/progress.pngbin0 -> 155 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/random.pngbin0 -> 1110 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/rating_half.pngbin0 -> 843 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/rating_off.pngbin0 -> 605 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/rating_on.pngbin0 -> 776 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/remove.pngbin0 -> 952 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/search.pngbin0 -> 2446 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/settings.pngbin0 -> 2709 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/status.pngbin0 -> 2214 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/subair.pngbin0 -> 744 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/up.pngbin0 -> 961 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/upload.pngbin0 -> 1009 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/buuftheme/wap.pngbin0 -> 967 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/clearRating.pngbin0 -> 545 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/add.pngbin0 -> 3247 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/back.pngbin0 -> 500 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/background.pngbin0 -> 3214 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/background_main.pngbin0 -> 221782 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/clear_rating.pngbin0 -> 656 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/down.pngbin0 -> 503 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/download.pngbin0 -> 3330 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/error.pngbin0 -> 1183 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/forward.pngbin0 -> 499 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/help.pngbin0 -> 4110 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/help_small.pngbin0 -> 3322 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/home.pngbin0 -> 4105 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/home_hover.pngbin0 -> 4665 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/list_heading.pngbin0 -> 316 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/log.pngbin0 -> 651 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/logo.pngbin0 -> 10261 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/more.pngbin0 -> 4297 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/now_playing.pngbin0 -> 879 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/phone.pngbin0 -> 3434 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/play.pngbin0 -> 3318 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/playing.pngbin0 -> 3879 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/podcast.pngbin0 -> 4696 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/podcast_small.pngbin0 -> 3473 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/progress.pngbin0 -> 155 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/random.pngbin0 -> 819 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/rating_half.pngbin0 -> 511 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/rating_on.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/remove.pngbin0 -> 3153 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/search.pngbin0 -> 4339 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/settings.pngbin0 -> 4401 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/status.pngbin0 -> 3999 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/up.pngbin0 -> 623 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/coolandclean/upload.pngbin0 -> 3355 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/current.gifbin0 -> 348 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/add.pngbin0 -> 644 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/back.pngbin0 -> 713 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/clear_rating.pngbin0 -> 2937 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/down.pngbin0 -> 609 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/download.pngbin0 -> 649 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/error.pngbin0 -> 3334 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/forward.pngbin0 -> 723 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/help.pngbin0 -> 1238 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/help_small.pngbin0 -> 812 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/home.pngbin0 -> 1064 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/log.pngbin0 -> 657 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/logo.pngbin0 -> 5494 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/more.pngbin0 -> 784 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/now_playing.pngbin0 -> 761 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/phone.pngbin0 -> 722 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/play.pngbin0 -> 610 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/playing.pngbin0 -> 1317 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/podcast.pngbin0 -> 1489 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/podcast_small.pngbin0 -> 876 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/random.pngbin0 -> 3099 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/rating_half.pngbin0 -> 777 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/rating_on.pngbin0 -> 845 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/remove.pngbin0 -> 633 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/search.pngbin0 -> 1198 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/settings.pngbin0 -> 1111 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/status.pngbin0 -> 1675 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/up.pngbin0 -> 610 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/denim/upload.pngbin0 -> 645 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/donate.pngbin0 -> 1036 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/donate_small.pngbin0 -> 585 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/down.gifbin0 -> 856 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/download.gifbin0 -> 855 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/error.pngbin0 -> 954 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/forward.gifbin0 -> 889 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/gpl.pngbin0 -> 2986 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/add.pngbin0 -> 2927 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/back.pngbin0 -> 2945 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/background_main.pngbin0 -> 230821 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/background_main_blank.pngbin0 -> 2303 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/clear_rating.pngbin0 -> 2976 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/down.pngbin0 -> 2970 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/download.pngbin0 -> 2970 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/error.pngbin0 -> 3322 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/forward.pngbin0 -> 2903 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/help.pngbin0 -> 2338 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/help_small.pngbin0 -> 1098 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/home.pngbin0 -> 1244 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/log.pngbin0 -> 792 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/logo.pngbin0 -> 10523 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/more.pngbin0 -> 1798 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/now_playing.pngbin0 -> 1359 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/phone.pngbin0 -> 722 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/play.pngbin0 -> 2970 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/playing.pngbin0 -> 1686 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/podcast.pngbin0 -> 1813 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/podcast_small.pngbin0 -> 897 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/random.pngbin0 -> 3161 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/rating_half.pngbin0 -> 762 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/rating_on.pngbin0 -> 830 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/remove.pngbin0 -> 2862 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/search.pngbin0 -> 1071 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/settings.pngbin0 -> 1840 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/status.pngbin0 -> 1181 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/up.pngbin0 -> 2941 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/groove/upload.pngbin0 -> 2941 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hd/background.pngbin0 -> 519958 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/help.pngbin0 -> 1484 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/help_small.pngbin0 -> 932 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/favicon.icobin0 -> 61798 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/help.pngbin0 -> 3136 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/home.pngbin0 -> 1300 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/more.pngbin0 -> 3166 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/now_playing.pngbin0 -> 3090 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/podcast_large.pngbin0 -> 3142 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/settings.pngbin0 -> 3244 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/status.pngbin0 -> 3101 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hicon/subsonic.pngbin0 -> 8114 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/Untitled-1.icobin0 -> 61798 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/help.pngbin0 -> 3169 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/home.pngbin0 -> 1314 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/more.pngbin0 -> 3200 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/now_playing.pngbin0 -> 3121 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/podcast_large.pngbin0 -> 3166 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/settings.pngbin0 -> 3280 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/status.pngbin0 -> 3113 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hiconi/subsonic.pngbin0 -> 7596 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/bg.jpgbin0 -> 11283 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/bg2.jpgbin0 -> 67101 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/favicon.icobin0 -> 61798 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/help.pngbin0 -> 3668 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/home.pngbin0 -> 3698 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/more.pngbin0 -> 3871 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/now_playing.pngbin0 -> 3624 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/podcast_large.pngbin0 -> 3898 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/settings.pngbin0 -> 3692 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/status.pngbin0 -> 3441 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/hitech/subsonic.pngbin0 -> 6984 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/home.pngbin0 -> 800 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/log.pngbin0 -> 992 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnight/back.pngbin0 -> 1251 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnight/forward.pngbin0 -> 1251 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/error.pngbin0 -> 1671 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_Now_Playing.pngbin0 -> 879 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_add.pngbin0 -> 371 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_back.pngbin0 -> 694 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_background.gifbin0 -> 9367 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_clear_Rating.pngbin0 -> 1139 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_donate.pngbin0 -> 5752 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_down.pngbin0 -> 630 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_download.pngbin0 -> 749 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls.jpgbin0 -> 9672 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls_hover.jpgbin0 -> 24098 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_forward.pngbin0 -> 693 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help.pngbin0 -> 1641 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help_small.pngbin0 -> 694 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home.pngbin0 -> 3453 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home_hover.jpgbin0 -> 14915 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_log.pngbin0 -> 820 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo.pngbin0 -> 9472 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo_favicon.icobin0 -> 894 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_more.pngbin0 -> 793 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_phone.pngbin0 -> 587 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_play.pngbin0 -> 934 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_playing.pngbin0 -> 1852 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast.pngbin0 -> 1369 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast_small.pngbin0 -> 666 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_random.pngbin0 -> 739 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_remove.pngbin0 -> 391 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_search.pngbin0 -> 2991 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_settings.pngbin0 -> 2148 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_status.pngbin0 -> 2007 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_table.jpgbin0 -> 1648 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_text_back.jpgbin0 -> 1921 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_up.pngbin0 -> 584 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_upload.pngbin0 -> 639 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/progress.pngbin0 -> 155 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/ratingHalf.pngbin0 -> 1092 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/ratingOff.pngbin0 -> 1029 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/midnightfun/ratingOn.pngbin0 -> 1230 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/monochrome/subdot.pngbin0 -> 6900 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/more.pngbin0 -> 1418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/now_playing.pngbin0 -> 1131 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/paypal.gifbin0 -> 2127 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/add.pngbin0 -> 644 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/back.pngbin0 -> 713 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/clear_rating.pngbin0 -> 2937 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/down.pngbin0 -> 609 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/download.pngbin0 -> 649 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/error.pngbin0 -> 3334 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/forward.pngbin0 -> 723 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/help.pngbin0 -> 1238 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/help_small.pngbin0 -> 812 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/home.pngbin0 -> 1064 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/log.pngbin0 -> 657 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/logo.pngbin0 -> 9827 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/more.pngbin0 -> 784 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/now_playing.pngbin0 -> 761 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/phone.pngbin0 -> 722 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/play.pngbin0 -> 610 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/playing.pngbin0 -> 1317 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/podcast.pngbin0 -> 1489 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/podcast_small.pngbin0 -> 876 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/random.pngbin0 -> 3099 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/rating_half.pngbin0 -> 777 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/rating_on.pngbin0 -> 845 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/remove.pngbin0 -> 633 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/search.pngbin0 -> 1198 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/settings.pngbin0 -> 1111 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/status.pngbin0 -> 1675 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/up.pngbin0 -> 610 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/pinkpanther/upload.pngbin0 -> 645 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/play.gifbin0 -> 70 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/podcast.pngbin0 -> 815 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/podcast_large.pngbin0 -> 1192 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/progress.pngbin0 -> 155 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/random.pngbin0 -> 932 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ratingHalf.pngbin0 -> 511 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ratingOff.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ratingOn.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/remove.gifbin0 -> 855 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/add.gifbin0 -> 13222 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/back.gifbin0 -> 13202 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/background.pngbin0 -> 707425 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/clearRating.pngbin0 -> 48184 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/current.gifbin0 -> 551 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/donate.pngbin0 -> 1095 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/down.gifbin0 -> 13265 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/download.gifbin0 -> 13258 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/error.pngbin0 -> 1102 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/favicon.icobin0 -> 3638 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/forward.gifbin0 -> 13261 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/help.pngbin0 -> 50596 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/help_small.pngbin0 -> 48608 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/home.pngbin0 -> 906 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/log.pngbin0 -> 992 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/more.pngbin0 -> 50720 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/now_playing.pngbin0 -> 51823 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/paypal.gifbin0 -> 2127 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/play.gifbin0 -> 13198 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/podcast.pngbin0 -> 1133 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/podcast_large.pngbin0 -> 1133 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/progress.pngbin0 -> 155 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/random.pngbin0 -> 48117 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/ratingHalf.pngbin0 -> 1092 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/ratingOff.pngbin0 -> 1029 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/ratingOn.pngbin0 -> 1230 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/remove.gifbin0 -> 13221 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/search.pngbin0 -> 1232 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/settings.pngbin0 -> 51387 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/status.pngbin0 -> 50439 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/subsonic_black.pngbin0 -> 62131 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/subsonic_white.pngbin0 -> 62273 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/up.gifbin0 -> 13265 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/upload.gifbin0 -> 13222 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/ripserver/wap.pngbin0 -> 48214 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/search.pngbin0 -> 1356 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/settings.pngbin0 -> 1327 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/share_facebook.pngbin0 -> 786 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/share_googleplus.pngbin0 -> 3340 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/share_twitter.pngbin0 -> 854 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/add.pngbin0 -> 807 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/back.pngbin0 -> 952 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/clear_rating.pngbin0 -> 2925 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/down.pngbin0 -> 738 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/download.pngbin0 -> 607 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/error.pngbin0 -> 3334 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/forward.pngbin0 -> 964 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/help.pngbin0 -> 1190 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/help_small.pngbin0 -> 791 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/home.pngbin0 -> 1022 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/log.pngbin0 -> 622 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/logo.pngbin0 -> 9096 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/more.pngbin0 -> 760 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/now_playing.pngbin0 -> 1180 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/phone.pngbin0 -> 722 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/play.pngbin0 -> 755 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/playing.pngbin0 -> 1168 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/podcast.pngbin0 -> 1283 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/podcast_small.pngbin0 -> 835 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/random.pngbin0 -> 3049 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/rating_half.pngbin0 -> 777 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/rating_on.pngbin0 -> 845 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/remove.pngbin0 -> 633 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/search.pngbin0 -> 1166 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/settings.pngbin0 -> 898 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/status.pngbin0 -> 1455 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/up.pngbin0 -> 749 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/simplify/upload.pngbin0 -> 605 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/favicon.icobin0 -> 61798 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/help.pngbin0 -> 3915 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/home.pngbin0 -> 1488 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/more.pngbin0 -> 3855 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/now_playing.pngbin0 -> 3667 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/podcast_large.pngbin0 -> 3693 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/settings.pngbin0 -> 3946 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/status.pngbin0 -> 3534 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/subsonic.pngbin0 -> 7194 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/slick/top_bg.jpgbin0 -> 10806 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/add.pngbin0 -> 6409 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/back.pngbin0 -> 986 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/clear_rating.pngbin0 -> 2927 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/down.pngbin0 -> 6474 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/download.pngbin0 -> 6488 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/error.pngbin0 -> 3334 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/forward.pngbin0 -> 998 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/help.pngbin0 -> 7241 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/help_small.pngbin0 -> 1076 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/home.pngbin0 -> 6836 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/log.pngbin0 -> 641 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/logo.pngbin0 -> 5145 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/more.pngbin0 -> 7009 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/now_playing.pngbin0 -> 914 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/phone.pngbin0 -> 769 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/play.pngbin0 -> 6458 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/playing.pngbin0 -> 7636 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/podcast.pngbin0 -> 7233 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/podcast_small.pngbin0 -> 837 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/random.pngbin0 -> 3050 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/rating_half.pngbin0 -> 777 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/rating_on.pngbin0 -> 845 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/remove.pngbin0 -> 6414 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/search.pngbin0 -> 7134 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/settings.pngbin0 -> 7413 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/status.pngbin0 -> 6935 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/up.pngbin0 -> 6459 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic/upload.pngbin0 -> 6487 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/add.pngbin0 -> 6645 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/back.pngbin0 -> 6767 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/clear_rating.pngbin0 -> 2937 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/down.pngbin0 -> 6684 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/download.pngbin0 -> 6688 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/error.pngbin0 -> 3334 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/forward.pngbin0 -> 6765 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/help.pngbin0 -> 7377 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/help_small.pngbin0 -> 589 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/home.pngbin0 -> 6983 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/log.pngbin0 -> 657 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/logo.pngbin0 -> 4904 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/more.pngbin0 -> 7178 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/now_playing.pngbin0 -> 761 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/phone.pngbin0 -> 769 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/play.pngbin0 -> 6658 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/playing.pngbin0 -> 8314 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/podcast.pngbin0 -> 7400 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/podcast_small.pngbin0 -> 876 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/random.pngbin0 -> 3099 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/rating_half.pngbin0 -> 777 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/rating_on.pngbin0 -> 845 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/remove.pngbin0 -> 6631 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/search.pngbin0 -> 7320 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/settings.pngbin0 -> 7483 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/status.pngbin0 -> 7089 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/up.pngbin0 -> 6681 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_blue/upload.pngbin0 -> 6668 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/add.pngbin0 -> 6395 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/back.pngbin0 -> 969 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/clear_rating.pngbin0 -> 2927 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/donate.pngbin0 -> 1899 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/down.pngbin0 -> 6467 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/download.pngbin0 -> 6413 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/error.pngbin0 -> 3334 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/favicon.icobin0 -> 1150 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/forward.pngbin0 -> 961 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/help.pngbin0 -> 7220 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/help_small.pngbin0 -> 1076 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/home.pngbin0 -> 6812 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/log.pngbin0 -> 641 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/logo.pngbin0 -> 5145 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/more.pngbin0 -> 6989 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/now_playing.pngbin0 -> 914 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/paypal.gifbin0 -> 956 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/phone.pngbin0 -> 769 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/play.pngbin0 -> 6418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/playing.pngbin0 -> 7612 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/podcast.pngbin0 -> 7196 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/podcast_small.pngbin0 -> 837 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/progress.pngbin0 -> 502 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/random.pngbin0 -> 3050 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/rating_half.pngbin0 -> 777 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/rating_off.pngbin0 -> 418 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/rating_on.pngbin0 -> 845 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/remove.pngbin0 -> 6392 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/search.pngbin0 -> 7103 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/settings.pngbin0 -> 7388 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/status.pngbin0 -> 6893 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/up.pngbin0 -> 6446 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/sonic_white/upload.pngbin0 -> 6444 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/spinner.gifbin0 -> 847 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/star_off.pngbin0 -> 594 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/starred.pngbin0 -> 699 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/status.pngbin0 -> 869 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/subair.pngbin0 -> 744 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/subsonic_black.pngbin0 -> 4797 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/subsonic_white.pngbin0 -> 4799 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/up.gifbin0 -> 856 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/upload.gifbin0 -> 311 bytes
-rw-r--r--subsonic-main/src/main/webapp/icons/wap.pngbin0 -> 488 bytes
-rw-r--r--subsonic-main/src/main/webapp/index.html10
-rw-r--r--subsonic-main/src/main/webapp/index.jsp10
-rw-r--r--subsonic-main/src/main/webapp/script/AC_OETags.js269
-rw-r--r--subsonic-main/src/main/webapp/script/builder.js136
-rw-r--r--subsonic-main/src/main/webapp/script/controls.js965
-rw-r--r--subsonic-main/src/main/webapp/script/effects.js1130
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/FancyZoom.js761
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/FancyZoomHTML.js318
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/closebox.pngbin0 -> 1910 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-fill.pngbin0 -> 134 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-l.pngbin0 -> 310 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-r.pngbin0 -> 290 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow1.pngbin0 -> 310 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow2.pngbin0 -> 164 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow3.pngbin0 -> 368 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow4.pngbin0 -> 178 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow5.pngbin0 -> 180 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow6.pngbin0 -> 428 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow7.pngbin0 -> 186 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow8.pngbin0 -> 426 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-1.pngbin0 -> 1882 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-10.pngbin0 -> 1892 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-11.pngbin0 -> 1901 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-12.pngbin0 -> 1902 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-2.pngbin0 -> 1893 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-3.pngbin0 -> 1922 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-4.pngbin0 -> 1890 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-5.pngbin0 -> 1938 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-6.pngbin0 -> 1927 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-7.pngbin0 -> 1898 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-8.pngbin0 -> 1910 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-9.pngbin0 -> 1901 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/jquery-1.7.1.min.js4
-rw-r--r--subsonic-main/src/main/webapp/script/jquery-ui-1.8.18.custom.min.js356
-rw-r--r--subsonic-main/src/main/webapp/script/pngfix.js39
-rw-r--r--subsonic-main/src/main/webapp/script/prototype.js4320
-rw-r--r--subsonic-main/src/main/webapp/script/scripts.js21
-rw-r--r--subsonic-main/src/main/webapp/script/smooth-scroll.js102
-rw-r--r--subsonic-main/src/main/webapp/script/swfobject.js4
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon.js221
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/b.gifbin0 -> 46 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/background.gifbin0 -> 43 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/l.gifbin0 -> 46 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/lb.gifbin0 -> 85 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/lt.gifbin0 -> 86 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/r.gifbin0 -> 46 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/rb.gifbin0 -> 86 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/rt.gifbin0 -> 85 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/stemb.gifbin0 -> 165 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/stemt.gifbin0 -> 167 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/tip_balloon/t.gifbin0 -> 46 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/boxsizing.htc157
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/handle.horizontal.hover.pngbin0 -> 440 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/handle.horizontal.pngbin0 -> 443 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/handle.vertical.hover.pngbin0 -> 395 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/handle.vertical.pngbin0 -> 398 bytes
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/luna.css75
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/range.js132
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/slider.js489
-rw-r--r--subsonic-main/src/main/webapp/script/webfx/timer.js62
-rw-r--r--subsonic-main/src/main/webapp/script/wz_tooltip.js1301
-rw-r--r--subsonic-main/src/main/webapp/style/barents.css30
-rw-r--r--subsonic-main/src/main/webapp/style/black.css17
-rw-r--r--subsonic-main/src/main/webapp/style/buuftheme.css120
-rw-r--r--subsonic-main/src/main/webapp/style/coolandclean.css116
-rw-r--r--subsonic-main/src/main/webapp/style/default.css245
-rw-r--r--subsonic-main/src/main/webapp/style/denim.css90
-rw-r--r--subsonic-main/src/main/webapp/style/groove.css102
-rw-r--r--subsonic-main/src/main/webapp/style/groove_simple.css32
-rw-r--r--subsonic-main/src/main/webapp/style/hd.css93
-rw-r--r--subsonic-main/src/main/webapp/style/hd1080.css17
-rw-r--r--subsonic-main/src/main/webapp/style/hd720.css16
-rw-r--r--subsonic-main/src/main/webapp/style/hd768.css16
-rw-r--r--subsonic-main/src/main/webapp/style/hicon.css85
-rw-r--r--subsonic-main/src/main/webapp/style/hiconi.css85
-rw-r--r--subsonic-main/src/main/webapp/style/hitech.css96
-rw-r--r--subsonic-main/src/main/webapp/style/lowerleftfade.pngbin0 -> 274 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/midnight.css77
-rw-r--r--subsonic-main/src/main/webapp/style/midnightfun.css158
-rw-r--r--subsonic-main/src/main/webapp/style/monochrome.css111
-rw-r--r--subsonic-main/src/main/webapp/style/monochrome_black.css115
-rw-r--r--subsonic-main/src/main/webapp/style/pinkpanther.css91
-rw-r--r--subsonic-main/src/main/webapp/style/ripserver.css44
-rw-r--r--subsonic-main/src/main/webapp/style/sandstorm.css39
-rw-r--r--subsonic-main/src/main/webapp/style/shadow.css107
-rw-r--r--subsonic-main/src/main/webapp/style/shadow.pngbin0 -> 3576 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/simplify.css90
-rw-r--r--subsonic-main/src/main/webapp/style/slick.css89
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.pngbin0 -> 180 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_75_ffffff_40x100.pngbin0 -> 178 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.pngbin0 -> 120 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_65_ffffff_1x400.pngbin0 -> 105 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_dadada_1x400.pngbin0 -> 111 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.pngbin0 -> 110 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_95_fef1ec_1x400.pngbin0 -> 119 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.pngbin0 -> 101 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_222222_256x240.pngbin0 -> 4369 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_2e83ff_256x240.pngbin0 -> 4369 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_454545_256x240.pngbin0 -> 4369 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_888888_256x240.pngbin0 -> 4369 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_cd0a0a_256x240.pngbin0 -> 4369 bytes
-rw-r--r--subsonic-main/src/main/webapp/style/smoothness/jquery-ui-1.8.18.custom.css565
-rw-r--r--subsonic-main/src/main/webapp/style/sonic.css88
-rw-r--r--subsonic-main/src/main/webapp/style/sonic_blue.css77
-rw-r--r--subsonic-main/src/main/webapp/style/sonic_white.css78
-rw-r--r--subsonic-main/src/main/webapp/style/upperrightfade.pngbin0 -> 275 bytes
-rw-r--r--subsonic-main/src/main/webapp/xsd/albumList2_example_1.xml16
-rw-r--r--subsonic-main/src/main/webapp/xsd/albumList_example_1.xml12
-rw-r--r--subsonic-main/src/main/webapp/xsd/album_example_1.xml17
-rw-r--r--subsonic-main/src/main/webapp/xsd/artist_example_1.xml24
-rw-r--r--subsonic-main/src/main/webapp/xsd/artists_example_1.xml14
-rw-r--r--subsonic-main/src/main/webapp/xsd/chatMessages_example_1.xml12
-rw-r--r--subsonic-main/src/main/webapp/xsd/directory_example_1.xml11
-rw-r--r--subsonic-main/src/main/webapp/xsd/directory_example_2.xml20
-rw-r--r--subsonic-main/src/main/webapp/xsd/error_example_1.xml8
-rw-r--r--subsonic-main/src/main/webapp/xsd/indexes_example_1.xml30
-rw-r--r--subsonic-main/src/main/webapp/xsd/jukeboxPlaylist_example_1.xml20
-rw-r--r--subsonic-main/src/main/webapp/xsd/jukeboxStatus_example_1.xml9
-rw-r--r--subsonic-main/src/main/webapp/xsd/license_example_1.xml8
-rw-r--r--subsonic-main/src/main/webapp/xsd/lyrics_example_1.xml39
-rw-r--r--subsonic-main/src/main/webapp/xsd/musicFolders_example_1.xml12
-rw-r--r--subsonic-main/src/main/webapp/xsd/nowPlaying_example_1.xml19
-rw-r--r--subsonic-main/src/main/webapp/xsd/ping_example_1.xml5
-rw-r--r--subsonic-main/src/main/webapp/xsd/playlist_example_1.xml37
-rw-r--r--subsonic-main/src/main/webapp/xsd/playlists_example_1.xml12
-rw-r--r--subsonic-main/src/main/webapp/xsd/podcasts_example_1.xml43
-rw-r--r--subsonic-main/src/main/webapp/xsd/randomSongs_example_1.xml20
-rw-r--r--subsonic-main/src/main/webapp/xsd/searchResult2_example_1.xml18
-rw-r--r--subsonic-main/src/main/webapp/xsd/searchResult3_example_1.xml54
-rw-r--r--subsonic-main/src/main/webapp/xsd/searchResult_example_1.xml20
-rw-r--r--subsonic-main/src/main/webapp/xsd/shares_example_1.xml23
-rw-r--r--subsonic-main/src/main/webapp/xsd/song_example_1.xml11
-rw-r--r--subsonic-main/src/main/webapp/xsd/starred2_example_1.xml30
-rw-r--r--subsonic-main/src/main/webapp/xsd/starred_example_1.xml30
-rw-r--r--subsonic-main/src/main/webapp/xsd/subsonic-rest-api.xsd434
-rw-r--r--subsonic-main/src/main/webapp/xsd/user_example_1.xml10
1024 files changed, 80461 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;
+ }
+ }
+ }
+}
diff --git a/subsonic-main/src/main/resources/ehcache.xml b/subsonic-main/src/main/resources/ehcache.xml
new file mode 100644
index 00000000..a79999ef
--- /dev/null
+++ b/subsonic-main/src/main/resources/ehcache.xml
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+CacheManager Configuration
+==========================
+An ehcache.xml corresponds to a single CacheManager.
+
+See instructions below or the ehcache schema (ehcache.xsd) on how to configure.
+
+System property tokens can be specified in this file which are replaced when the configuration
+is loaded. For example multicastGroupPort=${multicastGroupPort} can be replaced with the
+System property either from an environment variable or a system property specified with a
+command line switch such as -DmulticastGroupPort=4446. Another example, useful for Terracotta
+server based deployments is <terracottaConfig url="${serverAndPort}"/ and specify a command line
+switch of -Dserver36:9510
+
+The attributes of <ehcache> are:
+* name - an optional name for the CacheManager. The name is optional and primarily used
+for documentation or to distinguish Terracotta clustered cache state. With Terracotta
+clustered caches, a combination of CacheManager name and cache name uniquely identify a
+particular cache store in the Terracotta clustered memory.
+* updateCheck - an optional boolean flag specifying whether this CacheManager should check
+for new versions of Ehcache over the Internet. If not specified, updateCheck="true".
+* dynamicConfig - an optional setting that can be used to disable dynamic configuration of caches
+associated with this CacheManager. By default this is set to true - i.e. dynamic configuration
+is enabled. Dynamically configurable caches can have their TTI, TTL and maximum disk and
+in-memory capacity changed at runtime through the cache's configuration object.
+* monitoring - an optional setting that determines whether the CacheManager should
+automatically register the SampledCacheMBean with the system MBean server.
+
+Currently, this monitoring is only useful when using Terracotta clustering and using the
+Terracotta Developer Console. With the "autodetect" value, the presence of Terracotta clustering
+will be detected and monitoring, via the Developer Console, will be enabled. Other allowed values
+are "on" and "off". The default is "autodetect". This setting does not perform any function when
+used with JMX monitors.
+-->
+<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://ehcache.sourceforge.net/ehcache.xsd"
+ updateCheck="false"
+ monitoring="off"
+ dynamicConfig="true">
+
+ <!--
+ DiskStore configuration
+ =======================
+
+ The diskStore element is optional. To turn off disk store path creation, comment out the diskStore
+ element below.
+
+ Configure it if you have overflowToDisk or diskPersistent enabled for any cache.
+
+ If it is not configured, and a cache is created which requires a disk store, a warning will be
+ issued and java.io.tmpdir will automatically be used.
+
+ diskStore has only one attribute - "path". It is the path to the directory where
+ .data and .index files will be created.
+
+ If the path is one of the following Java System Property it is replaced by its value in the
+ running VM. For backward compatibility these should be specified without being enclosed in the ${token}
+ replacement syntax.
+
+ The following properties are translated:
+ * user.home - User's home directory
+ * user.dir - User's current working directory
+ * java.io.tmpdir - Default temp file path
+ * ehcache.disk.store.dir - A system property you would normally specify on the command line
+ e.g. java -Dehcache.disk.store.dir=/u01/myapp/diskdir ...
+
+ Subdirectories can be specified below the property e.g. java.io.tmpdir/one
+
+ -->
+ <!-- NOTE: The path is overridden in net.sourceforge.subsonic.cache.CacheFactory -->
+ <diskStore path="java.io.tmpdir"/>
+
+
+ <!-- Uncomment this to enable cache monitoring. -->
+ <!--<cacheManagerPeerListenerFactory-->
+ <!--class="org.terracotta.ehcachedx.monitor.probe.ProbePeerListenerFactory"-->
+ <!--properties="monitorAddress=localhost, monitorPort=9889, memoryMeasurement=true" />-->
+ <!---->
+
+ <!--
+ Cache configuration
+ ===================
+
+ The following attributes are required.
+
+ name:
+ Sets the name of the cache. This is used to identify the cache. It must be unique.
+
+ maxElementsInMemory:
+ Sets the maximum number of objects that will be created in memory. 0 = no limit.
+ In practice no limit means Integer.MAX_SIZE (2147483647) unless the cache is distributed
+ with a Terracotta server in which case it is limited by resources.
+
+ maxElementsOnDisk:
+ Sets the maximum number of objects that will be maintained in the DiskStore
+ The default value is zero, meaning unlimited.
+
+ eternal:
+ Sets whether elements are eternal. If eternal, timeouts are ignored and the
+ element is never expired.
+
+ overflowToDisk:
+ Sets whether elements can overflow to disk when the memory store
+ has reached the maxInMemory limit.
+
+ The following attributes and elements are optional.
+
+ overflowToOffHeap:
+ (boolean) This feature is available only in enterprise versions of Ehcache.
+ When set to true, enables the cache to utilize off-heap memory
+ storage to improve performance. Off-heap memory is not subject to Java
+ GC. The default value is false.
+
+ maxMemoryOffHeap:
+ (string) This feature is available only in enterprise versions of Ehcache.
+ Sets the amount of off-heap memory available to the cache.
+ This attribute's values are given as <number>k|K|m|M|g|G|t|T for
+ kilobytes (k|K), megabytes (m|M), gigabytes (g|G), or terabytes
+ (t|T). For example, maxMemoryOffHeap="2g" allots 2 gigabytes to
+ off-heap memory.
+
+ This setting is in effect only if overflowToOffHeap is true.
+
+ Note that it is recommended to set maxElementsInMemory to at least 100 elements
+ when using an off-heap store, otherwise performance will be seriously degraded,
+ and a warning will be logged.
+
+ The minimum amount that can be allocated is 128MB. There is no maximum.
+
+ timeToIdleSeconds:
+ Sets the time to idle for an element before it expires.
+ i.e. The maximum amount of time between accesses before an element expires
+ Is only used if the element is not eternal.
+ Optional attribute. A value of 0 means that an Element can idle for infinity.
+ The default value is 0.
+
+ timeToLiveSeconds:
+ Sets the time to live for an element before it expires.
+ i.e. The maximum time between creation time and when an element expires.
+ Is only used if the element is not eternal.
+ Optional attribute. A value of 0 means that and Element can live for infinity.
+ The default value is 0.
+
+ diskPersistent:
+ Whether the disk store persists between restarts of the Virtual Machine.
+ The default value is false.
+
+ diskExpiryThreadIntervalSeconds:
+ The number of seconds between runs of the disk expiry thread. The default value
+ is 120 seconds.
+
+ diskSpoolBufferSizeMB:
+ This is the size to allocate the DiskStore for a spool buffer. Writes are made
+ to this area and then asynchronously written to disk. The default size is 30MB.
+ Each spool buffer is used only by its cache. If you get OutOfMemory errors consider
+ lowering this value. To improve DiskStore performance consider increasing it. Trace level
+ logging in the DiskStore will show if put back ups are occurring.
+
+ clearOnFlush:
+ whether the MemoryStore should be cleared when flush() is called on the cache.
+ By default, this is true i.e. the MemoryStore is cleared.
+
+ statistics:
+ Whether to collect statistics. Note that this should be turned on if you are using
+ the Ehcache Monitor. By default statistics is turned off to favour raw performance.
+ To enable set statistics="true"
+
+ memoryStoreEvictionPolicy:
+ Policy would be enforced upon reaching the maxElementsInMemory limit. Default
+ policy is Least Recently Used (specified as LRU). Other policies available -
+ First In First Out (specified as FIFO) and Less Frequently Used
+ (specified as LFU)
+
+ copyOnRead:
+ Whether an Element is copied when being read from a cache.
+ By default this is false.
+
+ copyOnWrite:
+ Whether an Element is copied when being added to the cache.
+ By default this is false.
+ -->
+
+
+ <!--
+ Default Cache configuration. These settings will be applied to caches
+ created programmatically using CacheManager.add(String cacheName).
+ This element is optional, and using CacheManager.add(String cacheName) when
+ its not present will throw CacheException
+
+ The defaultCache has an implicit name "default" which is a reserved cache name.
+
+ <defaultCache
+ maxElementsInMemory="10000"
+ eternal="false"
+ timeToIdleSeconds="120"
+ timeToLiveSeconds="120"
+ overflowToDisk="true"
+ diskSpoolBufferSizeMB="10"
+ maxElementsOnDisk="10000000"
+ diskPersistent="false"
+ diskExpiryThreadIntervalSeconds="120"
+ memoryStoreEvictionPolicy="LRU"
+ statistics="true"
+ />
+ -->
+
+ <cache name="mediaFileMemoryCache"
+ maxElementsInMemory="1000"
+ eternal="false"
+ timeToIdleSeconds="0"
+ timeToLiveSeconds="10"
+ overflowToDisk="false"
+ statistics="true"
+ />
+
+ <cache name="musicFileMemoryCache"
+ maxElementsInMemory="1000"
+ eternal="false"
+ timeToIdleSeconds="0"
+ timeToLiveSeconds="10"
+ overflowToDisk="false"
+ statistics="true"
+ />
+
+ <cache name="userCache"
+ maxElementsInMemory="1000"
+ eternal="false"
+ timeToIdleSeconds="172800"
+ timeToLiveSeconds="172800"
+ overflowToDisk="false"
+ diskSpoolBufferSizeMB="1"
+ statistics="true"
+ />
+
+ <!--
+ Sample caches. Following are some example caches. Remove these before use.
+ -->
+
+ <!--
+ Sample cache named sampleCache1
+ This cache contains a maximum in memory of 10000 elements, and will expire
+ an element if it is idle for more than 5 minutes and lives for more than
+ 10 minutes.
+
+ If there are more than 10000 elements it will overflow to the
+ disk cache, which in this configuration will go to wherever java.io.tmp is
+ defined on your system. On a standard Linux system this will be /tmp"
+
+ <cache name="sampleCache1"
+ maxElementsInMemory="10000"
+ maxElementsOnDisk="1000"
+ eternal="false"
+ overflowToDisk="true"
+ diskSpoolBufferSizeMB="20"
+ timeToIdleSeconds="300"
+ timeToLiveSeconds="600"
+ memoryStoreEvictionPolicy="LFU"
+ transactionalMode="off"
+ />
+ -->
+
+
+ <!--
+ Sample cache named sampleCache2
+ This cache has a maximum of 1000 elements in memory. There is no overflow to disk, so 1000
+ is also the maximum cache size. Note that when a cache is eternal, timeToLive and
+ timeToIdle are not used and do not need to be specified.
+
+ <cache name="sampleCache2"
+ maxElementsInMemory="1000"
+ eternal="true"
+ overflowToDisk="false"
+ memoryStoreEvictionPolicy="FIFO"
+ />
+ -->
+
+
+ <!--
+ Sample cache named sampleCache3. This cache overflows to disk. The disk store is
+ persistent between cache and VM restarts. The disk expiry thread interval is set to 10
+ minutes, overriding the default of 2 minutes.
+
+ <cache name="sampleCache3"
+ maxElementsInMemory="500"
+ eternal="false"
+ overflowToDisk="true"
+ timeToIdleSeconds="300"
+ timeToLiveSeconds="600"
+ diskPersistent="true"
+ diskExpiryThreadIntervalSeconds="1"
+ memoryStoreEvictionPolicy="LFU"
+ />
+ -->
+
+</ehcache>
diff --git a/subsonic-main/src/main/resources/log4j.properties b/subsonic-main/src/main/resources/log4j.properties
new file mode 100644
index 00000000..13955699
--- /dev/null
+++ b/subsonic-main/src/main/resources/log4j.properties
@@ -0,0 +1,9 @@
+# Set root logger level to WARN and its only appender to A1.
+log4j.rootLogger=WARN, A1
+
+# A1 is set to be a ConsoleAppender.
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
+
+# A1 uses PatternLayout.
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+log4j.appender.A1.layout.ConversionPattern=[%d{ISO8601}] %-5p %c - %m%n
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpg b/subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpg
new file mode 100644
index 00000000..53f3fc64
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpg
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.png
new file mode 100644
index 00000000..5ba5254a
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.png
new file mode 100644
index 00000000..3d9f3f31
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.png
new file mode 100644
index 00000000..6663d48d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.png
new file mode 100644
index 00000000..b42954e9
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.png
new file mode 100644
index 00000000..08f58b83
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.png
new file mode 100644
index 00000000..ea3a0b62
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.png
new file mode 100644
index 00000000..505c259e
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.png
new file mode 100644
index 00000000..d964b959
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.png
new file mode 100644
index 00000000..05f5bb63
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.png
new file mode 100644
index 00000000..1a8a528c
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.png
new file mode 100644
index 00000000..7d25ea69
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.png
new file mode 100644
index 00000000..ccb1bf7f
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.png
new file mode 100644
index 00000000..400b6196
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.png
new file mode 100644
index 00000000..9ed06785
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.png
new file mode 100644
index 00000000..90cc14de
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.png
new file mode 100644
index 00000000..0c13e4f7
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.png
new file mode 100644
index 00000000..fd99ffdb
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.png
new file mode 100644
index 00000000..9d5512ac
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.png
new file mode 100644
index 00000000..3bca8892
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.png
new file mode 100644
index 00000000..0f85bf5c
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.png
new file mode 100644
index 00000000..d5555c06
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.png
new file mode 100644
index 00000000..78c498fe
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.png
new file mode 100644
index 00000000..0da7b6d3
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.png
new file mode 100644
index 00000000..dc4a5d71
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.png
new file mode 100644
index 00000000..3bba4bcd
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.png
new file mode 100644
index 00000000..211162d2
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.png
new file mode 100644
index 00000000..c47bf0af
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.png
new file mode 100644
index 00000000..4a67bead
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.png
new file mode 100644
index 00000000..1e520750
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.png
new file mode 100644
index 00000000..ec1716c5
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.png
new file mode 100644
index 00000000..0ad9d241
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.png
new file mode 100644
index 00000000..dd36bf0e
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.png
new file mode 100644
index 00000000..d3a58f0b
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.png
new file mode 100644
index 00000000..bb7d377b
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.png
new file mode 100644
index 00000000..e4d45068
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.png
new file mode 100644
index 00000000..b1373f34
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.png
new file mode 100644
index 00000000..a9266ceb
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.png
new file mode 100644
index 00000000..24cb3a04
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.png
new file mode 100644
index 00000000..9dd9d0f0
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.png
new file mode 100644
index 00000000..8b20b86b
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.png
new file mode 100644
index 00000000..d3381cf5
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.png
new file mode 100644
index 00000000..0b7a621d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.png
new file mode 100644
index 00000000..bcd25f1f
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.png
new file mode 100644
index 00000000..6073d700
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.png
new file mode 100644
index 00000000..9f448824
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.png
new file mode 100644
index 00000000..070e5a3e
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.png
Binary files differ
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties
new file mode 100644
index 00000000..888c6a82
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties
@@ -0,0 +1,702 @@
+#
+# Bulgarian localization.
+# Author: Ivan Achev
+#
+
+common.home = \u041d\u0430\u0447\u0430\u043b\u043e
+common.back = \u041e\u0431\u0440\u0430\u0442\u043d\u043e
+common.help = \u041f\u043e\u043c\u043e\u0449
+common.play = \u041f\u0443\u0441\u043d\u0438
+common.add = \u0414\u043e\u0431\u0430\u0432\u0438
+common.download = \u0421\u0432\u0430\u043b\u0438
+common.close = \u0417\u0430\u0442\u0432\u043e\u0440\u0438
+common.refresh = \u041e\u0431\u043d\u043e\u0432\u0438
+common.next = \u0421\u043b\u0435\u0434\u0432\u0430\u0449
+common.previous = \u041f\u0440\u0435\u0434\u0438\u0448\u0435\u043d
+common.more = \u041e\u0449\u0435
+common.ok = OK
+common.cancel = \u041e\u0442\u043c\u0435\u043d\u0438
+common.save = \u0417\u0430\u043f\u0430\u0437\u0438
+common.create = \u0421\u044a\u0437\u0434\u0430\u0439
+common.delete = \u0418\u0437\u0442\u0440\u0438\u0439
+common.unknown = (\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d)
+common.default = (\u041f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435)
+
+# login.jsp
+login.username = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+login.password = \u041f\u0430\u0440\u043e\u043b\u0430
+login.login = \u0412\u0445\u043e\u0434
+login.remember = \u0417\u0430\u043f\u043e\u043c\u043d\u0438 \u043c\u0435
+login.logout = \u0418\u0437\u043b\u044f\u0437\u043e\u0445\u0442\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0442 \u0430\u043a\u0430\u0443\u043d\u0442\u0430.
+login.error = \u0413\u0440\u0435\u0448\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430.
+login.insecure = {0} \u043d\u0435 \u0435 \u0437\u0430\u0449\u0438\u0442\u0435\u043d. \u041c\u043e\u043b\u044f \u0432\u043b\u0435\u0437\u0442\u0435 \u0441 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b "admin" <br> \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 "admin", \u0438\u043b\u0438 \u0449\u0440\u0430\u043a\u043d\u0435\u0442\u0435 <a href="login.view?user=admin&amp;password=admin">\u0442\u0443\u043a</a>. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0441\u043c\u0435\u043d\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0435\u0437\u0430\u0431\u0430\u0432\u043d\u043e.
+
+# accessDenied.jsp
+accessDenied.title = \u0414\u043e\u0441\u0442\u044a\u043f \u043e\u0442\u043a\u0430\u0437\u0430\u043d
+accessDenied.text = \u0421\u044a\u0436\u0430\u043b\u044f\u0432\u0430\u043c\u0435, \u043d\u044f\u043c\u0430\u0442\u0435 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0438\u0437\u0432\u044a\u0440\u0448\u0438\u0442\u0435 \u0442\u043e\u0432\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435.
+
+# top.jsp
+top.home = \u041d\u0430\u0447\u0430\u043b\u043e
+top.now_playing = \u041f\u043b\u0435\u044a\u0440
+top.settings = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
+top.status = \u0421\u0442\u0430\u0442\u0443\u0441
+top.podcast = \u041f\u043e\u0434\u043a\u0430\u0441\u0442
+top.more = \u041e\u0449\u0435
+top.help = \u041e\u0442\u043d\u043e\u0441\u043d\u043e
+top.search = \u0422\u044a\u0440\u0441\u0435\u043d\u0435
+top.upgrade = <b>\u0412\u043d\u0438\u043c\u0430\u043d\u0438\u0435!</b> \u041d\u0430\u043b\u0438\u0447\u043d\u0430 \u0435 \u043d\u043e\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f.<br>\u0421\u0432\u0430\u043b\u0438 {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u0442\u0443\u043a</a>.
+top.missing = \u041d\u044f\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0438 \u043f\u0430\u043f\u043a\u0438 \u0441 \u043c\u0443\u0437\u0438\u043a\u0430. \u041c\u043e\u043b\u044f \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435.
+top.logout = \u0418\u0437\u043b\u0435\u0437 {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;\u0438\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u0438<br>\
+ {1}&nbsp;\u0430\u043b\u0431\u0443\u043c\u0438<br>\
+ {2}&nbsp;\u043f\u0435\u0441\u043d\u0438<br>\
+ {3} (&#126; {4} \u0447\u0430\u0441\u0430)
+left.shortcut = \u0412\u0440\u044a\u0437\u043a\u0438
+left.radio = Internet TV/\u0420\u0430\u0434\u0438\u043e
+left.allfolders = \u0412\u0441\u0438\u0447\u043a\u0438 \u043f\u0430\u043f\u043a\u0438
+
+# playlist.jsp
+playlist.stop = \u0421\u043f\u0440\u0438
+playlist.start = \u041f\u0443\u0441\u043d\u0438
+playlist.confirmclear = \u0418\u0437\u0442\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430\u0442\u0430?
+playlist.clear = \u0418\u0437\u0447\u0438\u0441\u0442\u0438
+playlist.shuffle = \u0420\u0430\u0437\u0431\u044a\u0440\u043a\u0430\u0439
+playlist.repeat_on = \u041f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0435
+playlist.repeat_off = \u0411\u0435\u0437 \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0435
+playlist.undo = \u041e\u0442\u043c\u0435\u043d\u0438
+playlist.settings = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
+playlist.more = \u041e\u0449\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f
+playlist.more.playlist = \u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+playlist.more.sortbytrack = \u0421\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043f\u043e \u043f\u0435\u0441\u0435\u043d
+playlist.more.sortbyartist = \u0421\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043f\u043e \u0438\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b
+playlist.more.sortbyalbum = \u0421\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043f\u043e \u0430\u043b\u0431\u0443\u043c
+playlist.more.selection = \u041c\u0430\u0440\u043a\u0438\u0440\u0430\u043d\u0438 \u043f\u0435\u0441\u043d\u0438
+playlist.more.selectall = \u041c\u0430\u0440\u043a\u0438\u0440\u0430\u0439 \u0432\u0441\u0438\u0447\u043a\u0438
+playlist.more.selectnone = \u041e\u0442\u043c\u0430\u0440\u043a\u0438\u0440\u0430\u0439
+playlist.getflash = \u0421\u0432\u0430\u043b\u0435\u0442\u0435 \u0441\u0438 Flash \u043f\u043b\u0435\u044a\u0440
+playlist.load = \u0417\u0430\u0440\u0435\u0434\u0438
+playlist.save = \u0417\u0430\u043f\u0430\u0437\u0438
+playlist.append = \u0414\u043e\u0431\u0430\u0432\u0438 \u0432 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+playlist.remove = \u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0438
+playlist.up = \u041d\u0430\u0433\u043e\u0440\u0435
+playlist.down = \u041d\u0430\u0434\u043e\u043b\u0443
+playlist.empty = \u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430\u0442\u0430 \u0435 \u043f\u0440\u0430\u0437\u043d\u0430
+
+# videoPlayer.jsp
+videoPlayer.getflash = \u041c\u043e\u043b\u044f \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u0439\u0442\u0435 \u0441\u0438 Flash \u043f\u043b\u0435\u044a\u0440
+videoPlayer.popout = \u041e\u0442\u0432\u043e\u0440\u0438 \u0432 \u043d\u043e\u0432 \u043f\u0440\u043e\u0437\u043e\u0440\u0435\u0446
+
+# status.jsp
+status.title = \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430
+status.type = \u0422\u0438\u043f
+status.stream = \u041f\u043e\u0442\u043e\u043a
+status.download = \u0421\u0432\u0430\u043b\u0435\u043d\u0438
+status.upload = \u041a\u0430\u0447\u0435\u043d\u0438
+status.player = \u041f\u043b\u0435\u044a\u0440
+status.user = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+status.current = \u0422\u0435\u043a\u0443\u0449 \u0444\u0430\u0439\u043b
+status.transmitted = \u0418\u0437\u043f\u0440\u0430\u0442\u0435\u043d\u0438
+status.bitrate = \u0411\u0438\u0442\u0440\u0435\u0439\u0442 (Kbps)
+
+# search.jsp
+search.title = \u0422\u044a\u0440\u0441\u0435\u043d\u0435
+search.query = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b, \u0430\u043b\u0431\u0443\u043c \u0438\u043b\u0438 \u0438\u043c\u0435 \u043d\u0430 \u043f\u0435\u0441\u0435\u043d
+search.search = \u0422\u044a\u0440\u0441\u0438
+search.index = \u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0441\u0435 \u0438\u0437\u0432\u044a\u0440\u0448\u0432\u0430 \u0438\u043d\u0434\u0435\u043a\u0441\u0438\u0440\u0430\u043d\u0435 \u043e\u0442 \u0442\u044a\u0440\u0441\u0430\u0447\u043a\u0430\u0442\u0430. \u041c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.
+search.hits.none = \u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0441\u044a\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u044f.
+search.hits.more = \u041e\u0449\u0435
+search.hits.artists = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u0438
+search.hits.albums = \u0410\u043b\u0431\u0443\u043c\u0438
+search.hits.songs = \u041f\u0435\u0441\u043d\u0438
+
+# gettingStarted.jsp
+gettingStarted.title = \u0414\u0430 \u0437\u0430\u043f\u043e\u0447\u0432\u0430\u043c\u0435
+gettingStarted.text = <p>\u0414\u043e\u0431\u0440\u0435 \u0434\u043e\u0448\u043b\u0438 \u0432 Subsonic! \u0422\u0440\u044f\u0431\u0432\u0430\u0442 \u0441\u0430\u043c\u043e \u043e\u0449\u0435 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0431\u044a\u0440\u0437\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438, \u043f\u0440\u043e\u0441\u0442\u043e \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0441\u0442\u044a\u043f\u043a\u0438\u0442\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0438 \u043f\u043e-\u0434\u043e\u043b\u0443.<br> \
+ \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 "\u041d\u0430\u0447\u0430\u043b\u043e" \u0432 \u043b\u0435\u043d\u0442\u0430\u0442\u0430 \u0433\u043e\u0440\u0435, \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435\u0442\u0435 \u043f\u043e \u0432\u0441\u044f\u043a\u043e \u0432\u0440\u0435\u043c\u0435 \u043a\u044a\u043c \u0442\u043e\u0437\u0438 \u0435\u043a\u0440\u0430\u043d.</p> \
+ <p>\u0417\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u043c\u043e\u043b\u044f \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\u0420\u044a\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u043e\u0442\u043e</b></a> \u0437\u0430 \u043d\u043e\u0432\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438.</p>
+gettingStarted.step1.title = \u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0441\u043a\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430.
+gettingStarted.step1.text = \u0417\u0430\u0449\u0438\u0442\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 \u0441\u044a\u0440\u0432\u044a\u0440 \u043a\u0430\u0442\u043e \u0441\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 \u0441 \u0432\u0430\u0448\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0441\u043a\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b. \
+ \u041c\u043e\u0436\u0435\u0442\u0435 \u0441\u044a\u0449\u043e \u0442\u0430\u043a\u0430 \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u0442\u0435 \u043d\u043e\u0432\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b\u0438 \u0441 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u0438 \u043f\u0440\u0430\u0432\u0430.
+gettingStarted.step2.title = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u043f\u0430\u043f\u043a\u0438\u0442\u0435 \u0441 \u043c\u0443\u0437\u0438\u043a\u0430.
+gettingStarted.step2.text = \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043a\u044a\u0434\u0435 \u0441\u0435 \u043d\u0430\u043c\u0438\u0440\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u043c\u0443\u0437\u0438\u043a\u0430.
+gettingStarted.step3.title = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.
+gettingStarted.step3.text = \u041d\u044f\u043a\u043e\u0438 \u043f\u043e\u043b\u0435\u0437\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u043a\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0441\u043b\u0443\u0448\u0430\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u043c\u0443\u0437\u0438\u043a\u0430 \u0432 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442, \u043a\u044a\u0434\u0435\u0442\u043e \u0438 \u0434\u0430 \u0441\u0435 \u043d\u0430\u043c\u0438\u0440\u0430\u0442\u0435 \
+ \u0438\u043b\u0438 \u0434\u0430 \u044f \u0441\u043f\u043e\u0434\u0435\u043b\u044f\u0442\u0435 \u0441 \u043f\u0440\u0438\u044f\u0442\u0435\u043b\u0438 \u0438 \u0441\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u043e\u0442\u043e. \u0412\u0437\u0435\u043c\u0435\u0442\u0435 \u0432\u0430\u0448 \u043b\u0438\u0447\u0435\u043d <b><em>\u0432\u0430\u0448\u0435\u0442\u043e\u0438\u043c\u0435</em>.subsonic.org</b> \
+ \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0430\u0434\u0440\u0435\u0441.
+gettingStarted.hide = \u041d\u0435 \u043f\u043e\u043a\u0430\u0437\u0432\u0430\u0439 \u0442\u043e\u0432\u0430 \u043f\u043e\u0432\u0435\u0447\u0435
+gettingStarted.hidealert = \u0417\u0430 \u0434\u0430 \u043f\u043e\u043a\u0430\u0436\u0435\u0442\u0435 \u0442\u0430\u0437\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043e\u0442\u043d\u043e\u0432\u043e, \u043e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u041e\u0431\u0449\u0438.
+
+# home.jsp
+home.random.title = \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u0438
+home.newest.title = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0435\u043d\u0438
+home.highest.title = \u041f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433
+home.frequent.title = \u041d\u0430\u0439-\u0441\u043b\u0443\u0448\u0430\u043d\u0438
+home.recent.title = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u0441\u043b\u0443\u0448\u0430\u043d\u0438
+home.users.title = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438
+home.random.text = \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.newest.text = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0435\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.highest.text = \u041d\u0430\u0439-\u0432\u0438\u0441\u043e\u043a\u043e \u043e\u0446\u0435\u043d\u0435\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.frequent.text = \u041d\u0430\u0439-\u0447\u0435\u0441\u0442\u043e \u0441\u043b\u0443\u0448\u0430\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.recent.text = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u0441\u043b\u0443\u0448\u0430\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.users.text = \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430 \u0437\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438\u0442\u0435
+home.scan = \u0422\u0430\u0437\u0438 \u043f\u0430\u043f\u043a\u0430 \u0441\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u0430 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442\u0430. \u041d\u044f\u043a\u043e\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043d\u0435 \u0441\u0430 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0438 \u043e\u0449\u0435.
+home.listsize = {0} \u0430\u043b\u0431\u0443\u043c\u0430 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430
+home.albums = \u0410\u043b\u0431\u0443\u043c\u0438 {0} - {1}
+home.playcount = \u041f\u0440\u043e\u0441\u043b\u0443\u0448\u0430\u043d\u0438 {0} \u043f\u0435\u0441\u043d\u0438
+home.lastplayed = \u0421\u043b\u0443\u0448\u0430\u043d\u0430 {0}
+home.created = \u0421\u044a\u0437\u0434\u0430\u0434\u0435\u043d {0}
+home.chart.total = \u041e\u0431\u0449\u043e (MB)
+home.chart.stream = \u041f\u043e\u0442\u043e\u043a (MB)
+home.chart.download = \u0421\u0432\u0430\u043b\u0435\u043d\u0438 (MB)
+home.chart.upload = \u041a\u0430\u0447\u0435\u043d\u0438 (MB)
+
+# more.jsp
+more.title = \u041e\u0449\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438
+more.random.title = \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+more.random.text = \u0421\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430 \u0441
+more.random.songs = {0} \u043f\u0435\u0441\u043d\u0438
+more.random.auto = \u041e\u0449\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u0438 \u043f\u0435\u0441\u043d\u0438, \u043a\u043e\u0433\u0430\u0442\u043e \u0441\u0432\u044a\u0440\u0448\u0430\u0442 \u043f\u0435\u0441\u043d\u0438\u0442\u0435 \u0432 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430\u0442\u0430.
+more.random.ok = OK
+more.random.genre = \u043e\u0442 \u0436\u0430\u043d\u0440
+more.random.anygenre = \u0411\u0435\u0437 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435
+more.random.year = \u0438 \u0433\u043e\u0434\u0438\u043d\u0430
+more.random.anyyear = \u0411\u0435\u0437 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435
+more.random.folder = \u043e\u0442 \u043f\u0430\u043f\u043a\u0430
+more.random.anyfolder = \u0411\u0435\u0437 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435
+more.apps.title = Subsonic \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u0442\u0430</a> \u0441\u0430 \u043f\u0440\u0435\u0434\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438 \u0437\u0430 <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone</b> \u0438 <b>AIR</b>.</p>
+more.mobile.title = \u041c\u043e\u0431\u0438\u043b\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430
+more.mobile.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0432\u0430\u0442\u0435 {0} \u043e\u0442 \u0432\u0441\u0435\u043a\u0438 WAP-\u0441\u044a\u0432\u043c\u0435\u0441\u0442\u0438\u043c \u043c\u043e\u0431\u0438\u043b\u0435\u043d \u0442\u0435\u043b\u0435\u0444\u043e\u043d \u0438\u043b\u0438 PDA.<br> \
+ \u041f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u0441\u0435\u0442\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u044f URL \u0430\u0434\u0440\u0435\u0441 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0444\u043e\u043d: <b>http://yourhostname/wap</b></p> \
+ <p>\u0422\u043e\u0432\u0430 \u0438\u0437\u0438\u0441\u043a\u0432\u0430 \u043d\u0430\u043b\u0438\u0447\u0435\u043d \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0432\u0430\u0448\u0438\u044f \u0441\u044a\u0440\u0432\u044a\u0440 \u043f\u043e \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442.</p>
+more.podcast.title = \u041f\u043e\u0434\u043a\u0430\u0441\u0442\u0438\u043d\u0433
+more.podcast.text = <p>\u0417\u0430\u043f\u0430\u0437\u0435\u043d\u0438\u0442\u0435 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0438 \u0441\u0430 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0438 \u0438 \u043a\u0430\u0442\u043e \u043f\u043e\u0434\u043a\u0430\u0441\u0442.<br>\
+ \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441 \u0432\u044a\u0432 \u0432\u0430\u0448\u0435\u0442\u043e \u043f\u043e\u0434\u043a\u0430\u0441\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: <b>http://yourhostname/podcast</b>, \
+ \u0438\u043b\u0438 <b><a href="podcast.view?suffix=.rss">\u0449\u0440\u0430\u043a\u043d\u0435\u0442\u0435 \u0442\u0443\u043a</a>.</b></p>
+more.upload.title = \u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b
+more.upload.source = \u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u0444\u0430\u0439\u043b
+more.upload.target = \u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u0432
+more.upload.browse = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435
+more.upload.ok = \u041a\u0430\u0447\u0438
+more.upload.unzip = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0440\u0430\u0437\u0430\u0440\u0445\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 zip-\u0444\u0430\u0439\u043b\u043e\u0432\u0435.
+more.upload.progress = % \u0437\u0430\u0432\u044a\u0440\u0448\u0435\u043d\u0438. \u041c\u043e\u043b\u044f \u0438\u0437\u0447\u0430\u043a\u0430\u0439\u0442\u0435...
+
+# upload.jsp
+upload.title = \u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b
+upload.success = \u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043a\u0430\u0447\u0435\u043d <b>{0}</b>
+upload.empty = \u041d\u044f\u043c\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0437\u0430 \u043a\u0430\u0447\u0432\u0430\u043d\u0435.
+upload.failed = \u041a\u0430\u0447\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u0440\u0430\u0434\u0438 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0433\u0440\u0435\u0448\u043a\u0430:<br><b>"{0}"</b>
+upload.unzipped = \u0420\u0430\u0437\u0430\u0440\u0445\u0438\u0432\u0438\u0440\u0430\u043d {0}
+
+# help.jsp
+help.title = \u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 {0}
+help.upgrade = <b>\u0412\u043d\u0438\u043c\u0430\u043d\u0438\u0435!</b> \u041d\u0430\u043b\u0438\u0447\u043d\u0430 \u0435 \u043d\u043e\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044f. \u0421\u0432\u0430\u043b\u0438 \u043e\u0442 {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u0442\u0443\u043a</a>.
+help.version.title = \u0412\u0435\u0440\u0441\u0438\u044f
+help.builddate.title = \u041e\u0442 \u0434\u0430\u0442\u0430
+help.server.title = \u0421\u044a\u0440\u0432\u044a\u0440
+help.license.title = \u0423\u0441\u043b\u043e\u0432\u0438\u044f&nbsp;\u0437\u0430&nbsp;\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435
+help.license.text = {0} \u0435 \u0431\u0435\u0437\u043f\u043b\u0430\u0442\u0435\u043d \u0441\u043e\u0444\u0442\u0443\u0435\u0440, \u0440\u0430\u0437\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u044f\u0432\u0430\u043d \u0447\u0440\u0435\u0437 <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> \u043b\u0438\u0446\u0435\u043d\u0437 \u0441 \u043e\u0442\u0432\u043e\u0440\u0435\u043d \u043a\u043e\u0434. \
+ {0} \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">\u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u0430\u043d\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u0430 \u0442\u0440\u0435\u0442\u0438 \u0441\u0442\u0440\u0430\u043d\u0438</a>. \u041c\u043e\u043b\u044f \u043e\u0431\u044a\u0440\u043d\u0435\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0435 {0} <em>\u043d\u0435</em> \u0435 \
+ \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442 \u0437\u0430 \u043d\u0435\u043b\u0435\u0433\u0430\u043b\u043d\u043e \u0440\u0430\u0437\u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u043d\u0430 \u0437\u0430\u0449\u0438\u0442\u0435\u043d\u0438 \u0441 \u0430\u0432\u0442\u043e\u0440\u0441\u043a\u043e \u043f\u0440\u0430\u0432\u043e \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u0438. \u0412\u0438\u043d\u0430\u0433\u0438 \u0441\u043f\u0430\u0437\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u043a\u043e\u043d\u0438\u0442\u0435, \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u043d\u0438 \u0437\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430.
+help.homepage.title = \u041e\u0444\u0438\u0446\u0438\u0430\u043b\u0435\u043d \u0441\u0430\u0439\u0442
+help.forum.title = \u0424\u043e\u0440\u0443\u043c
+help.shop.title = \u041c\u0430\u0433\u0430\u0437\u0438\u043d
+help.contact.title = \u041a\u043e\u043d\u0442\u0430\u043a\u0442
+help.contact.text = {0} \u0435 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d \u0438 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u043e\u0442 Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u0432\u044a\u043f\u0440\u043e\u0441\u0438, \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0437\u0430 \u043f\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f, \u043c\u043e\u043b\u044f \u043f\u043e\u0441\u0435\u0442\u0435\u0442\u0435 \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic \u0424\u043e\u0440\u0443\u043c</a>.
+help.donate = {0} \u0435 \u0431\u0435\u0437\u043f\u043b\u0430\u0442\u0435\u043d, \u043d\u043e \u0432\u0438\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u043c\u043e\u0433\u043d\u0435\u0442\u0435 \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u043a\u0430\u0442\u043e \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u0435 <b><a href="donate.view?">\u0434\u0430\u0440\u0435\u043d\u0438\u0435</a></b>.
+help.log = \u041b\u043e\u0433 \u0444\u0430\u0439\u043b\u043e\u0432\u0435
+help.logfile = \u0426\u0435\u043b\u0438\u044f\u0442 \u043b\u043e\u0433 \u0435 \u0437\u0430\u043f\u0430\u0437\u0435\u043d \u0432 {0}.
+
+# settingsHeader.jsp
+settingsheader.title = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
+settingsheader.general = \u041e\u0431\u0449\u0438
+settingsheader.advanced = \u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438
+settingsheader.personal = \u0412\u0438\u0434
+settingsheader.musicFolder = \u041f\u0430\u043f\u043a\u0438 \u0441 \u043c\u0443\u0437\u0438\u043a\u0430
+settingsheader.internetRadio = Internet TV/\u0420\u0430\u0434\u0438\u043e
+settingsheader.podcast = \u041f\u043e\u0434\u043a\u0430\u0441\u0442
+settingsheader.player = \u041f\u043b\u0435\u044a\u0440\u0438
+settingsheader.network = \u041c\u0440\u0435\u0436\u0430
+settingsheader.transcoding = \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435
+settingsheader.user = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438
+settingsheader.search = \u0422\u044a\u0440\u0441\u0430\u0447\u043a\u0430
+settingsheader.coverArt = \u041e\u0431\u043b\u043e\u0436\u043a\u0438
+settingsheader.password = \u041f\u0430\u0440\u043e\u043b\u0430
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \u041f\u0430\u043f\u043a\u0430 \u0437\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0438
+generalsettings.musicmask = \u0424\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u043c\u0443\u0437\u0438\u043a\u0430
+generalsettings.videomask = \u0424\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u0432\u0438\u0434\u0435\u043e
+generalsettings.coverartmask = \u0424\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0438
+generalsettings.index = \u0418\u043d\u0434\u0435\u043a\u0441
+generalsettings.ignoredarticles = \u0421\u0438\u043c\u0432\u043e\u043b\u0438 \u0437\u0430 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u043d\u0435
+generalsettings.shortcuts = \u0412\u0440\u044a\u0437\u043a\u0438
+generalsettings.showgettingstarted = \u041f\u043e\u043a\u0430\u0437\u0432\u0430\u0439 "\u0414\u0430 \u0437\u0430\u043f\u043e\u0447\u0432\u0430\u043c\u0435" \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435
+generalsettings.welcometitle = \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043e \u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435
+generalsettings.welcomesubtitle = \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043e \u043f\u043e\u0434\u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435
+generalsettings.welcomemessage = \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043e \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435
+generalsettings.loginmessage = \u0421\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u0432\u0445\u043e\u0434
+generalsettings.language = \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0435\u043d \u0435\u0437\u0438\u043a
+generalsettings.theme = \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430 \u0442\u0435\u043c\u0430
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = \u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u0437\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435
+advancedsettings.coverartlimit = \u041b\u0438\u043c\u0438\u0442 \u0437\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0438<br><div class="detail">(0 = \u0411\u0435\u0437 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435)</div>
+advancedsettings.downloadlimit = \u041b\u0438\u043c\u0438\u0442 \u0437\u0430 \u0441\u0432\u0430\u043b\u044f\u043d\u0435 (Kbps)<br><div class="detail">(0 = \u0411\u0435\u0437 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435)</div>
+advancedsettings.uploadlimit = \u041b\u0438\u043c\u0438\u0442 \u0437\u0430 \u043a\u0430\u0447\u0432\u0430\u043d\u0435 (Kbps)<br><div class="detail">(0 = \u0411\u0435\u0437 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435)</div>
+advancedsettings.streamport = \u041f\u043e\u0440\u0442 \u0437\u0430 \u043d\u0435\u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d (SSL) \u043f\u043e\u0442\u043e\u043a<br><div class="detail">(0 = \u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d)</div>
+advancedsettings.ldapenabled = \u0412\u043a\u043b\u044e\u0447\u0438 LDAP \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP \u0444\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u0442\u044a\u0440\u0441\u0435\u043d\u0435
+advancedsettings.ldapmanagerdn = LDAP \u043c\u0435\u043d\u0438\u0434\u0436\u044a\u0440 DN<br><div class="detail">(\u041d\u0435\u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e)</div>
+advancedsettings.ldapmanagerpassword = \u041f\u0430\u0440\u043e\u043b\u0430
+advancedsettings.ldapautoshadowing = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438 \u0432 {0}
+
+# personalSettings.jsp
+personalsettings.title = \u0412\u044a\u043d\u0448\u0435\u043d \u0432\u0438\u0434 \u0437\u0430 {0}
+personalsettings.language = \u0415\u0437\u0438\u043a
+personalsettings.theme = \u0422\u0435\u043c\u0430
+personalsettings.display = \u041f\u043e\u043a\u0430\u0437\u0432\u0430\u0439
+personalsettings.browse = \u041e\u0441\u043d\u043e\u0432\u0435\u043d
+personalsettings.playlist = \u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+personalsettings.tracknumber = \u041f\u0435\u0441\u0435\u043d #
+personalsettings.artist = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b
+personalsettings.album = \u0410\u043b\u0431\u0443\u043c
+personalsettings.genre = \u0416\u0430\u043d\u0440
+personalsettings.year = \u0413\u043e\u0434\u0438\u043d\u0430
+personalsettings.bitrate = \u0411\u0438\u0442\u0440\u0435\u0439\u0442
+personalsettings.duration = \u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442
+personalsettings.format = \u0424\u043e\u0440\u043c\u0430\u0442
+personalsettings.filesize = \u0420\u0430\u0437\u043c\u0435\u0440
+personalsettings.captioncutoff = \u0417\u0430\u0433\u043b\u0430\u0432\u0438\u044f (\u0431\u0440\u043e\u0439 \u0431\u0443\u043a\u0432\u0438))
+personalsettings.partymode = \u041e\u043b\u0435\u043a\u043e\u0442\u0435\u043d \u0440\u0435\u0436\u0438\u043c
+personalsettings.shownowplaying = \u041f\u043e\u043a\u0430\u0437\u0432\u0430\u0439 \u043a\u0430\u043a\u0432\u043e \u0441\u043b\u0443\u0448\u0430\u0442 \u0434\u0440\u0443\u0433\u0438\u0442\u0435
+personalsettings.nowplayingallowed = \u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u043d\u0430 \u043e\u0441\u0442\u0430\u043d\u0430\u043b\u0438\u0442\u0435 \u0434\u0430 \u0432\u0438\u0436\u0434\u0430\u0442 \u043a\u0430\u043a\u0432\u043e \u0441\u043b\u0443\u0448\u0430\u043c \u0430\u0437
+personalsettings.showchat = \u041f\u043e\u043a\u0430\u0437\u0432\u0430\u0439 \u0447\u0430\u0442 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f
+personalsettings.finalversionnotification = \u0423\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u0439 \u043c\u0435 \u0437\u0430 \u043d\u043e\u0432\u0438 \u0432\u0435\u0440\u0441\u0438\u0438
+personalsettings.betaversionnotification = \u0423\u0432\u0435\u0434\u043e\u043c\u044f\u0432\u0430\u0439 \u043c\u0435 \u0437\u0430 \u043d\u043e\u0432\u0438 \u0431\u0435\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u0438
+personalsettings.lastfmenabled = \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0439 \u0442\u043e\u0432\u0430, \u043a\u043e\u0435\u0442\u043e \u0441\u043b\u0443\u0448\u0430\u043c \u0432 <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+personalsettings.lastfmpassword = Last.fm \u043f\u0430\u0440\u043e\u043b\u0430
+personalsettings.avatar.title = \u041b\u0438\u0447\u043d\u0430 \u0441\u043d\u0438\u043c\u043a\u0430
+personalsettings.avatar.none = \u0411\u0435\u0437 \u0441\u043d\u0438\u043c\u043a\u0430
+personalsettings.avatar.custom = \u041c\u043e\u044f \u0441\u043d\u0438\u043c\u043a\u0430
+personalsettings.avatar.changecustom = \u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u0430 \u0441\u043d\u0438\u043c\u043a\u0430
+personalsettings.avatar.upload = \u041a\u0430\u0447\u0438
+personalsettings.avatar.courtesy = \u0418\u043a\u043e\u043d\u043a\u0438\u0442\u0435 \u0441\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438 \u043e\u0442 <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = \u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u043b\u0438\u0447\u043d\u0430\u0442\u0430 \u0441\u043d\u0438\u043c\u043a\u0430
+avataruploadresult.success = \u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043a\u0430\u0447\u0435\u043d\u0430 \u0441\u043d\u0438\u043c\u043a\u0430 "{0}".
+avataruploadresult.failure = \u041a\u0430\u0447\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e. \u0412\u0438\u0436\u0442\u0435 <a href="help.view?">\u0434\u043e\u043a\u043b\u0430\u0434\u0430</a> \u0437\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438.
+
+# passwordSettings.jsp
+passwordsettings.title = \u0421\u043c\u044f\u043d\u0430 \u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \u041f\u0430\u043f\u043a\u0430
+musicfoldersettings.name = \u0418\u043c\u0435
+musicfoldersettings.enabled = \u0410\u043a\u0442\u0438\u0432\u043d\u0430
+musicfoldersettings.add = \u0414\u043e\u0431\u0430\u0432\u0438 \u043f\u0430\u043f\u043a\u0430
+musicfoldersettings.nopath = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u043f\u0430\u043f\u043a\u0430.
+
+# networkSettings.jsp
+networksettings.text = \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u043f\u043e-\u0434\u043e\u043b\u0443 \u0437\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u043d\u0430\u0447\u0438\u043d\u0430 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0432\u0430\u0448\u0438\u044f Subsonic \u0441\u044a\u0440\u0432\u044a\u0440 \u043f\u0440\u0435\u0437 Internet.<br> \
+ \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438, \u043c\u043e\u043b\u044f \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\u0420\u044a\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u043e\u0442\u043e</b></a> \u0437\u0430 \u043d\u043e\u0432\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438.
+networksettings.portforwardingenabled = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0440\u0443\u0442\u0435\u0440 \u0434\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430 \u0432\u0445\u043e\u0434\u044f\u0449\u0438 \u0432\u0440\u044a\u0437\u043a\u0438 \u043a\u044a\u043c Subsonic (\u0447\u0440\u0435\u0437 UPnP \u0438\u043b\u0438 NAT-PMP \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430).
+networksettings.portforwardinghelp = \u0410\u043a\u043e \u0440\u0443\u0442\u0435\u0440\u0430 \u0432\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e, \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u0435 \u0442\u043e\u0432\u0430 \u0440\u044a\u0447\u043d\u043e. \
+ \u0421\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435 \u043e\u0442 <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0438\u0442\u0435 \u043f\u043e\u0440\u0442 {0} \u043a\u044a\u043c \u043a\u043e\u043c\u043f\u044e\u0442\u044a\u0440\u0430 \u0441 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d Subsonic \u0441\u044a\u0440\u0432\u044a\u0440.
+networksettings.urlredirectionenabled = \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u043b\u0435\u0441\u043d\u043e \u0437\u0430\u043f\u043e\u043c\u043d\u044f\u0449 \u0441\u0435 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0432\u0430\u0448\u0438\u044f\u0442 \u0441\u044a\u0440\u0432\u044a\u0440.
+networksettings.status = \u0421\u0442\u0430\u0442\u0443\u0441:
+networksettings.trialexpired = \u041f\u0440\u043e\u0431\u043d\u0438\u044f\u0442 \u043f\u0435\u0440\u0438\u043e\u0434 \u0438\u0437\u0442\u0435\u0447\u0435 \u043d\u0430 {0}. \u041c\u043e\u043b\u044f <b><a href="donate.view?">\u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0434\u0430\u0440\u0435\u043d\u0438\u0435</a></b> \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u0430\u0437\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e.
+networksettings.trialnotexpired = \u0422\u0430\u0437\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u043d\u0430 \u0434\u043e {0}. \u0421\u043b\u0435\u0434 \u0442\u0430\u0437\u0438 \u0434\u0430\u0442\u0430 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u0435 <b><a href="donate.view?">\u0434\u0430\u0440\u0435\u043d\u0438\u0435</a></b> \u0437\u0430 \u0434\u0430 \u044f \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e.
+
+# transcodingSettings.jsp
+transcodingsettings.name = \u0418\u043c\u0435
+transcodingsettings.sourceformat = \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043e\u0442
+transcodingsettings.targetformat = \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u0432
+transcodingsettings.step1 = \u0421\u0442\u044a\u043f\u043a\u0430 1
+transcodingsettings.step2 = \u0421\u0442\u044a\u043f\u043a\u0430 2
+transcodingsettings.step3 = \u0421\u0442\u044a\u043f\u043a\u0430 3
+transcodingsettings.defaultactive = \u041f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435
+transcodingsettings.enabled = \u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e
+transcodingsettings.add = \u0414\u043e\u0431\u0430\u0432\u0438 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435
+transcodingsettings.recommended = \u041f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f
+transcodingsettings.noname = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0438\u043c\u0435.
+transcodingsettings.nosourceformat = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u043e\u0442 \u043a\u043e\u0439\u0442\u043e \u0449\u0435 \u0441\u0435 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430.
+transcodingsettings.notargetformat = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u043a\u044a\u043c \u043a\u043e\u0439\u0442\u043e \u0449\u0435 \u0441\u0435 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430.
+transcodingsettings.nostep1 = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u043d\u0435 \u0435\u0434\u043d\u0430 \u0441\u0442\u044a\u043f\u043a\u0430 \u0437\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435.
+transcodingsettings.info = <p class="detail">(%s = \u0424\u0430\u0439\u043b\u044a\u0442 \u0437\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435, %b = \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0438\u0442\u0440\u0435\u0439\u0442 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430, %t = \u041f\u0435\u0441\u0435\u043d, %a = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b, %l = \u0410\u043b\u0431\u0443\u043c)</p> \
+ <p>\u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0435 \u043f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430 \u043f\u0440\u0435\u0432\u0440\u044a\u0449\u0430\u043d\u0435 \u043d\u0430 \u0435\u0434\u0438\u043d \u043c\u0435\u0434\u0438\u0435\u043d \u0444\u043e\u0440\u043c\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433. \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043f\u0440\u0438 {1} \
+ \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0432\u0430 \u043f\u043e\u0442\u043e\u0447\u043d\u043e \u0438\u0437\u043b\u044a\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0435\u0434\u0438\u0439\u043d\u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u0438, \u043a\u043e\u0438\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u043d\u043e \u043d\u0435 \u0441\u0430 \u043f\u0440\u0438\u0433\u043e\u0434\u0435\u043d\u0438 \u0437\u0430 \u0442\u043e\u0432\u0430. \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0441\u0435 \u0438\u0437\u0432\u044a\u0440\u0448\u0432\u0430 \u043f\u043e \u0432\u0440\u0435\u043c\u0435 \u043d\u0430 \u0441\u0430\u043c\u043e\u0442\u043e \u0441\u043b\u0443\u0448\u0430\u043d\u0435 \u0438 \u0437\u0430\u0442\u043e\u0432\u0430 \
+ \u043d\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e \u0434\u0438\u0441\u043a\u043e\u0432\u043e \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e.<p/> \
+ <p>\u0424\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0442\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u0441\u0435 \u0438\u0437\u0432\u044a\u0440\u0448\u0432\u0430 \u043e\u0442 \u043a\u043e\u043d\u0437\u043e\u043b\u043d\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 \u0442\u0440\u0435\u0442\u0438 \u0441\u0442\u0440\u0430\u043d\u0438, \u043a\u043e\u0438\u0442\u043e \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0441\u0430 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0438 \u0432 {0}. \
+ \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u0449 \u043f\u0430\u043a\u0435\u0442 \u0437\u0430 Windows \
+ \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>\u0442\u0443\u043a</b></a>. \u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 \u0432\u0430\u0448 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u043e\u0440, \u0430\u043a\u043e \u0438\u0437\u043f\u044a\u043b\u043d\u044f\u0432\u0430 \
+ \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430\u043d\u0438\u044f: \
+ <ul> \
+ <li>\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u0440\u0438\u0442\u0435\u0436\u0430\u0432\u0430 \u043a\u043e\u043d\u0437\u043e\u043b\u0435\u043d \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.</li> \
+ <li>\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430 \u0440\u0435\u0437\u0443\u043b\u0442\u0430\u0442\u0430 \u043a\u044a\u043c stdout.</li> \
+ <li>\u0410\u043a\u043e \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0432 \u0441\u0442\u044a\u043f\u043a\u0438 2 \u0438 3, \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0434\u0430\u043d\u043d\u0438 \u043e\u0442 stdin.</li> \
+ </ul> \
+ </p> \
+ <p> \u041a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0441\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430 \u0437\u0430 \u0432\u0441\u0435\u043a\u0438 \u0435\u0434\u0438\u043d \u043f\u043b\u0435\u044a\u0440 \u043f\u043e\u043e\u0442\u0434\u0435\u043b\u043d\u043e, \u043e\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430. \u0410\u043a\u043e \u0435 \u043c\u0430\u0440\u043a\u0438\u0440\u0430\u043d\u043e "\u041f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435", \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435\u0442\u043e \
+ \u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0437\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u043d\u043e\u0432\u0438 \u043f\u043b\u0435\u044a\u0440\u0438.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL \u0430\u0434\u0440\u0435\u0441
+internetradiosettings.homepageurl = \u041e\u0444\u0438\u0446\u0438\u0430\u043b\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430
+internetradiosettings.name = \u0418\u043c\u0435
+internetradiosettings.enabled = \u0412\u043a\u043b\u044e\u0447\u0435\u043d
+internetradiosettings.add = \u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 Internet TV/\u0420\u0430\u0434\u0438\u043e
+internetradiosettings.nourl = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 URL \u0430\u0434\u0440\u0435\u0441.
+internetradiosettings.noname = \u041c\u043e\u043b\u044f \u043f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0438\u043c\u0435.
+
+# podcastSettings.jsp
+podcastsettings.update = \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0437\u0430 \u043d\u043e\u0432\u0438 \u0435\u043f\u0438\u0437\u043e\u0434\u0438
+podcastsettings.keep = \u0417\u0430\u043f\u0430\u0437\u0438
+podcastsettings.keep.all = \u0412\u0441\u0438\u0447\u043a\u0438 \u0435\u043f\u0438\u0437\u043e\u0434\u0438
+podcastsettings.keep.one = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u044f\u0442 \u0435\u043f\u0438\u0437\u043e\u0434
+podcastsettings.keep.many = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 {0} \u0435\u043f\u0438\u0437\u043e\u0434\u0430
+podcastsettings.download = \u041a\u043e\u0433\u0430\u0442\u043e \u0438\u043c\u0430 \u043d\u043e\u0432\u0438 \u0435\u043f\u0438\u0437\u043e\u0434\u0438
+podcastsettings.download.all = \u0421\u0432\u0430\u043b\u0438 \u0432\u0441\u0438\u0447\u043a\u0438
+podcastsettings.download.one = \u0421\u0432\u0430\u043b\u0438 \u0441\u0430\u043c\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u044f
+podcastsettings.download.many = \u0421\u0432\u0430\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 {0} \u0435\u043f\u0438\u0437\u043e\u0434\u0430
+podcastsettings.download.none = \u041d\u0435 \u0441\u0432\u0430\u043b\u044f\u0439
+podcastsettings.interval.manually = \u0420\u044a\u0447\u043d\u043e
+podcastsettings.interval.hourly = \u0412\u0441\u0435\u043a\u0438 \u0447\u0430\u0441
+podcastsettings.interval.daily = \u0412\u0441\u0435\u043a\u0438 \u0434\u0435\u043d
+podcastsettings.interval.weekly = \u0412\u0441\u044f\u043a\u0430 \u0441\u0435\u0434\u043c\u0438\u0446\u0430
+podcastsettings.folder = \u0421\u044a\u0445\u0440\u0430\u043d\u0438 \u043f\u043e\u0434\u043a\u0430\u0441\u0442\u0430 \u0432
+
+# playerSettings.jsp
+playersettings.noplayers = \u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043f\u043b\u0435\u044a\u0440\u0438.
+playersettings.type = \u0412\u0438\u0434
+playersettings.lastseen = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e
+playersettings.title = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043b\u0435\u044a\u0440
+
+playersettings.technology.web.title = \u0423\u0435\u0431 \u043f\u043b\u0435\u044a\u0440
+playersettings.technology.external.title = \u0412\u044a\u043d\u0448\u0435\u043d \u043f\u043b\u0435\u044a\u0440
+playersettings.technology.external_with_playlist.title = \u0412\u044a\u043d\u0448\u0435\u043d \u043f\u043b\u0435\u044a\u0440 \u0441 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+playersettings.technology.jukebox.title = \u0414\u0436\u0443\u0431\u043e\u043a\u0441
+playersettings.technology.web.text = \u0421\u043b\u0443\u0448\u0430\u0439\u0442\u0435 \u043c\u0443\u0437\u0438\u043a\u0430\u0442\u0430 \u0434\u0438\u0440\u0435\u043a\u0442\u043d\u043e \u0432 \u0431\u0440\u0430\u0443\u0437\u044a\u0440\u0430, \u0447\u0440\u0435\u0437 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0438\u044f \u0444\u043b\u0430\u0448 \u043f\u043b\u0435\u044a\u0440.
+playersettings.technology.external.text = \u0421\u043b\u0443\u0448\u0430\u0439\u0442\u0435 \u043c\u0443\u0437\u0438\u043a\u0430\u0442\u0430 \u0432 \u043b\u044e\u0431\u0438\u043c\u0438\u044f \u0441\u0438 \u043f\u043b\u0435\u0439\u044a\u0440, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0432 WinAmp \u0438\u043b\u0438 Windows Media Player.
+playersettings.technology.external_with_playlist.text = \u0421\u044a\u0449\u043e \u043a\u0430\u0442\u043e \u043f\u043e-\u0433\u043e\u0440\u0435, \u043d\u043e \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430\u0442\u0430 \u0441\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0432\u0430 \u043e\u0442 \u043f\u043b\u0435\u044a\u0440\u0430, \
+ \u0430 \u043d\u0435 \u043e\u0442 Subsonic \u0441\u044a\u0440\u0432\u044a\u0440\u0430. \u0412 \u0442\u043e\u0437\u0438 \u0440\u0435\u0436\u0438\u043c \u0435 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u043d\u0430\u043a\u044a\u0441\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u0435\u0441\u043d\u0438\u0442\u0435.
+playersettings.technology.jukebox.text = \u0421\u043b\u0443\u0448\u0430\u0439\u0442\u0435 \u043c\u0443\u0437\u0438\u043a\u0430\u0442\u0430 \u0434\u0438\u0440\u0435\u043a\u0442\u043d\u043e \u0447\u0440\u0435\u0437 \u0430\u0443\u0434\u0438\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u043d\u0430 Subsonic \u0441\u044a\u0440\u0432\u044a\u0440\u0430. (\u0421\u0430\u043c\u043e \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438).
+playersettings.name = \u0418\u043c\u0435 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430
+playersettings.coverartsize = \u0420\u0430\u0437\u043c\u0435\u0440 \u043d\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0438\u0442\u0435
+playersettings.maxbitrate = \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0438\u0442\u0440\u0435\u0439\u0442
+playersettings.coverart.off = \u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e
+playersettings.coverart.small = \u041c\u0430\u043b\u043a\u0438
+playersettings.coverart.medium = \u0421\u0440\u0435\u0434\u043d\u0438
+playersettings.coverart.large = \u0413\u043e\u043b\u0435\u043c\u0438
+playersettings.nolame = <em>Notice:</em> \u041d\u0435 \u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d LAME.<br>\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u041f\u043e\u043c\u043e\u0449 \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.
+playersettings.autocontrol = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430
+playersettings.dynamicip = \u041f\u043b\u0435\u044a\u0440\u044a\u0442 \u0438\u043c\u0430 \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441
+playersettings.transcodings = \u0410\u043a\u0442\u0438\u0432\u043d\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435
+playersettings.ok = \u0417\u0430\u043f\u0430\u0437\u0438
+playersettings.forget = \u0418\u0437\u0442\u0440\u0438\u0439 \u043f\u043b\u0435\u044a\u0440\u0430
+playersettings.clone = \u0414\u0443\u0431\u043b\u0438\u0440\u0430\u0439 \u043f\u043b\u0435\u044a\u0440\u0430
+
+# userSettings.jsp
+usersettings.title = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+usersettings.newuser = \u041d\u043e\u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+usersettings.admin = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440
+usersettings.settings = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u043c\u0435\u043d\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430
+usersettings.stream = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u043b\u0443\u0448\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435
+usersettings.jukebox = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u043b\u0443\u0448\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0432 \u0440\u0435\u0436\u0438\u043c \u0414\u0436\u0443\u0431\u043e\u043a\u0441
+usersettings.download = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u0432\u0430\u043b\u044f \u0444\u0430\u0439\u043b\u043e\u0432\u0435
+usersettings.upload = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u043a\u0430\u0447\u0432\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435
+usersettings.share = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u043f\u043e\u0434\u0435\u043b\u044f \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0441 \u0432\u0441\u0435\u043a\u0438
+usersettings.playlist= \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0432\u0430 \u0438 \u0438\u0437\u0442\u0440\u0438\u0432\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0438
+usersettings.coverart = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u043c\u0435\u043d\u044f \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u0438 \u0442\u0430\u0433\u043e\u0432\u0435 \u043d\u0430 \u043f\u0435\u0441\u043d\u0438\u0442\u0435
+usersettings.comment= \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0432\u0430 \u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0438 \u0438 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0438
+usersettings.podcast= \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0438\u043c\u0430 \u043f\u0440\u0430\u0432\u043e \u0434\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u041f\u043e\u0434\u043a\u0430\u0441\u0442\u0438\u0442\u0435
+usersettings.username = \u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+usersettings.email = Email
+usersettings.changepassword = \u0421\u043c\u044f\u043d\u0430 \u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430
+usersettings.password = \u041f\u0430\u0440\u043e\u043b\u0430
+usersettings.newpassword = \u041d\u043e\u0432\u0430 \u043f\u0430\u0440\u043e\u043b\u0430
+usersettings.confirmpassword = \u041f\u043e\u0432\u0442\u043e\u0440\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430
+usersettings.delete = \u0418\u0437\u0442\u0440\u0438\u0439 \u0442\u043e\u0437\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b
+usersettings.ldap = \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f \u0447\u0440\u0435\u0437 LDAP
+usersettings.nousername = \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435.
+usersettings.noemail= \u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d email \u0430\u0434\u0440\u0435\u0441.
+usersettings.useralreadyexists = \u0418\u043c\u0430 \u0432\u0435\u0447\u0435 \u0442\u0430\u043a\u044a\u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b.
+usersettings.nopassword = \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430.
+usersettings.wrongpassword = \u041f\u0430\u0440\u043e\u043b\u0438\u0442\u0435 \u043d\u0435 \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442.
+usersettings.ldapdisabled = LDAP \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u0442\u0430 \u043d\u0435 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430. \u0412\u0438\u0436\u0442\u0435 \u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.
+usersettings.passwordnotsupportedforldap = \u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u0441\u044a\u0437\u0434\u0430\u0432\u0430 \u0438\u043b\u0438 \u0441\u043c\u0435\u043d\u044f \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438 \u0441 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0447\u0440\u0435\u0437 LDAP.
+usersettings.ok = \u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043c\u0435\u043d\u0435\u043d\u0430 \u0437\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = \u041d\u0438\u043a\u043e\u0433\u0430
+musicfoldersettings.interval.one = \u0412\u0441\u0435\u043a\u0438 \u0434\u0435\u043d
+musicfoldersettings.interval.many = \u0412\u0441\u0435\u043a\u0438 {0} \u0434\u043d\u0438
+musicfoldersettings.hour = \u0432 {0}:00
+
+# main.jsp
+main.up = \u041d\u0430\u0433\u043e\u0440\u0435
+main.playall = \u041f\u0443\u0441\u043d\u0438 \u0432\u0441\u0438\u0447\u043a\u0438
+main.playrandom = \u041f\u0443\u0441\u043d\u0438 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u0438
+main.addall = \u0414\u043e\u0431\u0430\u0432\u0438 \u0432\u0441\u0438\u0447\u043a\u0438
+main.tags = \u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0439 \u0442\u0430\u0433\u043e\u0432\u0435\u0442\u0435
+main.playcount = \u0421\u043b\u0443\u0448\u0430\u043d\u043e {0} \u043f\u044a\u0442\u0438.
+main.lastplayed = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u043e \u043d\u0430 {0}.
+main.comment = \u041a\u043e\u043c\u0435\u043d\u0442\u0438\u0440\u0430\u0439
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>\u041f\u043e\u0434\u0447\u0435\u0440\u0442\u0430\u043d \u0442\u0435\u043a\u0441\u0442 </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>\u041d\u043e\u0432 \u0440\u0435\u0434</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>\u0428\u0440\u0438\u0444\u0442 Italic </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>\u041d\u043e\u0432 \u043f\u0430\u0440\u0430\u0433\u0440\u0430\u0444</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>\u0421\u043f\u0438\u0441\u044a\u043a </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>\u0412\u0440\u044a\u0437\u043a\u0430</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>\u0421\u043f\u0438\u0441\u044a\u043a \u0441 \u043f\u043e\u0440\u0435\u0434\u043d\u0438 \u0446\u0438\u0444\u0440\u0438</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>\u0412\u0440\u044a\u0437\u043a\u0430 \u0441\u044a\u0441 \u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435</td></tr>\
+ </table>
+main.sharealbum = \u0421\u043f\u043e\u0434\u0435\u043b\u0438
+main.more = \u041e\u0449\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f...
+main.more.share = \u0421\u043f\u043e\u0434\u0435\u043b\u0438 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u0442\u0435 \u043f\u0435\u0441\u043d\u0438
+main.donate = <a href="{0}" style="text-decoration:underline">\u041d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0434\u0430\u0440\u0435\u043d\u0438\u0435</a> \u0437\u0430 {1}!<br>(\u0438 \u0441\u043a\u0440\u0438\u0439\u0442\u0435 \u0442\u0430\u0437\u0438 \u0440\u0435\u043a\u043b\u0430\u043c\u0430)
+main.nowplaying = \u0421\u043b\u0443\u0448\u0430\u0442\u0435
+main.lyrics = \u0422\u0435\u043a\u0441\u0442
+main.minutesago = \u043c\u0438\u043d\u0443\u0442\u0438 \u043d\u0430\u0437\u0430\u0434
+main.chat = \u0427\u0430\u0442 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f
+main.message = \u041d\u0430\u043f\u0438\u0448\u0435\u0442\u0435 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435
+main.clearchat = \u0418\u0437\u0447\u0438\u0441\u0442\u0435\u0442\u0435 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u0442\u0430
+
+# rating.jsp
+rating.rating = \u0420\u0435\u0439\u0442\u0438\u043d\u0433
+rating.clearrating = \u0418\u0437\u0442\u0440\u0438\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430
+
+# coverArt.jsp
+coverart.change = \u041f\u0440\u043e\u043c\u0435\u043d\u0438
+coverart.zoom = \u0423\u0432\u0435\u043b\u0438\u0447\u0438
+
+# allmusic.jsp
+allmusic.text = \u0422\u044a\u0440\u0441\u0435\u043d\u0435 \u043d\u0430 \u0430\u043b\u0431\u0443\u043c\u0430 <em>{0}</em> \u0432 allmusic.com - \u041c\u043e\u043b\u044f \u0438\u0437\u0447\u0430\u043a\u0430\u0439\u0442\u0435.
+
+# changeCoverArt.jsp
+changecoverart.title = \u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0430\u0442\u0430
+changecoverart.address = \u0418\u043b\u0438 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0434\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e
+changecoverart.artist = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b
+changecoverart.album = \u0410\u043b\u0431\u0443\u043c
+changecoverart.search = \u0422\u044a\u0440\u0441\u0435\u043d\u0435 \u0432 Google
+changecoverart.wait = \u041c\u043e\u043b\u044f \u0438\u0437\u0447\u0430\u043a\u0430\u0439\u0442\u0435...
+changecoverart.success = \u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0435 \u0441\u0432\u0430\u043b\u0435\u043d\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u043e.
+changecoverart.error = \u0421\u0432\u0430\u043b\u044f\u043d\u0435\u0442\u043e \u043d\u0430 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e.
+changecoverart.noimagesfound = \u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = \u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u0441\u043c\u044f\u043d\u0430 \u043d\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0430:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = \u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0442\u0430\u0433\u043e\u0432\u0435
+edittags.file = \u0424\u0430\u0439\u043b
+edittags.track = \u041f\u0435\u0441\u0435\u043d
+edittags.songtitle = \u0417\u0430\u0433\u043b\u0430\u0432\u0438\u0435
+edittags.artist = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b
+edittags.album = \u0410\u043b\u0431\u0443\u043c
+edittags.year = \u0413\u043e\u0434\u0438\u043d\u0430
+edittags.genre = \u0416\u0430\u043d\u0440
+edittags.status = \u0421\u0442\u0430\u0442\u0443\u0441
+edittags.suggest = \u041f\u043e\u0434\u0441\u043a\u0430\u0436\u0438
+edittags.reset = \u0418\u0437\u0447\u0438\u0441\u0442\u0438
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = \u0417\u0430\u043f\u0430\u0437\u0438
+edittags.working = \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0432\u0430\u043d\u0435
+edittags.updated = \u041e\u0431\u043d\u043e\u0432\u0435\u043d
+edittags.skipped = \u041f\u0440\u043e\u043f\u0443\u0441\u043d\u0430\u0442
+edittags.error = \u0413\u0440\u0435\u0448\u043a\u0430
+
+# share.jsp
+share.title = \u0421\u043f\u043e\u0434\u0435\u043b\u044f\u043d\u0435
+
+# donate.jsp
+donate.title = \u0414\u0430\u0440\u0435\u043d\u0438\u0435
+donate.invalidlicense = \u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043b\u0438\u0446\u0435\u043d\u0437\u0435\u043d \u043a\u043b\u044e\u0447.
+donate.amount = \u0414\u0430\u0440\u0435\u0442\u0435 {0}
+
+donate.textbefore = <p>\u0411\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u044f \u0412\u0438, \u0447\u0435 \u0440\u0435\u0448\u0438\u0445\u0442\u0435 \u0434\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u0435 \u0434\u0430\u0440\u0435\u043d\u0438\u0435 \u0437\u0430 \u0434\u0430 \u043f\u043e\u0434\u043a\u0440\u0435\u043f\u0438\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 {0} ! \
+ \u0414\u0430\u0440\u0438\u0442\u0435\u043b\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u0442 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u043d\u043e\u0441\u0442 \u043a\u0430\u0442\u043e:</p> \
+ <ul> \
+ <li>\u0421\u043b\u0443\u0448\u0430\u043d\u0435 \u043d\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u043c\u0443\u0437\u0438\u043a\u0430 \u043f\u0440\u0435\u0437 <a href="http://subsonic.org/pages/apps.jsp" target="blank">Android, iPhone \u0438 Windows Phone</a>.</li> \
+ <li>\u0412\u0438\u0434\u0435\u043e \u043f\u043e\u0442\u043e\u043a.</li> \
+ <li>\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0435\u043d \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0441\u044a\u0440\u0432\u044a\u0440: <em>\u0432\u0430\u0448\u0435\u0442\u043e\u0438\u043c\u0435</em>.subsonic.org (\u0432\u0438\u0436\u0442\u0435 <a href="networkSettings.view">\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 &gt; \u041c\u0440\u0435\u0436\u0430</a>).</li> \
+ <li>\u0411\u0435\u0437 \u0440\u0435\u043a\u043b\u0430\u043c\u0438 \u0432 \u0443\u0435\u0431 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430\u0442\u0430.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">SubAir</a> - \u0434\u0435\u0441\u043a\u0442\u043e\u043f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441 \u043c\u043d\u043e\u0433\u043e \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438.</li> \
+ <li>\u0414\u0440\u0443\u0433\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u043d\u043e\u0441\u0442, \u043a\u043e\u044f\u0442\u043e \u0449\u0435 \u0431\u044a\u0434\u0435 \u0434\u043e\u0431\u0430\u0432\u044f\u043d\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e.</li> \
+ </ul> \
+ <p> \
+ \u041a\u0430\u0442\u043e \u0434\u0430\u0440\u0438\u0442\u0435\u043b, \u0412\u0438\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u0442\u0435 \u043b\u0438\u0446\u0435\u043d\u0437\u0435\u043d \u043a\u043b\u044e\u0447, \u043a\u043e\u0439\u0442\u043e \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0437\u0430 \u0442\u0430\u0437\u0438 \
+ \u0438 \u0432\u0441\u0438\u0447\u043a\u0438 \u0431\u044a\u0434\u0435\u0449\u0438 \u0432\u0435\u0440\u0441\u0438\u0438 \u043d\u0430 {0}.</p> \
+ <p>\u041f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u0430\u0442\u0430 \u0441\u0443\u043c\u0430 \u0437\u0430 \u0434\u0430\u0440\u0435\u043d\u0438\u0435 \u0435 <b>&euro;20</b>, \u043d\u043e \u0432\u0438\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043a\u0430\u043a\u0432\u0430\u0442\u043e \u0441\u0443\u043c\u0430 \u043f\u043e\u0436\u0435\u043b\u0430\u0435\u0442\u0435:</p>
+donate.textafter = <p>\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u0437\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435\u0442\u0435 \u043f\u0440\u0435\u043f\u0440\u0430\u0442\u0435\u043d\u0438 \u043a\u044a\u043c PayPal , \u043a\u044a\u0434\u0435\u0442\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043b\u0430\u0442\u0438\u0442\u0435 \u0447\u0440\u0435\u0437 \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430 \u043a\u0430\u0440\u0442\u0430 \u0438\u043b\u0438 \u043a\u0430\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \
+ \u0432\u0430\u0448\u0430\u0442\u0430 PayPal \u0441\u043c\u0435\u0442\u043a\u0430 (\u0430\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u0442\u0430\u043a\u0430\u0432\u0430). \u0429\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u043b\u0438\u0446\u0435\u043d\u0437\u043d\u0438\u044f \u043a\u043b\u044e\u0447 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f email \u0434\u043e \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u043c\u0438\u043d\u0443\u0442\u0438.</p> \
+ <p>\u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u0432\u044a\u043f\u0440\u043e\u0441\u0438, \u043c\u043e\u043b\u044f \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u0442\u0435 email \u0434\u043e \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = \u0422\u043e\u0432\u0430 \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 {2} \u0435 \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u0430\u043d\u043e \u0437\u0430 {0} \u043d\u0430 {1}. \u0411\u043b\u0430\u0433\u043e\u0434\u0430\u0440\u0438\u043c \u0412\u0438 \u0437\u0430 \u043f\u043e\u0434\u043a\u0440\u0435\u043f\u0430\u0442\u0430!
+donate.register = \u0421\u043b\u0435\u0434 \u043a\u0430\u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u043b\u0438\u0446\u0435\u043d\u0437\u0435\u043d \u043a\u043b\u044e\u0447, \u043c\u043e\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0433\u043e \u043f\u043e-\u0434\u043e\u043b\u0443.
+donate.register.email = Email
+donate.register.license = \u041b\u0438\u0446\u0435\u043d\u0437
+
+# podcastReceiver.jsp
+podcastreceiver.title = \u041f\u043e\u0434\u043a\u0430\u0441\u0442
+podcastreceiver.expandall = \u041f\u043e\u043a\u0430\u0436\u0438 \u0435\u043f\u0438\u0437\u043e\u0434\u0438\u0442\u0435
+podcastreceiver.collapseall = \u0421\u043a\u0440\u0438\u0439 \u0435\u043f\u0438\u0437\u043e\u0434\u0438\u0442\u0435
+podcastreceiver.status.new = \u041d\u043e\u0432
+podcastreceiver.status.downloading = \u0421\u0432\u0430\u043b\u044f\u043d\u0435
+podcastreceiver.status.completed = \u0417\u0430\u0432\u044a\u0440\u0448\u0435\u043d\u043e
+podcastreceiver.status.error = \u0413\u0440\u0435\u0448\u043a\u0430
+podcastreceiver.status.deleted = \u0418\u0437\u0442\u0440\u0438\u0442\u043e
+podcastreceiver.status.skipped = \u041f\u0440\u043e\u043f\u0443\u0441\u043d\u0430\u0442\u043e
+podcastreceiver.downloadselected= \u0421\u0432\u0430\u043b\u0438 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u0442\u0435
+podcastreceiver.deleteselected= \u0418\u0437\u0442\u0440\u0438\u0439 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u0442\u0435
+podcastreceiver.confirmdelete= \u0418\u0437\u0442\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u0442\u0435?
+podcastreceiver.check = \u041f\u0440\u043e\u0432\u0435\u0440\u0438 \u0437\u0430 \u043d\u043e\u0432\u0438 \u0435\u043f\u0438\u0437\u043e\u0434\u0438
+podcastreceiver.refresh = \u041f\u0440\u0435\u0437\u0430\u0440\u0435\u0434\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430
+podcastreceiver.settings = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u043f\u043e\u0434\u043a\u0430\u0441\u0442\u0430
+podcastreceiver.subscribe = \u0410\u0431\u043e\u043d\u0430\u043c\u0435\u043d\u0442 \u0437\u0430 \u043f\u043e\u0434\u043a\u0430\u0441\u0442
+
+# lyrics.jsp
+lyrics.title = \u0422\u0435\u043a\u0441\u0442 \u043d\u0430 \u043f\u0435\u0441\u0435\u043d\u0442\u0430
+lyrics.artist = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b
+lyrics.song = \u041f\u0435\u0441\u0435\u043d
+lyrics.search = \u0422\u044a\u0440\u0441\u0438
+lyrics.wait = \u0422\u044a\u0440\u0441\u0435\u043d\u0435 \u0437\u0430 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u0435, \u043c\u043e\u043b\u044f \u0438\u0437\u0447\u0430\u043a\u0430\u0439\u0442\u0435...
+lyrics.courtesy = (\u0422\u0435\u043a\u0441\u0442\u043e\u0432\u0435 \u043e\u0442 <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = \u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0442\u0435\u043a\u0441\u0442.
+
+# helpPopup.jsp
+helppopup.title = {0} \u041f\u043e\u043c\u043e\u0449
+helppopup.cover.title = \u0420\u0430\u0437\u043c\u0435\u0440 \u043d\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0430\u0442\u0430
+helppopup.cover.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u0435 \u0440\u0430\u0437\u043c\u0435\u0440\u0430 \u043d\u0430 \u043f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0438\u0442\u0435 \u043e\u0431\u043b\u043e\u0436\u043a\u0438, \u043a\u0430\u043a\u0442\u043e \u0438 \u043d\u0430\u043f\u044a\u043b\u043d\u043e \u0434\u0430 \u0438\u0437\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u0430\u0437\u0438 \u043e\u043f\u0446\u0438\u044f.</p>
+helppopup.transcode.title = \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0438\u0442\u0440\u0435\u0439\u0442
+helppopup.transcode.text = <p>\u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u043d\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430, \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u0435 \u0433\u043e\u0440\u043d\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430 \u0431\u0438\u0442\u0440\u0435\u0439\u0442\u0430 \u043d\u0430 \u043c\u0443\u0437\u0438\u043a\u0430\u043b\u043d\u0438\u044f \u043f\u043e\u0442\u043e\u043a. \
+ \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0430\u043a\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u043d\u0438\u0442\u0435 \u0432\u0438 mp3 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0441\u0430 \u0441 \u0431\u0438\u0442\u0440\u0435\u0439\u0442 256 Kbps (\u043a\u0438\u043b\u043e\u0431\u0438\u0442\u0430 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430), \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u043d\u0435 \u043d\u0430 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0438\u0442\u0440\u0435\u0439\u0442 \
+ 128 \u0449\u0435 \u043d\u0430\u043a\u0430\u0440\u0430 {0} \u0434\u0430 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043c\u0443\u0437\u0438\u043a\u0430\u0442\u0430 \u043e\u0442 256 \u043d\u0430 128 Kbps.</p> \
+ <p>\u0422\u0430\u0437\u0438 \u043e\u043f\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 \u0434\u0430 \u0438\u043c\u0430\u0442\u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d LAME . LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \u0435 mp3 \u0435\u043d\u043a\u043e\u0434\u0435\u0440 \u0441 \u043e\u0442\u0432\u043e\u0440\u0435\u043d \u043a\u043e\u0434. \u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0433\u043e <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">\u0441\u0432\u0430\u043b\u0438\u0442\u0435 \u043e\u0442 \u0442\u0443\u043a</a>. \
+ \u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0433\u043e \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u0442\u0435 \u0432 \u043f\u0430\u043f\u043a\u0430 SUBSONIC_HOME/transcode .</p>
+helppopup.playlistfolder.title = \u041f\u0430\u043f\u043a\u0430 \u0437\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0438
+helppopup.playlistfolder.text = <p>\u0422\u0443\u043a \u043f\u043e\u0441\u043e\u0447\u0432\u0430\u0442\u0435 \u043f\u0430\u043f\u043a\u0430\u0442\u0430, \u043a\u044a\u0434\u0435\u0442\u043e \u0449\u0435 \u0441\u0435 \u0441\u044a\u0445\u0440\u0430\u043d\u044f\u0432\u0430\u0442 \u0432\u0430\u0448\u0438\u0442\u0435 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0438.</p>
+helppopup.musicmask.title = \u0424\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u043c\u0443\u0437\u0438\u043a\u0430
+helppopup.musicmask.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043a\u043e\u0438 \u0442\u0438\u043f\u043e\u0432\u0435 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0449\u0435 \u0441\u0435 \u0440\u0430\u0437\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442 \u043a\u0430\u0442\u043e \u043c\u0443\u0437\u0438\u043a\u0430\u043b\u043d\u0438.</p>
+helppopup.videomask.title = \u0424\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u0432\u0438\u0434\u0435\u043e
+helppopup.videomask.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043a\u043e\u0438 \u0442\u0438\u043f\u043e\u0432\u0435 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0449\u0435 \u0441\u0435 \u0440\u0430\u0437\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442 \u043a\u0430\u0442\u043e \u0432\u0438\u0434\u0435\u043e.</p>
+helppopup.coverartmask.title = \u0424\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0438
+helppopup.coverartmask.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043a\u043e\u0438 \u0442\u0438\u043f\u043e\u0432\u0435 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0449\u0435 \u0441\u0435 \u0440\u0430\u0437\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442 \u043a\u0430\u0442\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0438 \u0437\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0438, \u043a\u043e\u0433\u0430\u0442\u043e \u0440\u0430\u0437\u0433\u043b\u0435\u0436\u0434\u0430\u0442\u0435 \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u0434\u0430\u0434\u0435\u043d\u0430 \u043f\u0430\u043f\u043a\u0430.</p>
+helppopup.downsamplecommand.title = \u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u0437\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043d\u0435
+helppopup.downsamplecommand.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043a\u0430\u043a\u0432\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u044a\u043b\u043d\u044f\u0432\u0430, \u043a\u043e\u0433\u0430\u0442\u043e \u0441\u0435 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430 \u043a\u044a\u043c \u043f\u043e-\u043d\u0438\u0441\u044a\u043a \u0431\u0438\u0442\u0440\u0435\u0439\u0442.</p>\
+ <p>(%s = \u0424\u0430\u0439\u043b\u044a\u0442, \u043a\u043e\u0439\u0442\u043e \u0449\u0435 \u0441\u0435 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0430, %b = \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0438\u0442\u0440\u0435\u0439\u0442 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430, %t = \u0417\u0430\u0433\u043b\u0430\u0432\u0438\u0435, %a = \u0418\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b, %l = \u0410\u043b\u0431\u0443\u043c)</p>
+helppopup.index.title = \u0418\u043d\u0434\u0435\u043a\u0441
+helppopup.index.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u0435 \u043a\u0430\u043a \u0434\u0430 \u0438\u0437\u0433\u043b\u0435\u0436\u0434\u0430 \u0438\u043d\u0434\u0435\u043a\u0441\u0430 (\u043d\u0430\u043c\u0438\u0440\u0430\u0449 \u0441\u0435 \u0432\u043b\u044f\u0432\u043e \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0430) . \u0424\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u0438 \u043f\u0430\u043f\u043a\u0438\u0442\u0435, \
+ \u043a\u043e\u0438\u0442\u043e \u0441\u0430 \u0432 \u043e\u0441\u043d\u043e\u0432\u043d\u0430\u0442\u0430 \u043c\u0443\u0437\u0438\u043a\u0430\u043b\u043d\u0430 \u043f\u0430\u043f\u043a\u0430, \u043c\u043e\u0433\u0430\u0442 \u043b\u0435\u0441\u043d\u043e \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0440\u0430\u0437\u0433\u043b\u0435\u0436\u0434\u0430\u043d\u0438 \u0447\u0440\u0435\u0437 \u0442\u043e\u0437\u0438 \u0438\u043d\u0434\u0435\u043a\u0441.</p> \
+ <p>\u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0432\u0430 \u0441\u043f\u0438\u0441\u044a\u043a \u043d\u0430 \u043e\u0442\u0434\u0435\u043b\u043d\u0438\u0442\u0435 \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0438 \u043d\u0430 \u0438\u043d\u0434\u0435\u043a\u0441\u0430. \u041e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e, \u0432\u0441\u0435\u043a\u0438 \u0435\u0434\u0438\u043d \u0435\u043b\u0435\u043c\u0435\u043d\u0442 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430 \u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0435\u0434\u043d\u0430 \u0431\u0443\u043a\u0432\u0430, \
+ \u043d\u043e \u0432\u0438\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u0438\u043c\u0432\u043e\u043b\u0430. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0430\u043a\u043e \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 <em>The</em> \u0442\u043e \u0442\u043e\u0437\u0438 \u043b\u0438\u043d\u043a \u0449\u0435 \u043e\u0442\u0432\u0430\u0440\u044f \u0432\u0441\u0438\u0447\u043a\u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0438 \
+ \u043f\u0430\u043f\u043a\u0438 \u0437\u0430\u043f\u043e\u0447\u0432\u0430\u0449\u0438 \u0441 "The".</p> \
+ <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0441\u044a\u0449\u043e \u0442\u0430\u043a\u0430 \u0434\u0430 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u0442\u0435 \u0433\u0440\u0443\u043f\u0430 \u043e\u0442 \u0441\u0438\u043c\u0432\u043e\u043b\u0438, \u043f\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438 \u0432 \u0441\u043a\u043e\u0431\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0430\u043a\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \
+ <em>A-E(ABCDE)</em> \u0449\u0435 \u0441\u0435 \u043f\u043e\u043a\u0430\u0437\u0432\u0430 \u043a\u0430\u0442\u043e <em>A-E</em>, \u0430 \u043b\u0438\u043d\u043a\u0430 \u0449\u0435 \u043e\u0442\u0432\u0430\u0440\u044f \u0432\u0441\u0438\u0447\u043a\u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u0435 \u0438 \u043f\u0430\u043f\u043a\u0438 \u0437\u0430\u043f\u043e\u0447\u0432\u0430\u0449\u0438 \u0441 \
+ A, B, C, D \u0438\u043b\u0438 E. \u0422\u043e\u0432\u0430 \u0435 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u043d\u043e \u0437\u0430 \u0433\u0440\u0443\u043f\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e-\u0440\u044f\u0434\u043a\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0438 \u0441\u0438\u043c\u0432\u043e\u043b\u0438 \u0438 \u0431\u0443\u043a\u0432\u0438 (\u043a\u0430\u0442\u043e X, Y \u0438 Z), \u0438\u043b\u0438 \
+ \u0437\u0430 \u0433\u0440\u0443\u043f\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0438\u043c\u0432\u043e\u043b\u0438 \u0441 \u0443\u0434\u0430\u0440\u0435\u043d\u0438\u044f (\u043a\u0430\u0442\u043e A, \u00c0 \u0438 \u00c1)</p> \
+ <p>\u0424\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435 \u0438 \u043f\u0430\u043f\u043a\u0438\u0442\u0435, \u043a\u043e\u0438\u0442\u043e \u043d\u0435 \u0441\u0430 \u0447\u0430\u0441\u0442 \u043e\u0442 \u0434\u0430\u0434\u0435\u043d\u0430 \u0433\u0440\u0443\u043f\u0430 \u0432 \u0438\u043d\u0434\u0435\u043a\u0441\u0430, \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u043f\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438 \u043f\u043e\u0434 \u043e\u0431\u0449\u0430 \u0433\u0440\u0443\u043f\u0430 "#".</p>
+helppopup.ignoredarticles.title = \u0418\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u043d\u0438 \u0441\u0438\u043c\u0432\u043e\u043b\u0438
+helppopup.ignoredarticles.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u0441\u043f\u0438\u0441\u044a\u043a \u043e\u0442 \u0431\u0443\u043a\u0432\u0438 \u0438 \u0441\u0438\u043c\u0432\u043e\u043b\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 "The"), \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u043d\u0438 \u043f\u0440\u0438 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0434\u0435\u043a\u0441\u0430.</p>
+helppopup.shortcuts.title = \u0412\u0440\u044a\u0437\u043a\u0438
+helppopup.shortcuts.text = <p>\u0421\u043f\u0438\u0441\u044a\u043a \u043e\u0442 \u043e\u0442\u0434\u0435\u043b\u043d\u0438 \u043f\u0430\u043f\u043a\u0438 \u043a\u044a\u043c \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u0438 \u0432\u0440\u044a\u0437\u043a\u0438. \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u043a\u0430\u0432\u0438\u0447\u043a\u0438 \u0437\u0430 \u0434\u0430 \u0433\u0440\u0443\u043f\u0438\u0440\u0430\u0442\u0435 \u0434\u0443\u043c\u0438, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:</p> \
+ <p><em>\u041d\u043e\u0432\u0438 \u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438 "\u041f\u0435\u0441\u043d\u0438 \u043e\u0442 \u0444\u0438\u043b\u043c\u0438"</em></p>
+helppopup.language.title = \u0415\u0437\u0438\u043a
+helppopup.language.text = <p>\u0422\u0443\u043a \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043a\u0430\u043a\u044a\u0432 \u0435\u0437\u0438\u043a \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435.</p>
+helppopup.visibility.title = \u0412\u044a\u043d\u0448\u0435\u043d \u0432\u0438\u0434
+helppopup.visibility.text = <p>\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043a\u0430\u043a\u0432\u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0438 \u0437\u0430 \u0432\u0441\u044f\u043a\u0430 \u043f\u0435\u0441\u0435\u043d, \u0430 \u0442\u0430\u043a\u0430 \u0441\u044a\u0449\u043e \u0441\u043b\u0435\u0434 \u043a\u043e\u043b\u043a\u043e \u0431\u0443\u043a\u0432\u0438 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0441\u044a\u043a\u0440\u0430\u0449\u0430\u0432\u0430\u043d\u043e \u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435\u0442\u043e. \u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \
+ \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0438\u044f \u0431\u0440\u043e\u0439 \u0441\u0438\u043c\u0432\u043e\u043b\u0438 \u0437\u0430 \u0437\u0430\u0433\u043b\u0430\u0432\u0438\u0435 \u043d\u0430 \u043f\u0435\u0441\u0435\u043d, \u0430\u043b\u0431\u0443\u043c \u0438 \u0438\u0437\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b.</p>
+helppopup.partymode.title = \u041e\u043b\u0435\u043a\u043e\u0442\u0435\u043d \u0440\u0435\u0436\u0438\u043c
+helppopup.partymode.text = <p>\u041a\u043e\u0433\u0430\u0442\u043e \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0442\u043e\u0437\u0438 \u0440\u0435\u0436\u0438\u043c, \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0435 \u043e\u043f\u0440\u043e\u0441\u0442\u0435\u043d \u0438 \u043f\u043e-\u043b\u0435\u0441\u0435\u043d \u0437\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043e\u0442 \u043d\u0435\u043e\u043f\u0438\u0442\u043d\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438. \
+ \u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0430 \u0441\u0435 \u0438\u0437\u0431\u044f\u0433\u0432\u0430 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u043e\u0431\u044a\u0440\u043a\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0438\u0442\u0435.</p>
+helppopup.theme.title = \u0422\u0435\u043c\u0430
+helppopup.theme.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043a\u0430\u043a\u0432\u0430 \u0442\u0435\u043c\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0437\u0430 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430. \u0422\u0435\u043c\u0430\u0442\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f \u0432\u044a\u043d\u0448\u043d\u0438\u044f \u0432\u0438\u0434 \u043d\u0430 {0} \u043f\u043e \u043e\u0442\u043d\u043e\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u0446\u0432\u0435\u0442\u043e\u0432\u0435, \u0448\u0440\u0438\u0444\u0442\u043e\u0432\u0435, \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438.\u0442.\u043d</p>
+helppopup.welcomemessage.title = \u041f\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043e \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435
+helppopup.welcomemessage.text = <p>\u0421\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435\u0442\u043e, \u043a\u043e\u0435\u0442\u043e \u0441\u0435 \u043f\u043e\u043a\u0430\u0437\u0432\u0430 \u0432 \u043d\u0430\u0447\u0430\u043b\u043d\u0430\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430.</p>
+helppopup.loginmessage.title = \u0421\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435
+helppopup.loginmessage.text = <p>\u0422\u043e\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0441\u0435 \u043f\u043e\u043a\u0430\u0437\u0432\u0430 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b.</p>
+helppopup.coverartlimit.title = \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u0437\u0430 \u043e\u0431\u043b\u043e\u0436\u043a\u0438
+helppopup.coverartlimit.text = <p>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0438\u044f\u0442 \u0431\u0440\u043e\u0439 \u043e\u0431\u043b\u043e\u0436\u043a\u0438, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u0441\u0435 \u043f\u043e\u043a\u0430\u0437\u0432\u0430\u0442 \u043d\u0430 \u0435\u0434\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430.</p>
+helppopup.downloadlimit.title = \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u0441\u0432\u0430\u043b\u044f\u043d\u0435
+helppopup.downloadlimit.text = <p>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a\u0432\u0430 \u0447\u0430\u0441\u0442 \u043e\u0442 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u0449\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u0441\u0432\u0430\u043b\u044f\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435.</p>
+helppopup.uploadlimit.title = \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u043a\u0430\u0447\u0432\u0430\u043d\u0435
+helppopup.uploadlimit.text = <p>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0430 \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a\u0432\u0430 \u0447\u0430\u0441\u0442 \u043e\u0442 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u0449\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u043a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b\u043e\u0432\u0435\u0442\u0435.</p>
+helppopup.streamport.title = \u041f\u043e\u0440\u0442 \u0437\u0430 \u043d\u0435\u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d (SSL) \u043f\u043e\u0442\u043e\u043a
+helppopup.streamport.text = <p>\u0422\u0430\u0437\u0438 \u043e\u043f\u0446\u0438\u044f \u0435 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0441\u0430\u043c\u043e \u0430\u043a\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 {0} \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440 \u0441\u044a\u0441 SSL (HTTPS).</p><p>\u041d\u044f\u043a\u043e\u0438 \u043f\u043b\u0435\u044a\u0440\u0438 \
+ (\u043a\u0430\u0442\u043e Winamp) \u043d\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u043f\u043e\u0442\u043e\u0447\u043d\u043e \u0430\u0443\u0434\u0438\u043e \u043f\u043e\u0434 SSL. \u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u043d\u043e\u043c\u0435\u0440\u0430 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0437\u0430 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0435\u043d http (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u0435 80 \
+ \u0438\u043b\u0438 4040) \u0430\u043a\u043e \u043d\u0435 \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u043f\u043e\u0442\u043e\u043a\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043b\u044a\u0447\u0432\u0430\u043d \u043f\u043e\u0434 SSL. \u0412 \u0442\u043e\u0437\u0438 \u0441\u043b\u0443\u0447\u0430\u0439, \u043f\u043e\u0442\u043e\u043a\u0430 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d.</p>
+helppopup.ldap.title = LDAP \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0430\u0432\u0430\u043d\u0435
+helppopup.ldap.text = <p>\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438\u0442\u0435 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 \u043f\u0440\u0438 \u0432\u0445\u043e\u0434 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043e\u0442 \u0432\u044a\u043d\u0448\u0435\u043d LDAP \u0441\u044a\u0440\u0432\u044a\u0440 (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u043d\u043e Windows Active Directory). \
+ \u041a\u043e\u0433\u0430\u0442\u043e LDAP-\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438 \u0432\u043b\u0438\u0437\u0430\u0442 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 {0}, \u0442\u044f\u0445\u043d\u043e\u0442\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0441\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u0442 \u043e\u0442 \u0432\u044a\u043d\u0448\u0435\u043d \u0441\u044a\u0440\u0432\u044a\u0440, \u0430 \u043d\u0435 \u043e\u0442 {0} .</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p> URL \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 LDAP \u0441\u044a\u0440\u0432\u044a\u0440\u0430. \u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044a\u0442 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 <em>ldap://</em> \u0438\u043b\u0438 <em>ldaps://</em> \
+ (\u0437\u0430 LDAP \u043f\u043e\u0434 SSL). \u0412\u0438\u0436\u0442\u0435 <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">\u0442\u0443\u043a</a> \
+ \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438.</p>
+helppopup.ldapsearchfilter.title = LDAP \u0444\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u0442\u044a\u0440\u0441\u0435\u043d\u0435
+helppopup.ldapsearchfilter.text = <p>\u0424\u0438\u043b\u0442\u044a\u0440, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0437\u0430 \u0442\u044a\u0440\u0441\u0435\u043d\u0435 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438. \u0422\u043e\u0432\u0430 \u0435 LDAP \u0444\u0438\u043b\u0442\u044a\u0440 \u0437\u0430 \u0442\u044a\u0440\u0441\u0435\u043d\u0435 \
+ (\u043a\u0430\u043a\u0442\u043e \u0435 \u043e\u043f\u0438\u0441\u0430\u043d \u0432 <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ \u0428\u0430\u0431\u043b\u043e\u043d\u044a\u0442 "'{0'}" \u0441\u0435 \u0437\u0430\u043c\u0435\u0441\u0442\u0432\u0430 \u0441 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: \
+ <ul>\
+ <li>(uid='{0'}) - \u0449\u0435 \u0442\u044a\u0440\u0441\u0438 \u0437\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0435 \u0441 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0441\u043f\u043e\u0440\u0435\u0434 uid \u0444\u0430\u043a\u0442\u043e\u0440\u0430.</li> \
+ <li>(sAMAccountName='{0'}) - \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u043f\u0440\u0435\u0434\u0438\u043c\u043d\u043e \u0437\u0430 \u0432\u0445\u043e\u0434 \u0432 Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP \u043c\u0435\u043d\u0438\u0434\u0436\u044a\u0440 DN
+helppopup.ldapmanagerdn.text = <p>\u0410\u043a\u043e LDAP \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430, \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 DN \
+ (<em>Distinguished Name</em>) \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 LDAP \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0437\u0430 \u043e\u0441\u044a\u0449\u0435\u0441\u0442\u0432\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430.</p>
+helppopup.ldapautoshadowing.title = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 LDAP \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438 \u0432 {0}
+helppopup.ldapautoshadowing.text = <p>\u0410\u043a\u043e \u0442\u0430\u0437\u0438 \u043e\u043f\u0446\u0438\u044f \u0435 \u043c\u0430\u0440\u043a\u0438\u0440\u0430\u043d\u0430, LDAP \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438\u0442\u0435 \u043d\u0435 \u0435 \u043d\u0443\u0436\u043d\u043e \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0438 \u0440\u044a\u0447\u043d\u043e \u0432 {0} \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0432\u043b\u044f\u0437\u0430\u0442 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438.</p> \
+ <p>\u0412\u043d\u0438\u043c\u0430\u043d\u0438\u0435! \u0422\u043e\u0432\u0430 \u043e\u0437\u043d\u0430\u0447\u0430\u0432\u0430, \u0447\u0435 \u0432\u0441\u0435\u043a\u0438 \u0435\u0434\u0438\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0441 \u0432\u0430\u043b\u0438\u0434\u043d\u043e LDAP \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0432\u043b\u0435\u0437\u0435 \u0432 {0}, \
+ \u043a\u043e\u0435\u0442\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u043d\u0435 \u0436\u0435\u043b\u0430\u0435\u0442\u0435.</p>
+helppopup.playername.title = \u0417\u0430\u0433\u043b\u0430\u0432\u0438\u0435 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430
+helppopup.playername.text = <p>\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043b\u0435\u0441\u043d\u043e \u0437\u0430 \u0437\u0430\u043f\u043e\u043c\u043d\u044f\u043d\u0435 \u0438\u043c\u0435 \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 "\u0421\u043b\u0443\u0436\u0435\u0431\u0435\u043d" \u0438\u043b\u0438 "\u0414\u043e\u043c\u0430\u0448\u0435\u043d".</p>
+helppopup.autocontrol.title = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430 \u043f\u043b\u0435\u044a\u0440\u0430
+helppopup.autocontrol.text = <p>\u0410\u043a\u043e \u0435 \u0438\u0437\u0431\u0440\u0430\u043d\u0430 \u0442\u0430\u0437\u0438 \u043e\u043f\u0446\u0438\u044f, {0} \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0449\u0435 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430 \u043f\u043b\u0435\u044a\u0440\u0430, \u043a\u043e\u0433\u0430\u0442\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 "\u041f\u0443\u0441\u043d\u0438" \
+ \u0432 \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430\u0442\u0430. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u0435\u043d \u0441\u043b\u0443\u0447\u0430\u0439, \u0442\u0440\u044f\u0431\u0432\u0430 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435 \u0438 \u043f\u0443\u0441\u043a\u0430\u0442\u0435 \u043f\u043b\u0435\u044a\u0440\u0430.</p>
+helppopup.dynamicip.title = \u0414\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441
+helppopup.dynamicip.text = <p>\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u0442\u0430\u0437\u0438 \u043e\u043f\u0446\u0438\u044f, \u0430\u043a\u043e \u043f\u043b\u0435\u044a\u0440\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441.</p>
+
+# wap/index.jsp
+wap.index.missing = \u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0430 \u043c\u0443\u0437\u0438\u043a\u0430
+wap.index.playlist = \u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+wap.index.search = \u0422\u044a\u0440\u0441\u0438
+wap.index.settings = \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
+
+# wap/browse.jsp
+wap.browse.playone = \u041f\u0443\u0441\u043d\u0438
+wap.browse.playall = \u041f\u0443\u0441\u043d\u0438 \u0432\u0441\u0438\u0447\u043a\u0438
+wap.browse.addone = \u0414\u043e\u0431\u0430\u0432\u0438
+wap.browse.addall = \u0414\u043e\u0431\u0430\u0432\u0438 \u0432\u0441\u0438\u0447\u043a\u0438
+wap.browse.downloadone = \u0421\u0432\u0430\u043b\u0438
+wap.browse.downloadall = \u0421\u0432\u0430\u043b\u0438 \u0432\u0441\u0438\u0447\u043a\u0438
+
+# wap/playlist.jsp
+wap.playlist.title = \u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0430
+wap.playlist.noplayer = \u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u043f\u043b\u0435\u044a\u0440
+wap.playlist.clear = \u0418\u0437\u0447\u0438\u0441\u0442\u0438
+wap.playlist.load = \u0417\u0430\u0440\u0435\u0434\u0438
+wap.playlist.random = \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u0438
+wap.playlist.play = \u041f\u0443\u0441\u043d\u0438 \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430
+
+# wap/search.jsp
+wap.search.title = \u0422\u044a\u0440\u0441\u0435\u043d\u0435
+
+# wap/searchResult.jsp
+wap.searchresult.index = \u0418\u043d\u0434\u0435\u043a\u0441\u044a\u0442 \u043d\u0430 \u0442\u044a\u0440\u0441\u0430\u0447\u043a\u0430\u0442\u0430 \u0441\u0435 \u0441\u044a\u0437\u0434\u0430\u0432\u0430 \u0432 \u043c\u043e\u043c\u0435\u043d\u0442\u0430. \u041c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.
+
+# wap/settings.jsp
+wap.settings.selectplayer = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043b\u0435\u044a\u0440
+wap.settings.allplayers = \u0412\u0441\u0438\u0447\u043a\u0438 \ No newline at end of file
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties
new file mode 100644
index 00000000..30ed0d53
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties
@@ -0,0 +1,753 @@
+#
+# Catalan localization.
+# Author: Josep Santalo Jordana (jsantajor at gmail.com)
+#
+
+common.home = Inici
+common.back = Enrere
+common.help = Ajuda
+common.play = Reproduir
+common.add = Afegir
+common.download = Descarregar
+common.close = Tancar
+common.refresh = Actualitzar
+common.next = Següent
+common.previous = Anterior
+common.more = Més
+common.ok = OK
+common.cancel = Cancel·la
+common.save = Guardar
+common.create = Crear
+common.delete = Esborrar
+common.unknown = (Desconegut)
+common.default = (Predeterminat)
+
+# login.jsp
+login.username = Usuari
+login.password = Contrasenya
+login.login = Iniciar sessió
+login.remember = Recordat
+login.logout = Desconnexió correcta.
+login.error = Usuari o contrasenya incorrecta.
+login.insecure = {0} no és segur. Si us plau inicieu sessió amb usuari<br>i contrasenya "admin", o cliqui <a href="login.view?user=admin&amp;password=admin">aquí</a>. Tot seguit, canviï la contrasenya el més ràpid possible.
+
+# accessDenied.jsp
+accessDenied.title = Accés denegat
+accessDenied.text = Vostè no està autoritzat a realitzar aquesta operació.
+
+# top.jsp
+top.home = Inici
+top.now_playing = Reproduint
+top.settings = Configuració
+top.status = Estat
+top.podcast = Podcast
+top.more = Més
+top.help = Ajuda
+top.search = Buscar
+top.upgrade = <b>Avis!</b> Una nova versió està disponible.<br>Descarregar {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">aquí</a>.
+top.missing = No s'ha trobat cap directori. Si us plau, canviï la configuració.
+top.logout = Desconnectar {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artistes<br>\
+ {1}&nbsp;àlbums<br>\
+ {2}&nbsp;cançons<br>\
+ {3}<br>\
+ {4}&nbsp;hores
+left.shortcut = Accés directe
+left.radio = Internet TV/radio
+left.allfolders = Tots els directoris
+
+# playlist.jsp
+playlist.stop = Parar
+playlist.start = Reproduir
+playlist.confirmclear = Realment vol netejar la llista?
+playlist.clear = Netejar
+playlist.shuffle = Mode aleatori
+playlist.repeat_on = Desactivar repetir
+playlist.repeat_off = Activar repetir
+playlist.undo = Desfer
+playlist.settings = Configuració
+playlist.more = Més accions...
+playlist.more.playlist = Llista de reproducció
+playlist.more.sortbytrack = Ordenar per pista
+playlist.more.sortbyartist = Ordenar per artista
+playlist.more.sortbyalbum = Ordenar per àlbum
+playlist.more.selection = Cançons seleccionades
+playlist.more.selectall = Seleccionar-ho tot
+playlist.more.selectnone = Esborrar selecció
+playlist.getflash = Get Flash player
+playlist.load = Carregar
+playlist.save = Guardar
+playlist.append = Afegir a la llista de reproducció
+playlist.remove = Esborrar
+playlist.up = Amunt
+playlist.down = Avall
+playlist.empty = La llista de reproducció està buida
+
+# videoPlayer.jsp
+videoPlayer.getflash = Si us plau, instal·li Flash Player
+videoPlayer.popout = Obrir a una nova finestra
+
+# loadPlaylist.jsp
+playlist.load.title = Carregar llista de reproducció
+playlist.load.appendtitle = Afegir a la llista de reproducció
+playlist.load.load = Carregar
+playlist.load.append = Afegir
+playlist.load.delete = Eliminar
+playlist.load.confirm_delete = Realment vol eliminar la llista de reproducció?
+playlist.load.missing_folder = El directori de la llista de reproducció "{0}" no existeix. Si us plau, canviï la configuració.
+playlist.load.empty = No hi ha cap llista de reproducció disponible.
+
+# savePlaylist.jsp
+playlist.save.title = Guardar la llista de reproducció
+playlist.save.save = Guardar
+playlist.save.name = Nom de la llista de reproducció
+playlist.save.format = Format
+playlist.save.missing_folder = El directori de la llista de reproducció "{0}" no existeix. Si us plau, canviï la configuració.
+playlist.save.noname = Si us plau, especifiqui el nom de la llista de reproducció.
+
+# status.jsp
+status.title = Estat
+status.type = Tipus
+status.stream = Stream
+status.download = Descarregar
+status.upload = Pujar
+status.player = Oient
+status.user = Usuari
+status.current = Cançó
+status.transmitted = Transmès
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Buscar
+search.query = Artista, àlbum o nom de la cançó
+search.search = Buscar
+search.index = L'índex de cerca s'està creant en aquest moment. Si us plau torni-ho a intentar més tard.
+search.hits.none = No s'han trobat resultats.
+search.hits.more = Més
+search.hits.artists = Artistes
+search.hits.albums = Àlbums
+search.hits.songs = Cançons
+
+# gettingStarted.jsp
+gettingStarted.title = Primers passos
+gettingStarted.text = <p>Benvingut a Subsonic! Per tal de configurar el programa de la manera més ràpida possible, només cal que segueixi els següent passos.<br> \
+ Cliqui al botó d''Inici que hi ha a la barra superior per tal de tornar a aquesta pàgina.</p> \
+ <p>Per a més informació, consulti la pàgina la guia <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Getting started</b></a>.</p>
+gettingStarted.step1.title = Canviar la contrasenya de l'administrador.
+gettingStarted.step1.text = Per tal de fer més segur el seu servidor es recomana canviar la contrasenya per defecte de l''administrador. \
+ També pot crear comptes d'usuari nous amb diferents privilegis associats.
+gettingStarted.step2.title = Configuri els directoris multimèdia.
+gettingStarted.step2.text = Indiqui a Subsonic a on guarda els arxius de música i de vídeo.
+gettingStarted.step3.title = Configuri els paràmetres de xarxa.
+gettingStarted.step3.text = Paràmetres útil per tal de gaudir de Subsonic remotament a través de Internet, \
+ o compartir-ho amb la família i amics. Aconsegueixi la seva adreça personal <b><em>el_seu_nom</em>.subsonic.org</b>.
+gettingStarted.hide = No mostrar aquest missatge de nou
+gettingStarted.hidealert = Per tal de tornar a mostrar aquest missatge, accedeixi a Configuració > General.
+
+# home.jsp
+home.random.title = Aleatori
+home.newest.title = El més nou
+home.highest.title = Els més valorats
+home.frequent.title = Escoltats freqüentment
+home.recent.title = Escoltats recentment
+home.users.title = Usuaris
+home.random.text = Àlbums aleatoris
+home.newest.text = Àlbums afegits o modificats recentment
+home.highest.text = Àlbums més ben valorats
+home.frequent.text = Àlbums freqüentment escoltats
+home.recent.text = Àlbums recentment escoltats
+home.users.text = Estadístiques d'usuari
+home.scan = El directori de música s''està escanejant ara mateix. Encara no estan disponibles totes les característiques.
+home.listsize = {0} àlbums per pàgina
+home.albums = Àlbums {0} - {1}
+home.playcount = Reproduïts {0} vegades
+home.lastplayed = Reproduïts {0}
+home.created = Modificats {0}
+home.chart.total = Total (MB)
+home.chart.stream = Streamed (MB)
+home.chart.download = Descarregats (MB)
+home.chart.upload = Pujats (MB)
+
+# more.jsp
+more.title = Més
+more.random.title = Llista de reproducció aleatòria
+more.random.text = Crear llista de reproducció aleatòria amb
+more.random.songs = {0} cançons
+more.random.auto = Reprodueixi més cançons de manera aleatòria quan s''arribi al final de la llista de reproducció.
+more.random.ok = OK
+more.random.genre = segons gènere
+more.random.anygenre = Qualsevol
+more.random.year = i any
+more.random.anyyear = Qualsevol
+more.random.folder = i directori
+more.random.anyfolder = Qualsevol
+more.apps.title = Subsonic Apps
+more.apps.text = <p>Hi ha aplicacions de <a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic</a> disponibles per a <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone&nbsp;7</b> i <b>AIR</b>.</p>
+more.mobile.title = Telèfon mòbil
+more.mobile.text = <p>Vostè pot controlar {0} amb qualsevol mòbil que tingui el WAP activat o amb una PDA.<br> \
+ Simplement ha d''accedir a la següent URL des del dispositiu: <b>http://yourhostname/wap</b></p> \
+ <p>Per tal que això funcioni, necessita que el seu servidor si pugui accedir dés de Internet.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Llistes de reproducció emmagatzemades com a Podcasts.<br>\
+ Usi la següent URL en el seu Podcast: <b>http://yourhostname/podcast</b>, \
+ o bé <b><a href="podcast.view?suffix=.rss">cliqui aquí</a>.</b></p>
+more.upload.title = Pujar arxiu
+more.upload.source = Seleccionar arxiu
+more.upload.target = Pujar a
+more.upload.browse = Escollir
+more.upload.ok = Pujar
+more.upload.unzip = Arxiu ZIP auto-descomprimible
+more.upload.progress = % completat. Si us plau, esperi...
+
+
+# upload.jsp
+upload.title = Pujant arxiu
+upload.success = Pujada completada <b>{0}</b>
+upload.empty = No hi ha arxius per a pujar.
+upload.failed = Ha fallat la pujada degut al següent error:<br><b>"{0}"</b>
+upload.unzipped = Descomprimit {0}
+
+# help.jsp
+help.title = Quant a {0}
+help.upgrade = <b>Avis!</b> Una nova versió està disponible.<br>Descarregar {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">aquí</a>.
+help.version.title = Versió
+help.builddate.title = Data de creació
+help.server.title = Server
+help.license.title = Llicència
+help.license.text = {0} es software lliure distribuït sota llicència de codi obert <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>. \
+ {0} usa <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">llibreries de tercers sota les respectives llicències</a>. Si us plau, noti que {0} <em>NO</em> \
+ és una eina per a la distribució il·legal de material amb copyright. Prengui atenció a les lleis específiques del seu país envers aquest punt.
+help.homepage.title = Pàgina web del projecte
+help.forum.title = Fòrum
+help.shop.title = Merchandise
+help.contact.title = Contacte
+help.contact.text = {0} està desenvolupat i mantingut per Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Si vostè té alguna pregunta, comentari o suggeriment, si us plau visiti \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} és gratuït, però pot contribuir al projecte realitzant un donatiu.
+help.log = Log
+help.logfile = El log complet esta guardat a {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Configuracions
+settingsheader.general = General
+settingsheader.advanced = Avançat
+settingsheader.personal = Aparença
+settingsheader.musicFolder = Directoris de música
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Oients
+settingsheader.share = Shared media
+settingsheader.network = Xarxa
+settingsheader.transcoding = Canviar format
+settingsheader.user = Usuaris
+settingsheader.search = Buscar
+settingsheader.coverArt = Caràtula
+settingsheader.password = Contrasenya
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Directori de la llista de reproducció
+generalsettings.musicmask = Arxius de música
+generalsettings.videomask = Arxius de vídeo
+generalsettings.coverartmask = Arxius de caràtula
+generalsettings.index = Índex
+generalsettings.ignoredarticles = Ignorar articles
+generalsettings.shortcuts = Accessos directes
+generalsettings.showgettingstarted = Mostrar "Primers passos" a l'inici
+generalsettings.welcometitle = Títol de benvinguda
+generalsettings.welcomesubtitle = Subtítol de benvinguda
+generalsettings.welcomemessage = Missatge de benvinguda
+generalsettings.loginmessage = Missatge d'inici de sessió
+generalsettings.language = Idioma predeterminat
+generalsettings.theme = Tema predeterminat
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Comanda de disminució de resolució
+advancedsettings.coverartlimit = Límit de la mida de la caràtula<br><div class="detail">(0 = Sense límit)</div>
+advancedsettings.downloadlimit = Límit de baixada (Kbps)<br><div class="detail">(0 = Sense límit)</div>
+advancedsettings.uploadlimit = Límit de pujada (Kbps)<br><div class="detail">(0 = Sense límit)</div>
+advancedsettings.streamport = Número del port SSL<br><div class="detail">(0 = Desactivat)</div>
+advancedsettings.ldapenabled = Activar autenticació LDAP
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = Filtre de cerca LDAP
+advancedsettings.ldapmanagerdn = Gestor LDAP DN<br><div class="detail">(Opcional)</div>
+advancedsettings.ldapmanagerpassword = Contrasenya
+advancedsettings.ldapautoshadowing = Crear usuaris de manera automàtica {0}
+
+# personalSettings.jsp
+personalsettings.title = Configuració d'aparença per a {0}
+personalsettings.language = Idioma
+personalsettings.theme = Tema
+personalsettings.display = Pantalla
+personalsettings.browse = Navegador
+personalsettings.playlist = Llista de reproducció
+personalsettings.tracknumber = Pista #
+personalsettings.artist = Artista
+personalsettings.album = Àlbum
+personalsettings.genre = Gènere
+personalsettings.year = Any
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Duració
+personalsettings.format = Format
+personalsettings.filesize = Mida de l'arxiu
+personalsettings.captioncutoff = Caràcters visibles
+personalsettings.partymode = Mode Festa
+personalsettings.shownowplaying = Mostrar el que altres escolten
+personalsettings.nowplayingallowed = Permetre als altres veure el que escolto
+personalsettings.showchat = Motrar els missatges del Xat
+personalsettings.finalversionnotification = Notifica'm sobre noves versions
+personalsettings.betaversionnotification = Notifica'm sobre noves versions beta
+personalsettings.lastfmenabled = Registrar el que estic reproduint a <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Nom d'usuari de Last.fm
+personalsettings.lastfmpassword = Contrasenya de Last.fm
+personalsettings.avatar.title = Imatge personal
+personalsettings.avatar.none = Sense imatge
+personalsettings.avatar.custom = Imatge personalitzada
+personalsettings.avatar.changecustom = Canviar la imatge personalitzada
+personalsettings.avatar.upload = Pujar
+personalsettings.avatar.courtesy = Icones cortesia de <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, i \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Canviar imatge personal
+avataruploadresult.success = Imatge personal carregada correctament "{0}".
+avataruploadresult.failure = S'ha produït un error al carregar la imatge. Vegi el <a href="help.view?">log</a> per a obtenir més detalls.
+
+# passwordSettings.jsp
+passwordsettings.title = Canviar contrasenya per {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Directori
+musicfoldersettings.name = Nom
+musicfoldersettings.enabled = Activat
+musicfoldersettings.add = Afegir directori multimèdia
+musicfoldersettings.nopath = Si us plau, especifiqui un directori.
+musicfoldersettings.notfound = Directori desconegut
+musicfoldersettings.scan = Escanejar directoris multimèdia
+musicfoldersettings.interval.never = Mai
+musicfoldersettings.interval.one = Cada dia
+musicfoldersettings.interval.many = Cada {0} dies
+musicfoldersettings.hour = a les {0}:00
+musicfoldersettings.nowscanning = S'està realitzant l'escaneig dels directoris multimèdia. Aquest procés tardarà uns quants minuts en funció de \
+ la mida de la vostre biblioteca multimèdia.
+musicfoldersettings.scannow = Escanejar ara els directoris multimèdia
+musicfoldersettings.fastcache = Mode d'accés ràpid
+musicfoldersettings.fastcache.description = Usi aquesta opció per tal de minimitzar l'accés a disc, per exemple si els arxius es troben a un disc de xarxa. \
+ Note: Els canvis d'aquests arxius només seran visibles després del procés d'escaneig. (veure més amunt).
+
+musicfoldersettings.organizebyfolderstructure = Organitzar segons l'estructura dels directoris
+musicfoldersettings.organizebyfolderstructure.description = Usi aquesta opció per tal de navegar per la seva biblioteca multimèdia usant l'estructura dels directoris enlloc dels ID3 tags artista/àlbum.
+
+# networkSettings.jsp
+networksettings.text = Usi els següents paràmetres per a controlar com s'accedeix al seu servidor Subsonic a través de Internet.<br> \
+ Si experimenta algun tipus de contratemps, visiti la guia de <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Primers passos</b></a>.
+networksettings.portforwardingenabled = Configuri el seu router de manera automàtica per tal de permetre les connexions entrants a Subsonic (usant reenviament de ports UPnP o NAT-PMP).
+networksettings.portforwardinghelp = Si el seu router no es pot configurar de manera automàtica pot intentar configurar-lo de manera manual. \
+ Pot intentar seguir les instruccions de <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Ha de reenviar el port {0} a l'ordinador on s'està executant el servidor de Subsonic.
+networksettings.urlredirectionenabled = Accedeixi al seu servidor a través de Internet usant una direcció fàcil de recordar.
+networksettings.status = Estat:
+networksettings.trialexpired = El període de prova va expirar el {0}. Si us plau, faci un <b><a href="donate.view?">donatiu</a></b> per tal de habilitar aquesta característica de manera permanent.
+networksettings.trialnotexpired = Aquesta característica estarà habilitada fins al {0}. Realitzi un <b><a href="donate.view?">donatiu</a></b> per usar-ho de manera permanent.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nom
+transcodingsettings.sourceformat = Convertir de
+transcodingsettings.targetformat = Convertir a
+transcodingsettings.step1 = Pas 1
+transcodingsettings.step2 = Pas 2
+transcodingsettings.step3 = Pas 3
+transcodingsettings.enabled = Activat
+transcodingsettings.noname = Si us plau, especifiqui un nom.
+transcodingsettings.nosourceformat = Si us plau, especifiqui un format des d'on convertir.
+transcodingsettings.notargetformat = Si us plau, especifiqui un format a on convertir.
+transcodingsettings.nostep1 = Si us plau, especifiqui com a mínim un pas per a canviar de format.
+transcodingsettings.info = <p class="detail">(%s = el format de l'arxiu que volem canviar, %b = Bitrate màxima del reproductor)</p> \
+ <p>El canvi de format d'un arxiu de so és el pas d'una codificació a una altra. El canvio de format de {1} \
+ permet fer streaming de so que normalment no es podria dur a terme. El canvio de format es fa en temps de reproducció i no \
+ necessita espai de disc extra.<p/> \
+ <p>El canvi de format es realitza mitjançant programes de línia de comandes de tercers els quals s'han de trobar instal·lats en {0}. \
+ Un paquet de windows pel canvi de format \
+ està disponible <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>aquí</b></a>. Vostè pot afegir el seu propi programa \
+ si compleix els següent requisits: \
+ <ul> \
+ <li>Ha de tenir una interfície de línia de comandes.</li> \
+ <li>Ha de ser capaç d'enviar la sortida a stdout.</li> \
+ <li>Si s'usa el pas 2 o 3 ha de ser capaç de llegir l'entrada de stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Cal remarcar que el canvio de codificació s'activa en el reproductor des de la pàgina de configuració.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL del stream
+internetradiosettings.homepageurl = Pàgina principal
+internetradiosettings.name = Nom
+internetradiosettings.enabled = Habilitat
+internetradiosettings.add = Afegir Internet TV/radio
+internetradiosettings.nourl = Especifiqui un URL.
+internetradiosettings.noname = Especifiqui un nom.
+
+# podcastSettings.jsp
+podcastsettings.update = Comprovar disponibilitat de nous episodis
+podcastsettings.keep = Mantenir
+podcastsettings.keep.all = Tots els episodis
+podcastsettings.keep.one = Episodi més recent
+podcastsettings.keep.many = Últim {0} episodi
+podcastsettings.download = Quan nous episodis estiguin disponibles
+podcastsettings.download.all = Descarregar-ho tot
+podcastsettings.download.one = Descarregar el més recent
+podcastsettings.download.many = Descarregar els últims {0} episodis
+podcastsettings.download.none = No facis res
+podcastsettings.interval.manually = Manualment
+podcastsettings.interval.hourly = Cada hora
+podcastsettings.interval.daily = Cada dia
+podcastsettings.interval.weekly = Cada setmana
+podcastsettings.folder = Guardar els Podcasts a
+
+# playerSettings.jsp
+playersettings.noplayers = No s'ha trobat cap oient.
+playersettings.type = Tipus
+playersettings.lastseen = Últim avís
+playersettings.title = Seleccioni un oient
+playersettings.technology.web.title = Reproductor Web
+playersettings.technology.external.title = Reproductor Extern
+playersettings.technology.external_with_playlist.title = Reproductor Extern amb llista de reproducció
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Reprodueixi música directament a un navegador web usant Flash player integrat.
+playersettings.technology.external.text = Reprodueixi música al seu reproductor preferit com ara WinAmp o Windows Media Player.
+playersettings.technology.external_with_playlist.text = El mateix que el cas anterior però la llista de reproducció es controla pel reproductor enlloc \
+ del servidor Subsonic. En aquest mode saltar-se cançons és possible.
+playersettings.technology.jukebox.text = Reprodueixi música directament a un aparell d''àudio de Subsonic. (Només usuaris autoritzats).
+playersettings.name = Nom de l'oient
+playersettings.coverartsize = Mida de la caràtula
+playersettings.maxbitrate = Bitrate màxim
+playersettings.coverart.off = Off
+playersettings.coverart.small = Petit
+playersettings.coverart.medium = Mitjà
+playersettings.coverart.large = Gran
+playersettings.nolame = <em>Notícia:</em> Sembla que LAME no està instal·lat.<br>Cliqui el botó d'ajuda per a més informació.
+playersettings.autocontrol = Controla el reproductor de manera automàtica
+playersettings.dynamicip = L'oient té IP dinàmica
+playersettings.transcodings = Canvi de format activat
+playersettings.ok = Guardar
+playersettings.forget = Esborrar oient
+playersettings.clone = Copiar oient
+
+# shareSettings.jsp
+sharesettings.name = Nom
+sharesettings.owner = Compartit per
+sharesettings.description = Descripció
+sharesettings.visits = Visites
+sharesettings.lastvisited = Última visita
+sharesettings.expires = Expira
+sharesettings.files = Arxius compartits
+sharesettings.expirein = Expira en
+sharesettings.expirein.week = 1w
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1y
+sharesettings.expirein.never = mai
+
+# userSettings.jsp
+usersettings.title = Seleccionar usuari
+usersettings.newuser = Nou usuari
+usersettings.admin = L'usuari és administrador
+usersettings.settings = L'usuari pot canviar paràmetres i la contrasenya
+usersettings.stream = L'usuari pot reproduir arxius
+usersettings.jukebox = L'usuari pot reproduir arxius en el mode jukebox
+usersettings.download = L'usuari pot descarregar arxius
+usersettings.upload = L'usuari pot pujar arxius al servidor
+usersettings.share = L'usuari pot compartir arxius amb qualsevol
+usersettings.playlist= L'usuari pot crear i esborrar llistes de reproducció
+usersettings.coverart = L'usuari pot canviar caràtules i els tags
+usersettings.comment= L'usuari pot crear i editar comentaris i qualificacions
+usersettings.podcast= L'usuari pot administrar Podcasts
+usersettings.username = Nom de l'usuari
+usersettings.email = Email
+usersettings.changepassword = Canviar contrasenya
+usersettings.password = Contrasenya
+usersettings.newpassword = Nova contrasenya
+usersettings.confirmpassword = Confirmar contrasenya
+usersettings.delete = Esborrar aquest usuari
+usersettings.ldap = Autenticació d'usuari a LDAP
+usersettings.nousername = No s'ha trobat el nom d'usuari.
+usersettings.noemail= Adreça d'email invàlida.
+usersettings.useralreadyexists = L'usuari ja existeix.
+usersettings.nopassword = Es necessària una contrasenya.
+usersettings.wrongpassword = Las contrasenyes no són coincidents.
+usersettings.ldapdisabled = Autenticació LDAP deshabilitada. Vegi configuració avançada.
+usersettings.passwordnotsupportedforldap = No es permet la creació o canvi de contrasenyes per a usuaris autenticats via LDAP.
+usersettings.ok = L'usuari {0} ha canviat la contrasenya correctament.
+
+# main.jsp
+main.up = Pujar
+main.playall = Reproduir-ho tot
+main.playrandom = Reproducció aleatòria
+main.addall = Afegir-ho tot
+main.tags = Editar tags
+main.playcount = Reproduït {0} cops.
+main.lastplayed = Última reproducció {0}.
+main.comment = Comentari
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>New paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Named link</td></tr>\
+ </table>
+main.sharealbum = Compartir
+main.more = Més accions...
+main.more.selection = Cançons seleccionades
+main.more.share = Compartir
+main.donate = <a href="{0}" style="text-decoration:underline">Donatiu</a> a {1}!<br>(i elimini aquest anunci)
+main.nowplaying = Actualment en reproducció
+main.lyrics = Lletres de cançons
+main.minutesago = minuts passats
+main.chat = Missatges de Xat
+main.scanning = Escanejant arxius:
+main.message = Escriure un missatge
+main.clearchat = Netejar missatges
+
+# rating.jsp
+rating.rating = Qualificació
+rating.clearrating = Netejar qualificació
+
+# coverArt.jsp
+coverart.change = Canviar
+coverart.zoom = Ampliar
+
+# allmusic.jsp
+allmusic.text = Buscant l''àlbum <em>{0}</em> a allmusic.com - Si us plau esperi.
+
+# changeCoverArt.jsp
+changecoverart.title = Canviar caràtula
+changecoverart.address = Introdueixi la direcció de la imatge de la caràtula
+changecoverart.artist = Artista
+changecoverart.album = Àlbum
+changecoverart.search = Cercador d'Imatges de Google
+changecoverart.wait = Esperi, si us plau...
+changecoverart.success = La imatge ha estat descarregada correctament.
+changecoverart.error = Error descarregant la imatge.
+changecoverart.noimagesfound = No s'han trobat imatges.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Ha fallat al canviar la caràtula:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Editar tags
+edittags.file = Arxiu
+edittags.track = Pista
+edittags.songtitle = Títol
+edittags.artist = Artista
+edittags.album = Àlbum
+edittags.year = Any
+edittags.genre = Genere
+edittags.status = Estat
+edittags.suggest = Suggerir
+edittags.reset = Reset
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Establir
+edittags.working = Treballant
+edittags.updated = Actualitzat
+edittags.skipped = Omès
+edittags.error = Error
+
+# share.jsp
+share.title = Share
+share.warning = <h2>IMPORTANT NOTICE!</h2><p>Play fair &ndash; Don't share copyrighted material in any manner that violates the law.</p>
+share.facebook = Share on Facebook
+share.twitter = Share on Twitter
+share.googleplus = Share on Google+
+share.link = Or share this with someone by sending them this link: <a href="{0}" target="_blank">{0}</a>
+share.disabled = To share your music with someone you must first register your own <em>subsonic.org</em> address.<br> \
+ Please go to <a href="networkSettings.view"><b>Settings &gt; Network</b></a> (administrative rights required).
+share.manage = Manage my shared media
+
+# donate.jsp
+donate.title = Donatiu
+donate.invalidlicense = Llicència invàlida.
+donate.amount = Donatiu de {0}
+
+donate.textbefore = <p>Moltes gràcies per considerar un donatiu per ajudar al projecte de {0}! \
+ Els donants aconseguiran accés a les característiques premium com ara:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> per a Android, iPhone i Windows Phone&nbsp;7*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> per a PlayBook, Roku, Mac, Chrome entre altres*.</li> \
+ <li>Streaming de Video.</li> \
+ <li>Una adreça pel teu servidor personal: <em>yourname</em>.subsonic.org (visiti <a href="networkSettings.view">Settings &gt; Network</a>).</li> \
+ <li>Compartir els seus arxius multimedia a Facebook, Twitter, Google+.</li> \
+ <li>Treure els anuncis de la interfície web.</li> \
+ <li>Futures funcionalitats.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Algunes aplicacions són venudes i desenvolupades per tercers.</p>\
+ <p>Com a donant, rebrà una llicència que només serà vàlida per a us personal, no comercial per a aquesta \
+ i per totes les pròximes versions de {0}. Per a us comercial, <a href="mailto:subsonic_donation@activeobjects.no">contacti</a> amb nosaltres per altres tipus de llicència.</p> \
+ <p>El donatiu recomanat és de <b>&euro;20</b>, però pot seleccionar la quantitat que més li agradi:</p>
+donate.textafter = <p>Cliqui a un dels icones per anar a PayPal a on podrà pagar mitjançant una targeta de crèdit o mitjançant \
+ el seu compte personal de PayPal (si en posseeix algun). Rebrà la clau de la llicència per email en pocs minuts.</p> \
+ <p>Si té qualsevol pregunta, pot posar-se en contacte per email a \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Aquesta còpia de {2} ha estat registrada a {0} al {1}. Moltes gràcies pel seu suport!
+donate.register = Després de rebre la clau de la llicència, registri-la aquí a sota.
+donate.resend = Ha perdut la seva clau de llicència? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Reenviar-la de nou</a>.
+donate.register.email = Email
+donate.register.license = License
+
+# podcastReceiver.jsp
+podcastreceiver.title = Receptor de Podcast
+podcastreceiver.expandall = Mostrar capítols
+podcastreceiver.collapseall = Amagar capítols
+podcastreceiver.status.new = Nou
+podcastreceiver.status.downloading = Descarregar
+podcastreceiver.status.completed = Complet
+podcastreceiver.status.error = Error
+podcastreceiver.status.deleted = Esborrat
+podcastreceiver.status.skipped = Omès
+podcastreceiver.downloadselected= Descarregar seleccionada
+podcastreceiver.deleteselected= Borrat seleccionat
+podcastreceiver.confirmdelete= Està segur de esborrar els Podcasts seleccionats?
+podcastreceiver.check = Comprovar nous capítols
+podcastreceiver.refresh = Actualitzar la pàgina
+podcastreceiver.settings = Configuració dels Podcast
+podcastreceiver.subscribe = Subscriure's a un Podcast
+
+# lyrics.jsp
+lyrics.title = Lletres de cançons
+lyrics.artist = Artista
+lyrics.song = Cançó
+lyrics.search = Cerca
+lyrics.wait = Buscant lletres de cançons, esperi un moment...
+lyrics.courtesy = (Lyrics by <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = No s'han trobat lletres de cançons.
+
+# helpPopup.jsp
+helppopup.title = Ajuda de {0}
+helppopup.cover.title = Mida de la caràtula
+helppopup.cover.text = <p>Permet especificar la mida de la caràtula visualitzada amb l'opció d'ocultar-la completament.</p>
+helppopup.transcode.title = Bitrate màxim
+helppopup.transcode.text = <p>Si ho desitja, pot limitar el bitrate del stream de música. \
+ Per exemple, si el seu mp3 original està codificat a 256 Kbps(kbits per segon), configurar el bitrate màxim \
+ a 128 farà que {0} es codifiqui la música de 256 a 128 Kbps.</p> \
+ <p>Aquesta opció necessita que LAME es trobi instal·lat. LAME<a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ és un codificador de MP3 de codi obert. Pot descarregar-lo <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">aquí</a>. \
+ Asseguris que s'instal·la a SUBSONIC_HOME/transcode, o en un directori que estigui present a la seva variable d'entorn PATH.</p>
+helppopup.playlistfolder.title = Directori de la llista de reproducció
+helppopup.playlistfolder.text = <p>Permet especificar el directori a on es troben les llistes de reproducció.</p>
+helppopup.musicmask.title = Arxius de música
+helppopup.musicmask.text = <p>Permet especificar els tipus d''arxius que s'haurien de reconèixer com a música quan naveguem pels directoris de música.</p>
+helppopup.videomask.title = Arxius de vídeo
+helppopup.videomask.text = <p>Permet especificar el tipus d''arxius que seran reconeguts com a vídeo.</p>
+helppopup.coverartmask.title = Arxius de caràtula
+helppopup.coverartmask.text = <p>Permet especificar els tipus d''arxius que s'haurien de reconèixer com a caràtules quan naveguem pels directoris de música.</p>
+helppopup.downsamplecommand.title = Comanda de Downsample
+helppopup.downsamplecommand.text = <p>Permet especificar la comanda a executar quan s''hagi de fer un downsampling a bitrates més baixes.</p>\
+ <p>(%s = L'arxiu a ser downsampled, %b = Bitrate Max del reproductor, %t = Títol, %a = Artista, %l = Àlbum)</p>
+helppopup.index.title = Índex
+helppopup.index.text = <p>Et permet especificar com hauria ser l''índex que es troba a la part superior de la pantalla. D'aquesta manera \
+ els arxius i directoris que es troben als directoris de música configurats es poden accedir amb més facilitat.</p> \
+ <p>La manera d''especificar els índexs és mitjançant una llista separada por espais en blanc. Normalment aquests índex són d'un caràcter \
+ però podem agrupar els que vulguem. Per exemple, a traves de l'índex <em>el</em> podrem accedir als arxius i directoris \
+ que comencin per "el".</p> \
+ <p>Vostè també pot crear un índex que sigui un grup de caràcters d'índexs. Per a fer-ho, els caràcters han d'estar entre parèntesis. \
+ Per exemple l'índex <em>A-E(ABCDE)</em> es mostrarà com a <em>A-E</em> i enllaçarà amb tots els arxius i directoris que comencin \
+ per A, B, C, D o E. Això pot ser útil per tal d'agrupar els caràcters que es fan servir menys (com ara X, Y y Z), o \
+ per tal d'agrupar els caràcters accentuats (com ara A, \u00c0 i \u00c1)</p> \
+ <p>Els Arxius i directoris que no es trobin sota cap índex s'assignaran automàticament al índex "#".</p>
+helppopup.ignoredarticles.title = Ignorar articles
+helppopup.ignoredarticles.text = <p>Et permet especificar una llista d''articles(com "The","el","la") que seran ignorats en el moment de crear l'índex.</p>
+helppopup.shortcuts.title = Accessos directes
+helppopup.shortcuts.text = <p>Una llista separada per espais dels directoris els quals es volen crear accessos directes. Faci servir cometes per a agrupar paraules com per exemple:</p> \
+ <p><em>Nou Incoming "Música electrònica"</em></p>
+helppopup.language.title = Idioma
+helppopup.language.text = <p>Et permet especificar l''idioma que es vol fer servir.</p>
+helppopup.visibility.title = Visibilitat
+helppopup.visibility.text = <p>Seleccioni els detalls que vol que es mostrin amb la cançó i quants caràcters es poden visualitzar. Aquest és el màxim \
+ de caràcters que es podran visualitzar al títol d'una cançó, d'un àlbum o al nom d'un artista.</p>
+helppopup.partymode.title = Mode Festa
+helppopup.partymode.text = <p>Quan el mode festa s'activa, la interfície d'usuari es simplifica per ser usat per usuaris sense experiència. \
+ Més concretament, s'activa un sistema per evitar l'eliminació per error de llistes de reproducció.</p>
+helppopup.theme.title = Tema
+helppopup.theme.text = <p>Et permet seleccionar el tema que es vol fer servir. Un tema defineix l'aparença(colors, fonts, imatges...) de {0}.</p>
+helppopup.welcomemessage.title = Missatge de benvinguda
+helppopup.welcomemessage.text = <p>El missatge que es mostra a l''inici de la pàgina.</p>
+helppopup.loginmessage.title = Missatges de Login
+helppopup.loginmessage.text = <p>El missatge que es mostra a la pàgina de login.</p>
+helppopup.coverartlimit.title = Límit de caràtules
+helppopup.coverartlimit.text = <p>Número màxim de caràtules que es mostren a una pàgina.</p>
+helppopup.downloadlimit.title = Límit de descarrega
+helppopup.downloadlimit.text = <p>Especifica l'ample de banda que es farà servir per a descarregar els arxius.</p>
+helppopup.uploadlimit.title = Límit de pujada
+helppopup.uploadlimit.text = <p>Especifica l'ample de banda que es farà servir per a pujar els arxius.</p>
+helppopup.streamport.title = Número de port SSL
+helppopup.streamport.text = <p>Aquesta opció només és rellevant si s'usa {0} a un servidor amb SSL (HTTPS).</p><p>Alguns reproductors \
+ (com ara Winamp) no suporten streaming sobre SSL per tant haurem d'especificar un port alternatiu http(normalment 80 \
+ o 4040) per a aquests. Els streams no s'encriptaran.</p>
+helppopup.ldap.title = Autenticació LDAP
+helppopup.ldap.text = <p>L''Usurari pot ser autenticat mitjançant un servidor LDAP extern (Incloent Windows Active Directory). \
+ Quan l'autenticació LDAP està activada, el nom d'usuari i la contrasenya són revisades pel servidor extern i no per part de {0}.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>URL del servidor LDAP. El protocol ha de ser tant <em>ldap://</em> com <em>ldaps://</em> \
+ (per LDAP over SSL). Per a més informació accedir al següent <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">link</a>.</p>
+
+helppopup.ldapsearchfilter.title = Filtre de cerca LDAP
+helppopup.ldapsearchfilter.text = <p>Expressió usada pel filtre de cerca. Aquest és un filtre de cerca LDAP \
+ (definit com al <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ El comportament "'{0'}" és substituït pel nom d''usuari, per exemple: \
+ <ul>\
+ <li>(uid='{0'}) - aquest cercarà el nom d''usuari a l''atribut uid.</li> \
+ <li>(sAMAccountName='{0'}) - típicament usat per la autenticació a Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>Si el servidor LDAP no suporta connexió anònima, s'haurà d''especificar el DN \
+ (<em>Distinguished Nom</em>) i la contrasenya per l'usuari LDAP que s'usarà.</p>
+helppopup.ldapautoshadowing.title = Crear de manera automàtica usuaris LDAP a {0}
+helppopup.ldapautoshadowing.text = <p>Amb aquesta opció seleccionada, els usuaris LDAP no cal que hagin hagut de crear-se de manera manual a {0} abans d''autenticar-se.</p> \
+ <p>NOTE! Això implica que qualsevol usuari amb un nom d'usuari i una contrasenya LDAP vàlids pot autenticar-se de manera \
+ correcta a {0}.</p>
+helppopup.playername.title = Nom de l'oient
+helppopup.playername.text = <p>Permet especificar un nom per l''oient(usuari@ip).</p>
+helppopup.autocontrol.title = Controla el reproductor de manera automàtica
+helppopup.autocontrol.text = <p>Si selecciona aquesta opció {0} executarà el reproductor en el moment que vostè cliqui a "Reproduir"\
+ a la llista de reproducció. Si no, haurà d'executar i connectar el reproductor.</p>
+helppopup.dynamicip.title = IP dinàmica
+helppopup.dynamicip.text = <p>Deshabiliti aquesta opció si l''oient usa una IP estàtica.</p>
+
+# wap/index.jsp
+wap.index.missing = No s'ha trobat música
+wap.index.playlist = Llista de reproducció
+wap.index.search = Buscar
+wap.index.settings = Configuració
+
+# wap/browse.jsp
+wap.browse.playone = Reprodueixi la cançó
+wap.browse.playall = Reprodueixi-ho tot
+wap.browse.addone = Afegeixi la cançó
+wap.browse.addall = Afegeixi-ho tot
+wap.browse.downloadone = Descarregar cançó
+wap.browse.downloadall = Descarregar-ho tot
+
+# wap/playlist.jsp
+wap.playlist.title = Llista de reproducció
+wap.playlist.noplayer = Cap reproductor connectat
+wap.playlist.clear = Netejar
+wap.playlist.load = Carregar
+wap.playlist.random = Random
+wap.playlist.play = Reproduir al dispositiu mòbil
+
+# wap/search.jsp
+wap.search.title = Buscar
+
+# wap/searchResult.jsp
+wap.searchresult.index = L'índex de cerca s'està creant en aquests moments. Si us plau, intenti-ho de nou més tard.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Seleccioni un reproductor
+wap.settings.allplayers = Tot
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties
new file mode 100644
index 00000000..38fc081d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties
@@ -0,0 +1,749 @@
+
+#
+# Czech localization.
+# Author: Robert Ilyk (rilyk@volny.cz)
+# Last update: 23-05-2012
+#
+
+common.home = Dom\u016f
+common.back = Zp\u011bt
+common.help = Pomoc
+common.play = P\u0159ehr\u00e1t
+common.add = P\u0159idat
+common.download = St\u00e1hnout
+common.close = Zav\u0159\u00edt
+common.refresh = Obnovit
+common.next = Dal\u0161\u00ed
+common.previous = P\u0159edchoz\u00ed
+common.more = V\u00edce
+common.ok = OK
+common.cancel = Zru\u0161it
+common.save = Ulo\u017eit
+common.create = Vytvo\u0159it
+common.delete = Smazat
+common.unknown = (Nezn\u00e1m\u00fd)
+common.default = (V\u00fdchoz\u00ed)
+
+# login.jsp
+login.username = Jm\u00e9no
+login.password = Heslo
+login.login = P\u0159ihl\u00e1sit
+login.remember = Pamatuj si m\u011b
+login.logout = Byli jste odhl\u00e1\u0161eni.
+login.error = \u0160patn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no nebo heslo.
+login.insecure = \u00da\u010det {0} nen\u00ed zabazpe\u010den\u00fd. Pros\u00edm p\u0159ihlaste se jako u\u017eivatel "admin" <br> s heslem "admin", nebo klikn\u011bte <a href="login.view?user=admin&amp;password=admin">zde</a> pro okam\u017eitou zm\u011bnu hesla.
+
+# accessDenied.jsp
+accessDenied.title = P\u0159\u00edstup byl odep\u0159en
+accessDenied.text = Nem\u00e1te opr\u00e1vn\u011bn\u00ed k proveden\u00ed po\u017eadovan\u00e9 operace.
+
+# top.jsp
+top.home = Dom\u016f
+top.now_playing = P\u0159ehr\u00e1v\u00e1n\u00ed
+top.settings = Nastaven\u00ed
+top.status = Status
+top.podcast = Podcasty
+top.more = V\u00edce
+top.help = Pomoc
+top.search = Hledat
+top.upgrade = <b>Pozor!</b> Je k dispozici nov\u00e1 verze aplikace.<br>St\u00e1hnout {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">zde</a>.
+top.missing = Nebyla nalezena slo\u017eka s hudbou. Pros\u00edm zm\u011b\u0148te nastaven\u00ed slo\u017eky s hudbou.
+top.logout = Odhl\u00e1sit u\u017eivatele {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;autor\u016f<br>\
+ {1}&nbsp;alb<br>\
+ {2}&nbsp;skladeb<br>\
+ {3} (&#126; {4} hodin)
+left.shortcut = Z\u00e1stupce
+left.radio = Internetov\u00e1 TV/r\u00e1dia
+left.allfolders = V\u0161echny slo\u017eky
+
+# playlist.jsp
+playlist.stop = Zastavit
+playlist.start = P\u0159ehr\u00e1t
+playlist.confirmclear = Opravdu chcete vy\u010distit seznam?
+playlist.clear = Vy\u010distit
+playlist.shuffle = N\u00e1hodn\u011b
+playlist.repeat_on = Opakov\u00e1n\u00ed zapnuto
+playlist.repeat_off = Opakov\u00e1n\u00ed vypnuto
+playlist.undo = Zp\u011bt
+playlist.settings = Nastaven\u00ed
+playlist.more = V\u00edce...
+playlist.more.playlist = Seznam
+playlist.more.sortbytrack = Se\u0159adit podle stopy
+playlist.more.sortbyartist = Se\u0159adit podle autora
+playlist.more.sortbyalbum = Se\u0159adit podle alba
+playlist.more.selection = Vybran\u00e9 skladby
+playlist.more.selectall = Vybrat v\u0161e
+playlist.more.selectnone = Zru\u0161it v\u0161e
+playlist.getflash = Z\u00edskat Flash player
+playlist.load = Nahr\u00e1t
+playlist.save = Ulo\u017eit
+playlist.append = P\u0159idat do seznamu
+playlist.remove = Odebrat
+playlist.up = Nahoru
+playlist.down = Dol\u016f
+playlist.empty = Seznam je pr\u00e1zdn\u00fd
+
+# videoPlayer.jsp
+videoPlayer.getflash = Pros\u00edm naistalujte si p\u0159ehrava\u010d Flash Player
+videoPlayer.popout = Otev\u0159\u00edt v nov\u00e9m okn\u011b
+
+# loadPlaylist.jsp
+playlist.load.title = Nahr\u00e1t seznam
+playlist.load.appendtitle = P\u0159idat do seznamu
+playlist.load.load = Nahr\u00e1t
+playlist.load.append = P\u0159idat
+playlist.load.delete = Odstranit
+playlist.load.confirm_delete = Opravdu chcete odstranit seznam?
+playlist.load.missing_folder = Seznam "{0}" neexistuje. Pros\u00edm upravte nastaven\u00ed.
+playlist.load.empty = \u017d\u00e1dn\u00fd seznam nen\u00ed k dispozici.
+
+# savePlaylist.jsp
+playlist.save.title = Ulo\u017eit seznam
+playlist.save.save = Ulo\u017eit
+playlist.save.name = N\u00e1zev seznamu
+playlist.save.format = Form\u00e1t
+playlist.save.missing_folder = Seznam "{0}" neexistuje. Pros\u00edm upravte nastaven\u00ed.
+playlist.save.noname = Pros\u00edm zadejte n\u00e1zev seznamu.
+
+# status.jsp
+status.title = Stav
+status.type = Typ
+status.stream = Vys\u00edl\u00e1n\u00ed
+status.download = St\u00e1hnout
+status.upload = Odeslat
+status.player = P\u0159ehrava\u010d
+status.user = U\u017eivatel
+status.current = Aktu\u00e1ln\u00ed soubor
+status.transmitted = P\u0159eneseno
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Hledat
+search.query = Autor, album nebo n\u00e1zev skladby
+search.search = Hledat
+search.index = Pr\u00e1v\u011b je vytv\u00e1\u0159en index pro vyhled\u00e1v\u00e1n\u00ed. Zkuste to pros\u00edm pozdeji.
+search.hits.none = Nenalezeno
+search.hits.more = V\u00edce
+search.hits.artists = Auto\u0159i
+search.hits.albums = Alba
+search.hits.songs = Skladby
+
+# gettingStarted.jsp
+gettingStarted.title = Za\u010d\u00edn\u00e1me
+gettingStarted.text = <p>V\u00edtejte v Subsonic! Nastaven\u00ed bude provedeno b\u011bhem p\u00e1r okam\u017eik\u016f, postupujte podle n\u00ed\u017ee uveden\u00fdch jednoduch\u00fdch krok\u016f.<br> \
+ Pro n\u00e1vrat na tuto obrazovku klikn\u011bte na tla\u010d\u00edtko "Dom\u016f" v n\u00e1strojov\u00e9 li\u0161t\u011b naho\u0159e.</p> \
+ <p>Pro v\u00edce informac\u00ed si pros\u00edm p\u0159e\u010dt\u011bte dokument <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Za\u010d\u00edn\u00e1me</b></a> (v anglick\u00e9m jazyce).</p>
+gettingStarted.step1.title = Zm\u011b\u0148te administr\u00e1torsk\u00e9 heslo.
+gettingStarted.step1.text = Zabezpe\u010dte si sv\u016fj server tim, \u017ee si zm\u011bn\u00edte v\u00fdchoz\u00ed heslo pro administr\u00e1torsk\u00fd \u00fa\u010det. \
+ M\u016f\u017eete tak\u00e9 vytvo\u0159it nov\u00e9 u\u017eivatele s omezen\u00fdmi pr\u00e1vy.
+gettingStarted.step2.title = Nastavte slo\u017eky s multimedi\u00e1ln\u00edmi soubory.
+gettingStarted.step2.text = Ur\u010dete slo\u017eky, ve kter\u00fdch m\u00e1te ulo\u017eeny soubory s hudbou nebo videem.
+gettingStarted.step3.title = Nastavte s\u00ed\u0165.
+gettingStarted.step3.text = N\u011bkter\u00e1 pot\u0159ebn\u00e1 nastaven\u00ed, pokud se rozhodnete p\u0159ehr\u00e1vat sv\u00e1 m\u00e9dia p\u0159es Internet, \
+ nebo je sd\u00edlet s rodinou \u010di p\u0159\u00e1teli. Z\u00edskejte svou soukromou adresu <b><em>mujserver</em>.subsonic.org</b> \
+ pro snadn\u011bj\u0161\u00ed sd\u00edlen\u00ed.
+gettingStarted.hide = Tento \u00favod p\u0159\u00ed\u0161t\u011b nezobrazovat
+gettingStarted.hidealert = Pro zobrazen\u00ed tohoto \u00favodu jd\u011bte do Nastaven\u00ed > Hlavn\u00ed.
+
+# home.jsp
+home.random.title = N\u00e1hodn\u011b
+home.newest.title = Nejnov\u011bj\u0161\u00ed
+home.highest.title = Nejl\u00e9pe hodnocen\u00e9
+home.frequent.title = Nej\u010dast\u011bj\u0161\u00ed
+home.recent.title = Naposledy pou\u017eit\u00e9
+home.users.title = U\u017eivatel\u00e9
+home.random.text = N\u00e1hodn\u00e1 alba
+home.newest.text = Naposledy p\u0159idan\u00e1 nebo upraven\u00e1 alba
+home.highest.text = Nejl\u00e9pe hodnocen\u00e1 alba
+home.frequent.text = Nejv\u00edce p\u0159ehr\u00e1van\u00e1 alba
+home.recent.text = Naposledy prehr\u00e1van\u00e1 alba
+home.users.text = U\u017eivatelsk\u00e1 statistika
+home.scan = Slo\u017eka s hudbou je moment\u00e1ln\u011b prohled\u00e1van\u00e1. V\u0161echny funkce je\u0161t\u011b nejsou k dispozici.
+home.listsize = {0} alb na stranu
+home.albums = Alba {0} - {1}
+home.playcount = P\u0159ehr\u00e1no {0} skladeb
+home.lastplayed = P\u0159ehr\u00e1no {0}
+home.created = Upraveno {0}
+home.chart.total = Souhrn (MB)
+home.chart.stream = Odvys\u00edl\u00e1no (MB)
+home.chart.download = Sta\u017eeno (MB)
+home.chart.upload = Odesl\u00e1no (MB)
+
+# more.jsp
+more.title = V\u00edce
+more.random.title = N\u00e1hodn\u00fd seznam
+more.random.text = Vytvo\u0159it n\u00e1hodn\u00fd seznam z
+more.random.songs = {0} skladeb
+more.random.auto = Pokra\u010duj v p\u0159ehr\u00e1v\u00e1n\u00ed n\u00e1hodn\u00fdch skladeb po dosa\u017een\u00ed konce seznamu.
+more.random.ok = OK
+more.random.genre = \u017e\u00e1nr
+more.random.anygenre = Jak\u00fdkoliv
+more.random.year = a rok
+more.random.anyyear = Jak\u00fdkoliv
+more.random.folder = ve slo\u017ece
+more.random.anyfolder = Jak\u00e9koliv
+more.apps.title = Aplikace Subsonic
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Aplikace Subsonic</a> jsou dostupn\u00e9 pro <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone&nbsp;7</b> a taky <b>AIR</b>.</p>
+more.mobile.title = Mobiln\u00ed telefon
+more.mobile.text = <p>{0} m\u016f\u017eete ovl\u00e1dat prost\u0159ednictv\u00edm libovoln\u00e9ho telefonu nebo PDA vybaven\u00e9ho technologi\u00ed WAP.<br> \
+ Nav\u0161tivte n\u00e1sledduj\u00edc\u00ed URL adresu ze sv\u00e9ho za\u0159\u00edzen\u00ed: <b>http://yourhostname/wap</b></p> \
+ <p>Pro tuto mo\u017enost je d\u016fle\u017eit\u00e9, aby server byl dostupn\u00fd z Interneru.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Ulo\u017een\u00e9 seznamy jsou dostupn\u00e9 jako Podscasty<br>\
+ Pou\u017eijte n\u00e1sleduj\u00edc\u00ed URL adresu pro p\u0159id\u00e1n\u00ed Podcastu do va\u0161eho za\u0159\u00edzen\u00ed: <b>http://yourhostname/podcast</b>, \
+ nebo <b><a href="podcast.view?suffix=.rss">klikn\u011bte zde</a>.</b></p>
+more.upload.title = Odeslat soubor
+more.upload.source = Vybrat soubor
+more.upload.target = Odeslat do
+more.upload.browse = Vybrat
+more.upload.ok = Odeslat
+more.upload.unzip = Automaticky rozbalit soubory ZIP.
+more.upload.progress = % dokon\u010deno. Pros\u00edm \u010dekejte...
+
+# upload.jsp
+upload.title = Pos\u00edl\u00e1m soubor na server
+upload.success = Soubor <b>{0}</b> byl \u00fasp\u011b\u0161n\u011b odesl\u00e1n
+upload.empty = \u017d\u00e1dn\u00fd soubor k odesl\u00e1n\u00ed.
+upload.failed = Odes\u00edl\u00e1n\u00ed souboru se nezda\u0159ilo z n\u00e1sleduj\u00edc\u00edho d\u016fvodu:<br><b>"{0}"</b>
+upload.unzipped = Rozbaleno {0}
+
+# help.jsp
+help.title = O aplikaci {0}
+help.upgrade = <b>Pozor!</b> Je k dispozici nov\u00e1 verze aplikace. St\u00e1hnout {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">zde</a>.
+help.version.title = Verze
+help.builddate.title = Datum vyd\u00e1n\u00ed
+help.server.title = Server
+help.license.title = Podm\u00ednky pou\u017eit\u00ed
+help.license.text = {0} je software distribuovan\u00fd pod <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source licenc\u00ed. \
+ {0} pou\u017e\u00edv\u00e1 <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licencovan\u00e9 knihovny t\u0159et\u00edch stran</a>.
+help.homepage.title = Domovsk\u00e1 str\u00e1nka
+help.forum.title = F\u00f3rum
+help.shop.title = Obchod
+help.contact.title = Kontakt
+help.contact.text = {0} je vyv\u00edjen a udr\u017eov\u00e1n Sindrem Mehusem \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Jestli\u017ee m\u00e1te jak\u00fdkoliv dotaz, p\u0159ipom\u00ednku nebo n\u00e1m\u011bt na zlep\u0161en\u00ed, pros\u00edm nav\u0161tivte \
+ <a href="http://forum.subsonic.org" target="_blank">f\u00f3rum Subsonic</a>.
+help.donate = {0} je zdarma, ale m\u016f\u017eete jej podpo\u0159it sv\u00fdm <b><a href="donate.view?">d\u00e1rkem</a></b>.
+help.log = Log
+help.logfile = Kompletn\u00ed log je ulo\u017een v {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Nastaven\u00ed
+settingsheader.general = Hlavn\u00ed
+settingsheader.advanced = Roz\u0161\u00ed\u0159en\u00e9
+settingsheader.personal = Osobn\u00ed
+settingsheader.musicFolder = Slo\u017eky s hudbou
+settingsheader.internetRadio = Internetov\u00e1 TV/r\u00e1dia
+settingsheader.podcast = Podcasty
+settingsheader.player = P\u0159ehrava\u010de
+settingsheader.share = Sd\u00edlen\u00e1 media
+settingsheader.network = S\u00ed\u0165
+settingsheader.transcoding = P\u0159ek\u00f3dov\u00e1n\u00ed
+settingsheader.user = U\u017eivatel\u00e9
+settingsheader.search = Vyhled\u00e1vac\u00ed index
+settingsheader.coverArt = Obr\u00e1zky alb
+settingsheader.password = Heslo
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Slo\u017eka seznamu
+generalsettings.musicmask = Typy hudebn\u00edch soubor\u016f
+generalsettings.videomask = Typy video soubor\u016f
+generalsettings.coverartmask = Typy obr\u00e1zk\u016f alb
+generalsettings.index = Index
+generalsettings.ignoredarticles = \u010c\u00e1sti n\u00e1zv\u016f pro vynech\u00e1n\u00ed
+generalsettings.shortcuts = Z\u00e1stupci
+generalsettings.showgettingstarted = Zobrazit po staru obrazovku "Za\u010d\u00edn\u00e1me"
+generalsettings.welcometitle = Titulek uv\u00edt\u00e1n\u00ed
+generalsettings.welcomesubtitle = Podtitulek uv\u00edt\u00e1n\u00ed
+generalsettings.welcomemessage = Uv\u00edtac\u00ed zpr\u00e1va
+generalsettings.loginmessage = P\u0159ihla\u0161ovac\u00ed zpr\u00e1va
+generalsettings.language = V\u00fdchoz\u00ed jazyk
+generalsettings.theme = V\u00fdchoz\u00ed motiv
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = P\u0159\u00edkaz pro downsampling
+advancedsettings.coverartlimit = Omezen\u00ed obr\u00e1zk\u016f alb<br><div class="detail">(0 = Neomezeno)</div>
+advancedsettings.downloadlimit = Omezen\u00ed pro stahov\u00e1n\u00ed (Kbps)<br><div class="detail">(0 = Neomezeno)</div>
+advancedsettings.uploadlimit = Omezen\u00ed pro odes\u00edl\u00e1n\u00ed (Kbps)<br><div class="detail">(0 = Neomezeno)</div>
+advancedsettings.streamport = Port pro vys\u00edl\u00e1n\u00ed (ne SSL)<br><div class="detail">(0 = Vypnuto)</div>
+advancedsettings.ldapenabled = Zapnout LDAP ov\u011b\u0159ov\u00e1n\u00ed
+advancedsettings.ldapurl = URL adresa LDAP
+advancedsettings.ldapsearchfilter = Filtr vyhled\u00e1v\u00e1n\u00ed LDAP
+advancedsettings.ldapmanagerdn = Spr\u00e1vce DN LDAP <br><div class="detail">(Voliteln\u00e9)</div>
+advancedsettings.ldapmanagerpassword = Heslo
+advancedsettings.ldapautoshadowing = Automaticky vytvo\u0159it u\u017eivatele v {0}
+
+# personalSettings.jsp
+personalsettings.title = Osobn\u00ed nastaven\u00ed u\u017eivatele {0}
+personalsettings.language = Jazyk
+personalsettings.theme = Motiv
+personalsettings.display = Zobrazen\u00ed
+personalsettings.browse = Proch\u00e1zet
+personalsettings.playlist = Seznam
+personalsettings.tracknumber = \u010c\u00edslo skladby
+personalsettings.artist = Autor
+personalsettings.album = Album
+personalsettings.genre = \u017d\u00e1nr
+personalsettings.year = Rok
+personalsettings.bitrate = Bit rate
+personalsettings.duration = D\u00e9lka
+personalsettings.format = Form\u00e1t
+personalsettings.filesize = Velikost souboru
+personalsettings.captioncutoff = Zkr\u00e1cen\u00ed popisu
+personalsettings.partymode = Zjednodu\u0161en\u00e9 rozhran\u00ed
+personalsettings.shownowplaying = Uka\u017e mi, co p\u0159ehr\u00e1vaj\u00ed ostatn\u00ed
+personalsettings.nowplayingallowed = Uka\u017e ostatn\u00edm, co p\u0159ehr\u00e1v\u00e1m j\u00e1
+personalsettings.showchat = Zobraz kr\u00e1tk\u00e9 zpr\u00e1vy (chat)
+personalsettings.finalversionnotification = Upozorni m\u011b na nov\u00e9 verze programu
+personalsettings.betaversionnotification = Upozorni m\u011b na nov\u00e9 beta-verze programu
+personalsettings.lastfmenabled = Informuj slu\u017ebu <a href="http://last.fm/" target="_blank">Last.fm</a> o tom, co pr\u00e1v\u011b p\u0159ehr\u00e1v\u00e1m
+personalsettings.lastfmusername = U\u017eivatel Last.fm
+personalsettings.lastfmpassword = Heslo Last.fm
+personalsettings.avatar.title = Osobn\u00ed obr\u00e1zek
+personalsettings.avatar.none = Bez obr\u00e1zku
+personalsettings.avatar.custom = Vlastn\u00ed obr\u00e1zek
+personalsettings.avatar.changecustom = Vlo\u017eit vlastn\u00ed obr\u00e1zek
+personalsettings.avatar.upload = Odeslat
+personalsettings.avatar.courtesy = Ikony jsou k dispozici se svolen\u00edm <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a> a \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Zm\u011bnit osobn\u00ed obr\u00e1zek
+avataruploadresult.success = Osobn\u00ed obr\u00e1zek "{0}" byl \u00fasp\u011b\u0161n\u011b odesl\u00e1n.
+avataruploadresult.failure = Nepoda\u0159ilo se odeslat obr\u00e1zek. Pro v\u00edce informac\u00ed prohl\u00e9dn\u011bte <a href="help.view?">log</a>.
+
+# passwordSettings.jsp
+passwordsettings.title = Zm\u011bnit heslo u\u017eivatele {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Slo\u017eka
+musicfoldersettings.name = N\u00e1zev
+musicfoldersettings.enabled = Aktivn\u00ed
+musicfoldersettings.add = P\u0159idat slo\u017eku s hudbou
+musicfoldersettings.nopath = Vyberte pros\u00edm slo\u017eku.
+
+# networkSettings.jsp
+networksettings.text = Pomoc\u00ed nastaven\u00ed n\u00ed\u017ee m\u016f\u017eete ur\u010dit, jak budete p\u0159istupovat k Subsonic server prost\u0159ednictv\u00edm Internetu.<br> \
+ Jestli\u017ee naraz\u00edte na pot\u00ed\u017ee, pros\u00edm pro\u010dt\u011bte si p\u0159\u00edru\u010dku <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Za\u010d\u00edn\u00e1me</b></a>.
+networksettings.portforwardingenabled = Automaticky nakonfigurovat router tak, aby umo\u017enil p\u0159\u00edchoz\u00ed p\u0159ipojen\u00ed na Subsonic (pou\u017eit\u00edm UPnP nebo NAT-PMP sm\u011brov\u00e1n\u00ed portu).
+networksettings.portforwardinghelp = Pokud v\u00e1\u0161 router nen\u00ed mo\u017en\u00e9 konfigurovat automaticky, m\u016f\u017eete jej nastavit ru\u010dn\u011b. \
+ Postupujte podle instrukc\u00ed popsan\u00fdch na <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Mus\u00edte p\u0159esm\u011brovat port {0} na po\u010d\u00edta\u010d, kde b\u011b\u0159\u00ed server Subsonic.
+networksettings.urlredirectionenabled = P\u0159\u00edstup k serveru p\u0159es Internet pomoc\u00ed snadno zapamatovateln\u00e9 adresy.
+networksettings.status = Status:
+networksettings.trialexpired = Zku\u0161ebn\u00ed doba vypr\u0161ela {0}. Pros\u00edm <b><a href="donate.view?">podpo\u0159te</a></b> aplikaci Subsonic a povol\u00edte t\u00edm v\u0161echny sou\u010d\u00e1sti trvale.
+networksettings.trialnotexpired = Tato sou\u010d\u00e1st bude dostupn\u00e1 do {0}. Po t\u00e9to dob\u011b mus\u00edte <b><a href="donate.view?">podpo\u0159it</a></b> aplikaci Subsonic, abyste j\u00ed mohli pou\u017e\u00edvat trvale.
+
+# transcodingSettings.jsp
+transcodingsettings.name = N\u00e1zev
+transcodingsettings.sourceformat = P\u0159ek\u00f3dovat z
+transcodingsettings.targetformat = P\u0159ek\u00f3dovat do
+transcodingsettings.step1 = Krok 1
+transcodingsettings.step2 = Krok 2
+transcodingsettings.step3 = Krok 3
+transcodingsettings.defaultactive = V\u00fdchoz\u00ed
+transcodingsettings.enabled = Aktivn\u00ed
+transcodingsettings.add = P\u0159idat p\u0159ek\u00f3dov\u00e1n\u00ed
+transcodingsettings.noname = Zadejte pros\u00edm n\u00e1zev.
+transcodingsettings.nosourceformat = Zadejte pros\u00edm zdrojov\u00fd form\u00e1t
+transcodingsettings.notargetformat = Zadejte pros\u00edm c\u00edlov\u00fd form\u00e1t
+transcodingsettings.nostep1 = Zadejte pros\u00edm alespo\u0148 jeden krok p\u0159ek\u00f3dov\u00e1n\u00ed
+transcodingsettings.info = <p class="detail">(%s = Soubor, kter\u00fd bude p\u0159ek\u00f3dov\u00e1n, %b = Maxim\u00e1ln\u00ed bitrate pro p\u0159ehrava\u010d)</p> \
+ <p>P\u0159ek\u00f3dov\u00e1n\u00ed je proces konverze jednoho form\u00e1tu m\u00e9dia do jin\u00e9ho form\u00e1tu. P\u0159ek\u00f3dov\u00e1n\u00ed {1} \
+ umo\u017e\u0148uje vys\u00edlat media, kter\u00e1 za norm\u00e1ln\u00edch okolnost\u00ed nejsou vys\u00edlateln\u00e1. Proces p\u0159ek\u00f3dov\u00e1n\u00ed prob\u00edh\u00e1 v re\u00e1ln\u00e9m \u010dase a nevy\u017eaduje \
+ pro svou \u010dinnost \u017e\u00e1dn\u00fd dodate\u010dn\u00fd diskov\u00fd prostor.<p/> \
+ <p>P\u0159ek\u00f3dov\u00e1n\u00ed je obvykle realizov\u00e1no prost\u0159ednictv\u00edm program\u016f t\u0159et\u00edch stran instalovan\u00fdch ve slo\u017ece {0}. \
+ Bal\u00edk kod\u00e9r\u016f pro Windows \
+ je dostupn\u00fd <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>zde</b></a>. M\u016f\u017eete tak\u00e9 p\u0159idat Va\u0161e vlastn\u00ed kod\u00e9ry, kter\u00e9 spl\u0148uj\u00ed \
+ tyto n\u00e1sleduj\u00edc\u00ed po\u017eadavky: \
+ <ul> \
+ <li>Obsahuj\u00ed rozhran\u00ed dostupn\u00e9 z p\u0159\u00edkazov\u00e9 \u0159\u00e1dky.</li> \
+ <li>Mus\u00ed b\u00fdt schopny odeslat v\u00fdstup do za\u0159\u00edzen\u00ed stdout.</li> \
+ <li>P\u0159i pou\u017eit\u00ed v kroku 2 nebo 3 mus\u00ed um\u011bt p\u0159ij\u00edmat vstup ze za\u0159\u00edzen\u00ed stdin.</li> \
+ </ul> \
+ </p> \
+ <p> P\u0159ek\u00f3dov\u00e1n\u00ed je aktivov\u00e1no na z\u00e1klad\u011b u\u017eivatelsk\u00e9ho nastaven\u00ed p\u0159ehrava\u010de na z\u00e1lo\u017ece "Nastaven\u00ed". Jestli\u017ee je vybr\u00e1na volba "V\u00fdchoz\u00ed", je kod\u00e9r \
+ aktivov\u00e1n automaticky pro nov\u011b definovan\u00e9 p\u0159ehrava\u010de.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL adresa vys\u00edl\u00e1n\u00ed
+internetradiosettings.homepageurl = Domovsk\u00e1 str\u00e1nka
+internetradiosettings.name = N\u00e1zev
+internetradiosettings.enabled = Aktivn\u00ed
+internetradiosettings.add = P\u0159idat Internetovou TV/r\u00e1dio
+internetradiosettings.nourl = Zadejte URL adresu.
+internetradiosettings.noname = Zadejte n\u00e1zev
+
+# podcastSettings.jsp
+podcastsettings.update = Zjistit nov\u00e9 d\u00edly
+podcastsettings.keep = Zachovat
+podcastsettings.keep.all = V\u0161echny d\u00edly
+podcastsettings.keep.one = Nejnov\u011bj\u0161\u00ed d\u00edly
+podcastsettings.keep.many = Posledn\u00ed {0} d\u00edly
+podcastsettings.download = Jestli\u017ee jsou dostupn\u00e9 nov\u00e9 d\u00edly
+podcastsettings.download.all = St\u00e1hni v\u0161echny
+podcastsettings.download.one = St\u00e1hni nejnov\u011bj\u0161\u00ed
+podcastsettings.download.many = St\u00e1hni posledn\u00edch {0} d\u00edl\u016f
+podcastsettings.download.none = Ned\u011blej nic
+podcastsettings.interval.manually = Ru\u010dn\u011b
+podcastsettings.interval.hourly = Ka\u017edou hodinu
+podcastsettings.interval.daily = Ka\u017ed\u00fd den
+podcastsettings.interval.weekly = Ka\u017ed\u00fd t\u00fdden
+podcastsettings.folder = Ulo\u017eit podcast do
+
+# playerSettings.jsp
+playersettings.noplayers = Nebyl nalezen \u017e\u00e1dn\u00fd p\u0159ehrava\u010d
+playersettings.type = Typ
+playersettings.lastseen = Naposledy shl\u00e9dnut\u00e9
+playersettings.title = Vyber p\u0159ehrava\u010d
+playersettings.technology.web.title = Webov\u00fd p\u0159ehrava\u010d
+playersettings.technology.external.title = Extern\u00ed p\u0159ehrava\u010d
+playersettings.technology.external_with_playlist.title = Extern\u00ed p\u0159ehrava\u010d s podporou seznamu
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = P\u0159ehr\u00e1v\u00e1 hudbu p\u0159\u00edmo v prohl\u00ed\u017ee\u010di prost\u0159ednictv\u00edm p\u0159ehrava\u010de Flash player.
+playersettings.technology.external.text = P\u0159ehr\u00e1v\u00e1 hudbu ve Va\u0161em obl\u00edben\u00e9m p\u0159ehrava\u010di jako je WinAmp nebo Windows Media Player.
+playersettings.technology.external_with_playlist.text = Podobn\u011b jako p\u0159edchoz\u00ed, ale seznam skladeb je spravov\u00e1n vlastn\u00edm p\u0159ehrava\u010dem \
+ a ne serverem Subsonic. V tomto re\u017eimu je mo\u017en\u00e9 p\u0159eskakovat skladby v seznamu.
+playersettings.technology.jukebox.text = P\u0159ehr\u00e1v\u00e1 hudbu p\u0159\u00edmo na za\u0159\u00edzen\u00ed instalovan\u00e9m v serveru Subsonic (Pouze vybran\u00ed u\u017eivatel\u00e9).
+playersettings.name = N\u00e1zev p\u0159ehrava\u010de
+playersettings.coverartsize = Velikost obr\u00e1zku alb
+playersettings.maxbitrate = Maxim\u00e1ln\u00ed bitrate
+playersettings.coverart.off = Vypnuto
+playersettings.coverart.small = Mal\u00fd
+playersettings.coverart.medium = St\u0159edn\u00ed
+playersettings.coverart.large = Velk\u00fd
+playersettings.nolame = <em>Pozn\u00e1mka:</em> Kodek LAME nen\u00ed pravd\u011bpodobn\u011b nainstalov\u00e1n.<br>Pro v\u00edce informac\u00ed klikni na tla\u010d\u00edtko Pomoc.
+playersettings.autocontrol = Automatick\u00e9 \u0159\u00edzen\u00ed p\u0159ehr\u00e1v\u00e1n\u00ed
+playersettings.dynamicip = P\u0159ehrava\u010d m\u00e1 dynamicky p\u0159id\u011blenou adresu IP
+playersettings.transcodings = Aktivovat p\u0159ek\u00f3dov\u00e1n\u00ed
+playersettings.ok = Ulo\u017eit
+playersettings.forget = Smazat p\u0159ehrava\u010d
+playersettings.clone = Kop\u00edrovat p\u0159ehrava\u010d
+
+# shareSettings.jsp
+sharesettings.name = N\u00e1zev
+sharesettings.owner = Sd\u00edleno u\u017eivatelem
+sharesettings.description = Popis
+sharesettings.visits = Nav\u0161t\u011bv
+sharesettings.lastvisited = Naposledy nav\u0161t\u00edveno
+sharesettings.expires = Vypr\u0161\u00ed
+sharesettings.files = Sd\u00edlen\u00e9 soubory
+sharesettings.expirein = Vypr\u0161\u00ed za
+sharesettings.expirein.week = t\u00fdden
+sharesettings.expirein.month = m\u011bs\u00edc
+sharesettings.expirein.year = rok
+sharesettings.expirein.never = nikdy
+
+# userSettings.jsp
+usersettings.title = Vybrat u\u017eivatele
+usersettings.newuser = Nov\u00fd u\u017eivatel
+usersettings.admin = U\u017eivatel je Administr\u00e1torem
+usersettings.settings = U\u017eivatel m\u016f\u017ee m\u011bnit nastaven\u00ed a heslo
+usersettings.stream = U\u017eivatel m\u016f\u017ee p\u0159ehr\u00e1vat soubory
+usersettings.jukebox = U\u017eivatel m\u016f\u017ee p\u0159ehr\u00e1vat soubory v re\u017eimu Jukebox
+usersettings.download = U\u017eivatel m\u016f\u017ee stahovat soubory ze serveru
+usersettings.upload = U\u017eivatel m\u016f\u017ee pos\u00edlat soubory na server
+usersettings.share = U\u017eivatel m\u016f\u017ee sd\u00edlet soubory s ostatn\u00edmi
+usersettings.playlist= U\u017eivatel m\u016f\u017ee vytv\u00e1\u0159et a mazat seznamy
+usersettings.coverart = U\u017eivatel m\u016f\u017ee m\u011bnit obr\u00e1zky alb a popisy skladeb
+usersettings.comment= U\u017eivatel m\u016f\u017ee vytv\u00e1\u0159et koment\u00e1\u0159e a hodnocen\u00ed
+usersettings.podcast= U\u017eivatel m\u016f\u017ee pracovat s Podcasty
+usersettings.username = Jm\u00e9no
+usersettings.email = Email
+usersettings.changepassword = Zm\u011bnit heslo
+usersettings.password = Heslo
+usersettings.newpassword = Nov\u00e9 heslo
+usersettings.confirmpassword = Potvrdit heslo
+usersettings.delete = Smazat tohoto u\u017eivatele
+usersettings.ldap = Ov\u011b\u0159it u\u017eivatele pomoc\u00ed LDAP
+usersettings.nousername = Chyb\u00ed jm\u00e9no u\u017eivatele.
+usersettings.noemail= \u0160patn\u00e1 emailov\u00e1 adresa.
+usersettings.useralreadyexists = Tento u\u017eivatel ji\u017e existuje.
+usersettings.nopassword = Je po\u017eadov\u00e1no heslo.
+usersettings.wrongpassword = Hesla se neshoduj\u00ed.
+usersettings.ldapdisabled = Ove\u0159ov\u00e1n\u00ed pomoc\u00ed LDAP nen\u00ed povoleno. Zkontrolujte roz\u0161\u00ed\u0159en\u00e9 nastaven\u00ed.
+usersettings.passwordnotsupportedforldap = Nen\u00ed mo\u017en\u00e9 nastavit nebo zm\u011bnit heslo pro u\u017eivatele s ov\u011b\u0159ov\u00e1n\u00ed pomoc\u00ed LDAP
+usersettings.ok = Heslo u\u017eivatele {0} bylo zm\u011bn\u011bno.
+
+# searchSettings.jsp
+searchsettings.auto = Automaticky aktualizovat vyhled\u00e1vac\u00ed index
+searchsettings.manual = Aktualizovat vyhled\u00e1vac\u00ed index nyn\u00ed.
+searchsettings.interval.never = Nikdy
+searchsettings.interval.one = Ka\u017ed\u00fd den
+searchsettings.interval.many = Ka\u017ed\u00e9 {0} dny
+searchsettings.hour = v {0}:00
+searchsettings.text = Vyhled\u00e1vac\u00ed index je pr\u00e1v\u011b vytv\u00e1\u0159en. Tato operace m\u016f\u017ee trvat n\u011bkolik minut v z\u00e1vislosti \
+ na velikosti knihovny m\u00e9di\u00ed. M\u016f\u017eete pokra\u010dovat ve vyhled\u00e1v\u00e1n\u00ed {0} i kdy\u017e index je\u0161t\u011b nen\u00ed vytvo\u0159en.
+
+# main.jsp
+main.up = Nahoru
+main.playall = P\u0159ehr\u00e1t v\u0161e
+main.playrandom = P\u0159ehr\u00e1t n\u00e1hodn\u011b
+main.addall = P\u0159idat v\u0161e
+main.tags = Upravit popisy
+main.playcount = P\u0159ehr\u00e1no {0} kr\u00e1t.
+main.lastplayed = Naposledy p\u0159ehr\u00e1no {0}.
+main.comment = Koment\u00e1\u0159
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Tu\u010dn\u00e9 </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Nov\u00fd \u0159\u00e1dek</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Kurziva </td><td style="padding-left:3em;padding-right:1em">(pr\u00e1zdn\u00fd \u0159\u00e1dek) </td><td>Nov\u00fd odstavec</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>V\u00fdpis polo\u017eek </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Odkaz</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>\u010c\u00edslovan\u00fd v\u00fdpis polo\u017eek</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Pojmenovan\u00fd odkaz</td></tr>\
+ </table>
+main.sharealbum = Sd\u00edlet
+main.more = V\u00edce...
+main.more.selection = Vybran\u00e9 skladby
+main.more.share = Sd\u00edlet
+main.donate = <a href="{0}" style="text-decoration:underline">Podpo\u0159te</a> {1}!<br>(reklama bude odstran\u011bna)
+main.nowplaying = P\u0159ehr\u00e1v\u00e1n\u00ed
+main.lyrics = Texty
+main.minutesago = minut zp\u011bt
+main.chat = Kr\u00e1tk\u00e9 zpr\u00e1vy
+main.message = Napsat kr\u00e1tkou zpr\u00e1vu
+main.clearchat = Smazat kr\u00e1tkou zpr\u00e1vu
+
+# rating.jsp
+rating.rating = Hodnocen\u00ed
+rating.clearrating = Smazat hodnocen\u00ed
+
+# coverArt.jsp
+coverart.change = Zm\u011bnit
+coverart.zoom = Zv\u011bt\u0161it
+
+# allmusic.jsp
+allmusic.text = Vyhled\u00e1v\u00e1m album <em>{0}</em> na serveru allmusic.com - Pros\u00edm \u010dekejte.
+
+# changeCoverArt.jsp
+changecoverart.title = Zm\u011bnit obr\u00e1zek alba
+changecoverart.address = Zadat adresu obr\u00e1zku alba
+changecoverart.artist = Autor
+changecoverart.album = Album
+changecoverart.searchdiscogs = Hledat v Discogs
+changecoverart.wait = Pros\u00edm \u010dekejte...
+changecoverart.success = Obr\u00e1zek byl \u00fasp\u011b\u0161n\u011b sta\u017een.
+changecoverart.error = Chyba p\u0159i pos\u00edl\u00e1n\u00ed obr\u00e1zku
+changecoverart.noimagesfound = Obr\u00e1zek nebyl nalezen.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Chyba p\u0159i zm\u011bn\u011b obr\u00e1zku alba <br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Upravit popis
+edittags.file = Soubor
+edittags.track = Stopa
+edittags.songtitle = N\u00e1zev
+edittags.artist = Autor
+edittags.album = Album
+edittags.year = Rok
+edittags.genre = \u017d\u00e1nr
+edittags.status = Status
+edittags.suggest = Navrhnout
+edittags.reset = Resetovat
+edittags.suggest.short = N
+edittags.reset.short = R
+edittags.set = Nastavit
+edittags.working = Pracuje
+edittags.updated = Aktualizov\u00e1no
+edittags.skipped = Vynech\u00e1no
+edittags.error = Chyba
+
+# share.jsp
+share.title = Sd\u00edlet
+share.warning = <h2>D\u016eLE\u017dIT\u00c9 UPOZORN\u011aN\u00cd!</h2><p>Bu\u010fte f\u00e9rov\u00ed a nesd\u00edlejte soubory chr\u00e1n\u011bn\u00e9 autorsk\u00fdmi pr\u00e1vy, proto\u017ee t\u00edm p\u0159ekra\u010dujete z\u00e1kon.</p>
+share.facebook = Sd\u00edlet na Facebooku
+share.twitter = Sd\u00edlet na Twiteru
+share.googleplus = Sd\u00edlet na Google+
+share.link = Nebo sd\u00edlejte s k\u00fdmkoliv tak, \u017ee jim za\u0161lete tento odkaz: <a href="{0}" target="_blank">{0}</a>
+share.disabled = Proto, abyste mohli sd\u00edlet hudbu s k\u00fdmkoliv si mus\u00edte zaregistrovat vlastn\u00ed adresu na <em>subsonic.org</em>.<br> \
+ Pros\u00edm p\u0159ejd\u011bte na <a href="networkSettings.view"><b>Nastaven\u00ed &gt; S\u00ed\u0165</b></a> (jsou vy\u017eadov\u00e1na opr\u00e1vn\u011bn\u00ed administr\u00e1tora).
+share.manage = Spravovat m\u00e1 sd\u00edlen\u00e1 media
+
+# donate.jsp
+donate.title = Podpo\u0159it
+donate.invalidlicense = \u0160patn\u00fd licen\u010dn\u00ed kl\u00ed\u010d.
+donate.amount = Podpo\u0159it {0}
+
+donate.textbefore = <p>D\u011bkujeme, \u017ee jste se rozhodli podpo\u0159it projekt {0}! \
+ Jako d\u00e1rce finan\u010dn\u00ed podpory z\u00edsk\u00e1te p\u0159\u00edstup k pr\u00e9miov\u00fdm sou\u010d\u00e1stem jako:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Aplikace</a> pro Android, iPhone a Windows Phone&nbsp;7*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Aplikace</a> pro PlayBook, Roku, Mac, Chrome a dal\u0161\u00ed*.</li> \
+ <li>Vys\u00edl\u00e1n\u00ed videa.</li> \
+ <li>Soukromou adresu ve tvaru: <em>mujserver</em>.subsonic.org (prohl\u00e9dn\u011bte <a href="networkSettings.view">Nastaven\u00ed &gt; S\u00ed\u0165</a>).</li> \
+ <li>Sd\u00edlen\u00ed Va\u0161ich medi\u00ed na Facebooku, Twitteru, Google+.</li> \
+ <li>Zru\u0161en\u00ed reklam v u\u017eivatelsk\u00e9m rozhran\u00ed.</li> \
+ <li>Dal\u0161\u00ed sou\u010d\u00e1sti, kter\u00e9 budou pozd\u011bji dod\u00e1ny.</li> \
+ </ul> \
+ <p style="font-size:9px;">* N\u011bkter\u00e9 aplikace jsou prod\u00e1v\u00e1ny v\u00fdvoj\u00e1\u0159i t\u0159et\u00edch stran.</p>\
+ <p>Jako d\u00e1rce finan\u010dn\u00ed podpory obdr\u017e\u00edte licen\u010dn\u00ed kl\u00ed\u010d kter\u00fd je pou\u017eiteln\u00fd pro nekomer\u010dn\u00ed \u00fa\u010dely s touto \
+ a kteroukoliv n\u00e1sleduj\u00edc\u00ed verz\u00ed aplikace {0}. Pro komer\u010dn\u00ed \u00fa\u010dely pros\u00edme <a href="mailto:subsonic_donation@activeobjects.no">kontaktujte n\u00e1s</a> pro vy\u017e\u00edzen\u00ed licen\u010dn\u00edch opr\u00e1vn\u011bn\u00ed.</p> \
+ <p>Navrhovan\u00e1 v\u00fd\u0161e finan\u010dn\u00edho p\u0159\u00edsp\u011bvku je <b>&euro;20</b>, ale m\u016f\u017eete vybrat jakoukoliv \u010d\u00e1stku:</p>
+donate.textafter = <p>Klikn\u011bte na jedno z tla\u010d\u00edtek platby pomoc\u00ed slu\u017eby PayPal, kde m\u016f\u017eete zaplatit Va\u0161i \u010d\u00e1stku \
+ pomoc\u00ed platebn\u00ed karty, nebo pomoc\u00ed existuj\u00edc\u00edho \u00fa\u010dtu PayPal (jestli\u017ee jej m\u00e1te). Po dokon\u010den\u00ed platby obdr\u017e\u00edte licen\u010dn\u00ed kl\u00ed\u010d emailem.</p> \
+ <p>Jestli\u017ee m\u00e1te jak\u00fdkoliv dotaz, pros\u00edme za\u0161lete jej na adresu \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Tato kopie {2} byla zaregistrov\u00e1na na {0}, {1}. D\u011bkujeme za Va\u0161i podporu!
+donate.register = Po obdr\u017een\u00ed Va\u0161eho licen\u010dn\u00edho kl\u00ed\u010de prove\u010fte registraci n\u00ed\u017ee.
+donate.resend = M\u00e1te ji\u017e zakoupenou licenci ale ztratili jste kl\u00ed\u010d? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Poslat kl\u00ed\u010d znovu</a>.
+donate.register.email = Email
+donate.register.license = Licence
+
+# podcastReceiver.jsp
+podcastreceiver.title = P\u0159ij\u00edma\u010d Podcast\u016f
+podcastreceiver.expandall = Zobrazit d\u00edly
+podcastreceiver.collapseall = Skr\u00fdt d\u00edly
+podcastreceiver.status.new = Nov\u00e9
+podcastreceiver.status.downloading = Stahov\u00e1n\u00ed
+podcastreceiver.status.completed = Dokon\u010deno
+podcastreceiver.status.error = Chyba
+podcastreceiver.status.deleted = Smaz\u00e1no
+podcastreceiver.status.skipped = Vynech\u00e1no
+podcastreceiver.downloadselected= St\u00e1hnout vybran\u00e9
+podcastreceiver.deleteselected= Smazat vybran\u00e9
+podcastreceiver.confirmdelete= Opravdu chcete smazat vybran\u00e9 Podcasty?
+podcastreceiver.check = Zjistit nov\u00e9 d\u00edly
+podcastreceiver.refresh = Obnovit str\u00e1nku
+podcastreceiver.settings = Nastaven\u00ed Podsatu
+podcastreceiver.subscribe = P\u0159ihl\u00e1sit do Podcastu
+
+# lyrics.jsp
+lyrics.title = Texty
+lyrics.artist = Autor
+lyrics.song = Sklatba
+lyrics.search = Hledat
+lyrics.wait = Vyhled\u00e1v\u00e1m text skladby, pros\u00edm \u010dekejte...
+lyrics.courtesy = (Text od <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Nenalezen text sklatby.
+
+# helpPopup.jsp
+helppopup.title = {0} Pomoc
+helppopup.cover.title = Velikost obr\u00e1zku alba
+helppopup.cover.text = <p>Umo\u017e\u0148uje zvolit velikost zobrazovan\u00e9ho obr\u00e1zku alba s mo\u017enost\u00ed jej \u00fapln\u011b vypnout.</p>
+helppopup.transcode.title = Maxim\u00e1ln\u00ed bitrate
+helppopup.transcode.text = <p>Jestli\u017ee m\u00e1te omezeno p\u00e1smo propustnosti internetu, m\u016f\u017eete omezit maxim\u00e1ln\u00ed bitrate vys\u00edlan\u00e9 hudby. \
+ Nap\u0159\u00edklad, jesli\u017ee origin\u00e1ln\u00ed soubor mp3 m\u00e1 bitrate 256Kbps, nastaven\u00ed maxim\u00e1ln\u00edho vys\u00edlan\u00e9ho bitrate \
+ na 128Kbps zp\u016fsob\u00ed, \u017ee soubor bude automaticky p\u0159evzorkov\u00e1n pro vys\u00edl\u00e1n\u00ed na 128Kbps.</p> \
+ <p>Tato mo\u017enost vy\u017eaduje instalov\u00e1n LAME enkod\u00e9r. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ je open source mp3 enkod\u00e9r. M\u016f\u017eete jej <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">st\u00e1hnout zde</a>. \
+ Enkod\u00e9r mus\u00ed b\u00fdt um\u00edst\u011bn ve slo\u017ece SUBSONIC_HOME/transcode, nebo ve slo\u017ece, jej\u00ed\u017e cesta je zadan\u00e1 v syst\u00e9mov\u00e9 prom\u011bnn\u00e9 PATH</p>
+helppopup.playlistfolder.title = Slo\u017eka seznamu
+helppopup.playlistfolder.text = <p>Umo\u017e\u0148uje zvolit slo\u017eku, ve kter\u00e9 jsou ulo\u017eeny seznamy skladeb.</p>
+helppopup.musicmask.title = Typy hudebn\u00edch soubor\u016f
+helppopup.musicmask.text = <p>Umo\u017e\u0148uje zvolit typy soubor\u016f, kter\u00e9 budou pova\u017eov\u00e1ny za hudebn\u00ed soubory p\u0159i proch\u00e1zen\u00ed slo\u017ekou hudby.</p>
+helppopup.videomask.title = Typy video soubor\u016f
+helppopup.videomask.text = <p>Umo\u017e\u0148uje zvolit typy soubor\u016f, kter\u00e9 budou pova\u017eov\u00e1ny za video soubory p\u0159i proch\u00e1zen\u00ed slo\u017ekou videa.</p>
+helppopup.coverartmask.title = Typy obr\u00e1zk\u016f alb
+helppopup.coverartmask.text = <p>Umo\u017e\u0148uje zvolit typy soubor\u016f, kter\u00e9 budou pova\u017eov\u00e1ny za obr\u00e1zky alb p\u0159i proch\u00e1zen\u00ed slo\u017ekou hudby.</p>
+helppopup.downsamplecommand.title = P\u0159\u00edkaz pro p\u0159evzorkov\u00e1n\u00ed
+helppopup.downsamplecommand.text = <p>Umo\u017e\u0148uje zvolit, jak\u00fd p\u0159\u00edkaz bude spu\u0161t\u011bn, jesli\u017ee je nastaveno p\u0159evzorkov\u00e1n\u00ed na ni\u017e\u0161\u00ed bitrate.</p>\
+ <p>(%s = soubor, kter\u00fd m\u00e1 b\u00fdt p\u0159evzorkov\u00e1n, %b = maxim\u00e1ln\u00ed bitrate p\u0159ehrava\u010de)</p>
+helppopup.index.title = Index
+helppopup.index.text = <p>Umo\u017e\u0148uje zvolit, jak bude vypadat index (viditeln\u00fd naho\u0159e na obrazovce). Prost\u0159ednictv\u00edm tohoto indexu \
+ jsou jednodu\u0161eji dostupn\u00e9 soubory a slo\u017eky, kter\u00e9 jsou um\u00edst\u011bn\u00e9 p\u0159\u00edmo v ko\u0159eni slo\u017eky s hudbou.</p> \
+ <p>Index je seznam polo\u017eek odd\u011blen\u00fdch mezerami. Norm\u00e1ln\u011b je ka\u017ed\u00e1 polo\u017eka zastoupen\u00e1 jedn\u00edm znakem, \
+ m\u016f\u017eete ale tak\u00e9 specifikovat v\u00edceznakovou polo\u017eku. Nap\u0159\u00edklad, polo\u017eka <em>The</em> bude slou\u017eit jako z\u00e1stupce \
+ v\u0161ech soubor\u016f a slo\u017eek, kter\u00e9 za\u010d\u00ednaj\u00ed slovem "The".</p> \
+ <p>M\u016f\u017eete tak\u00e9 vytvo\u0159it index s pou\u017eit\u00edm skupin znak\u016f v z\u00e1vork\u00e1ch. Nap\u0159\u00edklad, polo\u017eka <em>A-E(ABCDE)</em> \
+ bude v indexu zobrazena jako "A-E" a bude slou\u017eit jako z\u00e1stupce pro v\u0161echny soubory a slo\u017eky za\u010d\u00ednaj\u00edc\u00ed \
+ znaky A, B, C, D nebo E. Toto m\u016f\u017ee b\u00fdt u\u017eite\u010dn\u00e9 v p\u0159\u00edpad\u011b, \u017ee pot\u0159ebujete seskupit m\u00e9n\u011b pou\u017e\u00edvan\u00e9 znaky (jako X, Y a Z), nebo \
+ pro seskupen\u00ed znak\u016f s diakritikou (jako nap\u0159\u00edklad A, \u00c0 a \u00c1)</p> \
+ <p>Soubory a slo\u017eky, kter\u00e9 nespadaj\u00ed pod \u017e\u00e1dn\u00e9ho z\u00e1stupce indexu, budou um\u00edst\u011bny pod polo\u017eku "#".</p>
+helppopup.ignoredarticles.title = Vynechan\u00e9 n\u00e1zvy
+helppopup.ignoredarticles.text = <p>Umo\u017e\u0148uje zvolit seznam n\u00e1zv\u016f, kter\u00e9 maj\u00ed b\u00fdt vynech\u00e1ny p\u0159i tvorb\u011b indexu (jako nap\u0159\u00edklad "The").</p>
+helppopup.shortcuts.title = Z\u00e1stupci
+helppopup.shortcuts.text = <p>Seznam mezerou odd\u011blen\u00fdch slo\u017eek, pro kter\u00e9 maj\u00ed b\u00fdt vytvo\u0159eny zkratky. Pro seskupov\u00e1n\u00ed slov pou\u017eijte uvozovky, nap\u0159\u00edklad:</p> \
+ <p><em>Nov\u011b ulo\u017een\u00e9 "Hudebn\u00ed soubory"</em></p>
+helppopup.language.title = Jazyk
+helppopup.language.text = <p>Umo\u017e\u0148uje vybrat, kter\u00fd jazyk bude pou\u017eit.</p>
+helppopup.visibility.title = Viditelnost
+helppopup.visibility.text = <p>Vyberte, jak\u00fd detail bude zobrazen pro ka\u017edou skladbu p\u0159i zobrazen\u00ed popis\u016f a taky maxim\u00e1ln\u00ed po\u010det \
+ znak\u016f, kter\u00e9 budou zobrazov\u00e1ny pro autora, n\u00e1zvu skladby a n\u00e1zvu alba.</p>
+helppopup.partymode.title = Zjednodu\u0161en\u00e9 rozhran\u00ed
+helppopup.partymode.text = <p>Jestli\u017ee je zvoleno Zjednodu\u0161en\u00e9 rozhran\u00ed, bude u\u017eivatelsk\u00e9 rozhran\u00ed zjednodu\u0161eno pro lep\u0161\u00ed orientaci nezku\u0161en\u00fdch u\u017eivatel\u016f. \
+ V tomto p\u0159\u00edpad\u011b je taky ohl\u00edd\u00e1no ne\u00famysln\u00e9 smaz\u00e1n\u00ed seznamu skladeb.</p>
+helppopup.theme.title = Motiv
+helppopup.theme.text = <p>Umo\u017e\u0148uje zvolit, jak\u00fd motiv bude pou\u017eit. Motiv ur\u010duje, jak bude vypadat {0}, jak\u00e9 budou pou\u017eity barvy, p\u00edsma, obr\u00e1zky atd.</p>
+helppopup.welcomemessage.title = Uv\u00edtac\u00ed zpr\u00e1va
+helppopup.welcomemessage.text = <p>Zpr\u00e1va, kter\u00e1 bude zobrazena na domovsk\u00e9 str\u00e1nce.</p>
+helppopup.loginmessage.title = P\u0159ihla\u0161ovac\u00ed zpr\u00e1va
+helppopup.loginmessage.text = <p>Zpr\u00e1va, kter\u00e1 bude zobrazena na p\u0159ihla\u0161ovac\u00ed str\u00e1nce.</p>
+helppopup.coverartlimit.title = Po\u010det obr\u00e1zk\u016f alb
+helppopup.coverartlimit.text = <p>Maxim\u00e1ln\u00ed po\u010det obr\u00e1zk\u016f alb zobrazen\u00fdch na jedn\u00e9 str\u00e1nce.</p>
+helppopup.downloadlimit.title = Limit stahov\u00e1n\u00ed
+helppopup.downloadlimit.text = <p>Horn\u00ed limit \u0161\u00ed\u0159ky p\u00e1sma pro stahov\u00e1n\u00ed soubor\u016f ze serveru.</p>
+helppopup.uploadlimit.title = Limit pos\u00edl\u00e1n\u00ed
+helppopup.uploadlimit.text = <p>Horn\u00ed limit \u0161\u00ed\u0159ky p\u00e1sma pro pos\u00edl\u00e1n\u00ed soubor\u016f na server.</p>
+helppopup.streamport.title = Port pro vys\u00edl\u00e1n\u00ed (ne SSL)
+helppopup.streamport.text = <p>Tato mo\u017enost je relevantn\u00ed pouze, pokud pou\u017e\u00edv\u00e1te {0} na serveru s SSL \u0161ifrov\u00e1n\u00edm (HTTPS).</p><p>N\u011bkter\u00e9 p\u0159ehrava\u010de \
+ (nap\u0159\u00edklad Winamp) neumo\u017e\u0148uj\u00ed vys\u00edl\u00e1n\u00ed prost\u0159ednictv\u00edm SSL. Zvolte port pro standardn\u00ed HTTP (obvykle 80 \
+ nebo 4040) jestli\u017ee nechcete, aby vys\u00edl\u00e1n\u00ed prob\u00edhalo prost\u0159ednictv\u00edm SSL. Pozor! Vys\u00edl\u00e1n\u00ed v tomto p\u0159\u00edpad\u011b nebude \u0161ifrov\u00e1no.</p>
+helppopup.ldap.title = Ov\u011b\u0159en\u00ed LDAP
+helppopup.ldap.text = <p>U\u017eivatel\u00e9 mohou b\u00fdt ov\u011b\u0159ov\u00e1ni prost\u0159ednictv\u00edm extern\u00edho serveru LDAP (v\u010detn\u011b Windows Active Directory). \
+ Jestli\u017ee se tito u\u017eivatel\u00e9 p\u0159ihla\u0161uj\u00ed do {0}, jm\u00e9no a heslo jsou ov\u011b\u0159ov\u00e1ny prost\u0159ednictv\u00edm extern\u00edho serveru.</p>
+helppopup.ldapurl.title = URL adresa LDAP
+helppopup.ldapurl.text = <p>URL adresa LDAP serveru. Pou\u017eit\u00fd protokol mus\u00ed b\u00fdt bu\u010fto <em>ldap://</em> nebo <em>ldaps://</em> \
+ (pro LDAP p\u0159es SSL). <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">Zde</a> \
+ se do\u010dtete podrobn\u00e9 informace.</p>
+helppopup.ldapsearchfilter.title = Filtr vyhled\u00e1v\u00e1n\u00ed LDAP
+helppopup.ldapsearchfilter.text = <p>V\u00fdraz pro filtr pou\u017eit\u00fd p\u0159i vyhled\u00e1v\u00e1n\u00ed u\u017eivatele. Filtr vyhled\u00e1v\u00e1n\u00ed LDAP \
+ (popsan\u00fd v <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Symbol "'{0'}" je nahrazen za p\u0159ihla\u0161ovac\u00ed jm\u00e9no, nap\u0159\u00edklad: \
+ <ul>\
+ <li>(uid='{0'}) - tento v\u00fdraz vyhled\u00e1v\u00e1 jm\u00e9no u\u017eivatele podle atributu uid.</li> \
+ <li>(sAMAccountName='{0'}) - tento v\u00fdraz se pou\u017e\u00edv\u00e1 pro ov\u011b\u0159ov\u00e1n\u00ed v Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = Spr\u00e1vce DN LDAP
+helppopup.ldapmanagerdn.text = <p>Jestli\u017ee LDAP server nepodporuje anonymn\u00ed p\u0159ipojen\u00ed, mus\u00edte specifikovat DN \
+ (<em>Distinguished Name</em>) a heslo u\u017eivatele, pod kter\u00fdm se k LDAP budete p\u0159ipojovat.</p>
+helppopup.ldapautoshadowing.title = Automaticky vytvo\u0159it u\u017eivatele LDAP v {0}
+helppopup.ldapautoshadowing.text = <p>Jestli\u017ee je tato mo\u017enost zvolena, u\u017eivatel\u00e9 LDAP nemus\u00ed b\u00fdt manu\u00e1ln\u011b vytvo\u0159eni v {0} je\u0161t\u011b p\u0159ed p\u0159ihl\u00e1\u0161en\u00edm.</p> \
+ <p>Pozor! Znamen\u00e1 to, \u017ee se jak\u00fdkoliv u\u017eivatel s platn\u00fdm LDAP jm\u00e9nem a heslem m\u016f\u017ee p\u0159ihl\u00e1sit do {0}, \
+ co\u017e m\u016f\u017ee b\u00fdt ne\u017e\u00e1douc\u00ed.</p>
+helppopup.playername.title = N\u00e1zev p\u0159ehrava\u010de
+helppopup.playername.text = <p>Umo\u017e\u0148uje zvolit n\u00e1zev p\u0159ehrava\u010de pro lep\u0161\u00ed zapamatov\u00e1n\u00ed, nap\u0159\u00edklad "V pr\u00e1ci" nebo "Doma".</p>
+helppopup.autocontrol.title = Automatick\u00e9 \u0159\u00edzen\u00ed p\u0159ehr\u00e1v\u00e1n\u00ed
+helppopup.autocontrol.text = <p>Tato volba umo\u017e\u0148uje nastavit {0} tak, aby se p\u0159ehr\u00e1v\u00e1n\u00ed spustilo automaticky, jestli\u017ee kliknete "P\u0159ehr\u00e1t" \
+ v seznamu skladeb. V opa\u010dn\u00e9m p\u0159\u00edpad\u011b mus\u00edte spustit p\u0159ehrava\u010d ru\u010dn\u011b.</p>
+helppopup.dynamicip.title = Dynamick\u00e1 IP adresa
+helppopup.dynamicip.text = <p>Tuto mo\u017enost vypn\u011bte pokud p\u0159ehrava\u010d pou\u017e\u00edv\u00e1 statickou IP adresu.</p>
+
+# wap/index.jsp
+wap.index.missing = Nenalezena \u017e\u00e1dn\u00e1 hudba
+wap.index.playlist = Seznam
+wap.index.search = Hledat
+wap.index.settings = Nastaven\u00ed
+
+# wap/browse.jsp
+wap.browse.playone = P\u0159ehr\u00e1t skladbu
+wap.browse.playall = P\u0159ehr\u00e1t v\u0161e
+wap.browse.addone = P\u0159idat skladbu
+wap.browse.addall = P\u0159idate v\u0161e
+wap.browse.downloadone = St\u00e1hnout skladbu
+wap.browse.downloadall = St\u00e1hnout v\u0161e
+
+# wap/playlist.jsp
+wap.playlist.title = Seznam
+wap.playlist.noplayer = Nen\u00ed p\u0159ipojen \u017e\u00e1dn\u00fd p\u0159ehrava\u010d
+wap.playlist.clear = Vy\u010distit
+wap.playlist.load = Nahr\u00e1t
+wap.playlist.random = N\u00e1hodn\u011b
+wap.playlist.play = P\u0159ehr\u00e1t na telefonu
+
+# wap/search.jsp
+wap.search.title = Hledat
+
+# wap/searchResult.jsp
+wap.searchresult.index = Pr\u00e1v\u011b je vytv\u00e1\u0159en index pro vyhled\u00e1v\u00e1n\u00ed. Zkuste to pros\u00edm pozdeji.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Vybrat p\u0159ehrava\u010d
+wap.settings.allplayers = V\u0161e
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties
new file mode 100644
index 00000000..76a78a4d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties
@@ -0,0 +1,704 @@
+ # Danish localization.
+ # Forfatter: Morten Hartvich
+ #
+
+common.home = Hjem
+common.back = Tilbage
+common.help = Hj\u00e6lp
+common.play = Spil
+common.add = Tilf\u00f8j
+common.download = Download
+common.close = Luk
+common.refresh = opdat\u00e9r
+common.next = N\u00e6ste
+common.previous = Forrige
+common.more = Mere
+common.ok = OK
+common.cancel = Annuller
+common.save = Gem
+common.create = Opret
+common.delete = Slet
+common.unknown = (Ukendt)
+common.default = (Standard)
+
+# Login.jsp
+login.username = Brugernavn
+login.password = Password
+login.login = Log in
+login.remember = Husk mig
+login.logout = Du er nu logget ud, Tak for bes\u00f8get.
+login.error = Forkert brugernavn eller password
+login.insecure = {0} er ikke sikret. Log ind med brugernavn og <br> password "admin", eller klik p\u00e5 <a href="login.view?user=admin&amp;password=admin">her</a>. Derefter \u00e6ndre adgangskode \u00f8jeblikkeligt.
+
+# AccessDenied.jsp
+accessDenied.title = Ingen adgang
+accessDenied.text = Du har desv\u00e6rre ikke tilladelse til at udf\u00f8re den \u00f8nskede handling.
+
+# Top.jsp
+top.home = Forside
+top.now_playing = Afspilning
+top.settings = Indstillinger
+top.status = Status
+top.podcast = Podcast
+top.more = Mere
+top.help = Hj\u00e6lp
+top.search = S\u00f8g
+top.upgrade = <b> Bem\u00e6rk! </b> En ny version er tilg\u00e6ngelig. <br> Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')"> her</a>.
+top.missing = Ingen medie mapper fundet. Skal du \u00e6ndre indstillingerne.
+top.logout = Log ud {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artister<br>\
+ {1}&nbsp;albums<br>\
+ {2}&nbsp;sange<br>\
+ {3} (&#126; {4} timer)
+left.shortcut = Genveje
+left.radio = Internet TV / radio
+left.allfolders = Alle mapper
+
+# Playlist.jsp
+playlist.stop = Stop
+playlist.start = Spil
+playlist.confirmclear = Slet spilleliste?
+playlist.clear = Ryd
+playlist.shuffle = Blande
+playlist.repeat_on = Gentag er p\u00e5
+playlist.repeat_off = Gentag er slukket
+playlist.undo = Fortryd
+playlist.settings = Indstillinger
+playlist.more = Flere handlinger ...
+playlist.more.playlist = Spilleliste
+playlist.more.sortbytrack = Sorter efter spor
+playlist.more.sortbyartist = Sorter efter kunstner
+playlist.more.sortbyalbum = Sortering album
+playlist.more.selection = valgte sange
+playlist.more.selectall = V\u00e6lg alle
+playlist.more.selectnone = V\u00e6lg ingen
+playlist.getflash = F\u00e5 Flash Player
+playlist.load = Indl\u00e6s
+playlist.save = Gem
+playlist.append = Tilf\u00f8j til afspilningsliste
+playlist.remove = Fjern
+playlist.up = Up
+playlist.down = Ned
+playlist.empty = Spillelisten er tom
+
+# Status.jsp
+status.title = Status
+status.type = Type
+status.stream = Stream
+status.download = Download
+status.upload = Upload
+status.player = Afspiller
+status.user = Bruger
+status.current = Aktuelle fil
+status.transmitted = Overf\u00f8rt
+status.bitrate = Bitrate (Kbps)
+
+# Search.jsp
+search.title = Title
+search.search = S\u00f8g
+search.index = S\u00f8geanmodningsparameteren indeks er i \u00f8jeblikket ved at blive oprettet. Pr\u00f8v igen senere.
+search.hits.none = Ingen resultater fundet.
+
+# Home.jsp
+home.random.title = Blandet
+home.newest.title = Nyeste
+home.highest.title = H\u00f8jeste karakter
+home.frequent.title = Hyppigst spillet
+home.recent.title = For nylig spillet
+home.users.title = Brugere
+home.random.text = Blandet album
+home.newest.text = Senest tilf\u00f8jede eller opdateret albums
+home.highest.text = H\u00f8jeste popul\u00e6re albums
+home.frequent.text = Hyppigst spillet albums
+home.recent.text = For nylig spillede albums
+home.users.text = Brugerstatisik
+home.scan = Mappen medie er i \u00f8jeblikket ved at blive scannet. Alle funktioner er endnu ikke tilg\u00e6ngelige.
+home.listsize = {0} albums per side
+home.albums = Albums {0} - {1}
+home.playcount = Spillet {0} sange
+home.lastplayed = Sidst spillet {0}
+home.created = \u00c6ndret {0}
+home.chart.total = Total (MB)
+home.chart.stream = Streamede (MB)
+home.chart.download = Downloaded (MB)
+home.chart.upload = Uploaded (MB)
+
+# More.jsp
+more.title = Mere
+more.random.title = Tilf\u00e6ldige afspilningsliste
+more.random.text = Opret tilf\u00e6ldig spilleliste med
+more.random.songs = {0} sange
+more.random.auto = Spil flere tilf\u00e6ldige sange n\u00e5r slutningen af afspilningslisten er n\u00e5et.
+more.random.ok = OK
+more.random.genre = fra genre
+more.random.anygenre = Enhver
+more.random.year = og \u00e5r
+more.random.anyyear = Enhver
+more.random.folder = i mappen
+more.random.anyfolder = Enhver
+more.apps.title = Subsonic Apps
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic Apps</a> er til \
+ r\u00e5dighed for <b>iPhone</b>, <b>Android</b> og <b>AIR</b>.</p>
+more.mobile.title = Mobiltelefon
+more.mobile.text = <p> Du kan styre {0} fra en WAP-mobiltelefon eller PDA. <br> \
+ Blot bes\u00f8ge f\u00f8lgende webadresse fra telefonen: <b> http://yourhostname/wap </b> </p> \
+ <p> Dette kr\u00e6ver, at din server kan n\u00e5s fra internettet. </p>
+more.podcast.title = Podcast
+more.podcast.text = <p> Gemte spillelister er tilg\u00e6ngelige som Podcasts. <br> \
+ Brug f\u00f8lgende URL i din Podcast receiver: <b> http://yourhostname/podcast </b>, \
+ eller <b> <a href="podcast.view?suffix=.rss"> Klik her</a>.</b> </p>
+more.upload.title = Upload fil
+more.upload.source = V\u00e6lg fil
+more.upload.target = Upload til
+more.upload.browse = V\u00e6lg
+more.upload.ok = Upload
+more.upload.unzip = Automatisk udpakning af zip-fil.
+more.upload.progress =% fuldf\u00f8rt. Vent venligst ...
+
+# Upload.jsp
+upload.title = Overf\u00f8rer fil
+upload.success = uploadet <b> {0} </b>
+upload.empty = Ingen filer til overf\u00f8rsel.
+upload.failed = Uploading mislykkedes med f\u00f8lgende fejl: <br> <b> "{0}" </b>
+upload.unzipped = Udpakket {0}
+
+# Help.jsp
+help.title = Om {0}
+help.upgrade = <b> Bem\u00e6rk! </b> En ny version er tilg\u00e6ngelig. Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">her</a>.
+help.version.title = Version
+help.builddate.title = Oprettelsesdato
+help.server.title = Server
+help.license.title = Licens
+help.license.text = {0} er gratis software, der distribueres i henhold til <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank"> GPL </a> open-source licens. \
+ {0} bruger <a href="helpPopup.view?topic=licenses" onclick="return popup(this,''help'')"> licenseret tredjeparts biblioteker </a>.
+help.homepage.title = Hjemmeside
+help.forum.title = Forum
+help.shop.title = Merchandise
+help.contact.title = Kontakt
+help.contact.text = {0} er udviklet og vedligeholdes af Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no"> sindre@activeobjects.no </a>). \
+ Hvis du har sp\u00f8rgsm\u00e5l, kommentarer eller forslag til forbedringer, kan du bes\u00f8ge \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} er gratis, men du kan bidrage til projektet ved at give en <b> <a href="donate.view?"> donation </a> </b>.
+help.log = Log
+help.logfile = Den komplette log er gemt i {0}.
+
+# SettingsHeader.jsp
+settingsheader.title = Indstillinger
+settingsheader.general = General
+settingsheader.advanced = Avanceret
+settingsheader.personal = Personlig
+settingsheader.musicFolder = Medie mapper
+settingsheader.internetRadio = Internet TV / radio
+settingsheader.podcast = Podcast
+settingsheader.player = Afspillere
+settingsheader.share = Delt medie
+settingsheader.network = Netv\u00e6rk
+settingsheader.transcoding = Kodning
+settingsheader.user = Brugere
+settingsheader.search = S\u00f8g
+settingsheader.coverArt = Cover
+settingsheader.password = Password
+
+# GeneralSettings.jsp
+generalsettings.playlistfolder = Spilleliste mappe
+generalsettings.musicmask = Musik maske
+generalsettings.videomask = Video maske
+generalsettings.ignoredarticles = Ord som skal ignoreres
+generalsettings.loginmessage = Logon meddelelse
+generalsettings.coverartmask = Cover maske
+generalsettings.index = Indeks
+generalsettings.shortcuts = Genveje
+generalsettings.showgettingstarted = Vis "Kom godt i gang" ved Login
+generalsettings.welcometitle = Velkommen titel
+generalsettings.welcomesubtitle = Velkommen undertitel
+generalsettings.welcomemessage = Velkomstmeddelelse
+generalsettings.language = Standard sprog
+generalsettings.theme = Standard tema
+
+# AdvancedSettings.jsp
+advancedsettings.downsamplecommand = Downsample kommando
+advancedsettings.coverartlimit = Cover gr\u00e6nse <br> <div class="detail"> (0 = ubegr\u00e6nset) </ div>
+advancedsettings.downloadlimit = Download gr\u00e6nse (Kbps) <br> <div class="detail"> (0 = ubegr\u00e6nset) </ div>
+advancedsettings.uploadlimit = Upload gr\u00e6nse (Kbps) <br> <div class="detail"> (0 = ubegr\u00e6nset) </ div>
+advancedsettings.streamport = Ikke-SSL stream port <br> <div class="detail"> (0 = Deaktiveret) </ div>
+advancedsettings.ldapenabled = Aktiv\u00e9r LDAP-godkendelse
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP s\u00f8gefilter
+advancedsettings.ldapmanagerdn = LDAP manager DN <br> <div class="detail"> (valgfri) </ div>
+advancedsettings.ldapmanagerpassword = Password
+advancedsettings.ldapautoshadowing = Automatisk oprettet brugere i {0}
+
+# PersonalSettings.jsp
+personalsettings.title = Personlige indstillinger for {0}
+personalsettings.language = Sprog
+personalsettings.theme = Tema
+personalsettings.display = Display
+personalsettings.browse = Gennemse
+personalsettings.playlist = Spilleliste
+personalsettings.tracknumber = Track #
+personalsettings.artist = Kunstner
+personalsettings.album = Album
+personalsettings.genre = Genre
+personalsettings.year = \u00c5r
+personalsettings.bitrate = Bithastighed
+personalsettings.duration = Varighed
+personalsettings.format = Format
+personalsettings.filesize = Filst\u00f8rrelse
+personalsettings.captioncutoff = Caption cutoff
+personalsettings.partymode = Fest indstilling
+personalsettings.shownowplaying = Vis hvad andre spiller
+personalsettings.nowplayingallowed = Lad andre se, hvad jeg spiller
+personalsettings.finalversionnotification = Advis\u00e9r mig om nye versioner
+personalsettings.betaversionnotification = Advis\u00e9r mig om nye beta-versioner
+personalsettings.showchat = Vis chat meddelelse
+personalsettings.lastfmenabled = Registrer hvad jeg spiller p\u00e5 <a href="http://last.fm/" target="_blank"> Last.fm </a>
+personalsettings.lastfmusername = Last.fm brugernavn
+personalsettings.lastfmpassword = Last.fm adgangskode
+personalsettings.avatar.title = Personligt image
+personalsettings.avatar.none = Intet image
+personalsettings.avatar.custom = Tilpassede image
+personalsettings.avatar.changecustom = Skift tilpassede image
+personalsettings.avatar.upload = Upload
+personalsettings.avatar.courtesy = Icons courtesy of <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# AvatarUploadResult.jsp
+avataruploadresult.title = Skift personlig image
+avataruploadresult.success = Uploadet personlig image "{0}".
+avataruploadresult.failure = Kunne ikke uploade personlig image. Se <a href="help.view?"> log </a> for yderligere oplysninger.
+
+# PasswordSettings.jsp
+passwordsettings.title = Skift adgangskode til {0}
+
+# MusicFolderSettings.jsp
+musicfoldersettings.path = Mappe
+musicfoldersettings.name = Navn
+musicfoldersettings.enabled = Aktiveret
+musicfoldersettings.add = Tilf\u00f8j medie mappe
+musicfoldersettings.nopath = Angiv en mappe.
+
+# TranscodingSettings.jsp
+transcodingsettings.name = Navn
+transcodingsettings.sourceformat = Konverter fra
+transcodingsettings.targetformat = Konverter til
+transcodingsettings.step1 = Trin 1
+transcodingsettings.step2 = Trin 2
+transcodingsettings.step3 = Trin 3
+transcodingsettings.defaultactive = Standard
+transcodingsettings.enabled = Aktiveret
+transcodingsettings.add = Tilf\u00f8j kodning
+transcodingsettings.noname = Angiv et navn.
+transcodingsettings.nosourceformat = Angiv formatet til at konvertere fra.
+transcodingsettings.notargetformat = Angiv formatet til at konvertere til.
+transcodingsettings.nostep1 = Angiv mindst \u00e9t kodning skridt.
+transcodingsettings.info = <p class="detail">(% s = Den fil, der skal omkodet,% b = Max bitrate for afspilleren)</p> \
+ <p>Kodning er processen som konvertere fra ét medie format til et andet. {1}''s omkodning \
+ giver mulighed for streaming af medier, der normalt ville ikke v\u00e6re mulige at streame. Denne omkodning er foretaget on-the-fly og kr\u00e6ver nogen diskaktivitet. <p/> \
+ <p>Den faktiske omkodning er udf\u00f8rt af tredjepart kommandolinje-programmer, som skal installeres i {0}. \
+ En omkodning pakke til Windows \
+ er tilg\u00e6ngelig <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>her</b></a>. Du kan tilf\u00f8je dine egne brugerdefinerede omkodninger og kaldet transcoder, hvis det \
+ opfylder f\u00f8lgende krav: \
+ <ul> \
+ <li>Den skal have en kommandolinje-gr\u00e6nseflade.</li> \
+ <li>Det skal kunne sende output til stdout.</li> \
+ <li>Hvis der anvendes i trin 2 eller 3, skal den v\u00e6re i stand til at l\u00e6se input fra stdin.</li> \
+ </ul> \
+ </p> \
+ <p>Bem\u00e6rk, at omkodningen er aktiveret p\u00e5 en per-afspiller i ops\u00e6tningsmenuen af afspillere. Hvis "Standard" er markeret, vil omkodningen \
+ aktiveres automatisk for nye afspillere. </p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradiosettings.homepageurl = Hjemmeside
+internetradiosettings.name = Navn
+internetradiosettings.enabled = Aktiveret
+internetradiosettings.add = Tilf\u00f8j Internet TV / radio
+internetradiosettings.nourl = Angiv en webadresse.
+internetradiosettings.noname = Angiv et navn.
+
+# PodcastSettings.jsp
+podcastsettings.update = Kontroller for nye episoder
+podcastsettings.keep = Hold
+podcastsettings.keep.all = Alle episoder
+podcastsettings.keep.one = Nyeste episode
+podcastsettings.keep.many = Seneste {0} episoder
+podcastsettings.download = N\u00e5r nye episoder er tilg\u00e6ngelige
+podcastsettings.download.all = Download alle
+podcastsettings.download.one = Download den seneste en
+podcastsettings.download.many = Download sidste {0} episoder
+podcastsettings.download.none = Ingen
+podcastsettings.interval.manually = Manuelt
+podcastsettings.interval.hourly = Hver time
+podcastsettings.interval.daily = Hver dag
+podcastsettings.interval.weekly = Hver uge
+podcastsettings.folder = Gem Podcasts i
+
+# PlayerSettings.jsp
+playersettings.noplayers = Ingen spillere fundet.
+playersettings.type = Type
+playersettings.lastseen = Sidst set
+playersettings.title = V\u00e6lg afspiller
+playersettings.technology.web.title = Web afspiller
+playersettings.technology.external.title = Eksterne afspiller
+playersettings.technology.external_with_playlist.title = Eksterne afspiller med afspilningsliste
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Spil medie direkte i webbrowseren ved hj\u00e6lp af integreret Flash Player.
+playersettings.technology.external.text = Spil medie i din foretrukne afspiller, s\u00e5som Winamp eller Windows Media Player.
+playersettings.technology.external_with_playlist.text = Samme som ovenfor, men afspilningslisten forvaltes af afspilleren, snarere \
+ end Subsonic serveren. I denne tilstand, springe inden sange er muligt.
+playersettings.technology.jukebox.text = Spil medie direkte p\u00e5 lydenheden til Subsonic serveren. (Autoriserede brugere).
+playersettings.name = Afspiller navn
+playersettings.coverartsize = Cover st\u00f8rrelse
+playersettings.maxbitrate = Max bitrate
+playersettings.coverart.off = Off
+playersettings.coverart.small = Small
+playersettings.coverart.medium = Medium
+playersettings.coverart.large = Large
+playersettings.nolame = <em> Meddelelse: </ em> LAME synes ikke at v\u00e6re installeret. <br> Klik p\u00e5 knappen Hj\u00e6lp for yderligere oplysninger.
+playersettings.autocontrol = Control afspilleren automatisk
+playersettings.dynamicip = Afspiller har dynamisk IP-adresse
+playersettings.transcodings = Aktive kodninger
+playersettings.ok = Gem
+playersettings.forget = Slet afspiller
+playersettings.clone = Klon afspiller
+
+# NetworkSettings.jsp
+networksettings.text = Brug indstillingerne nedenfor til at kontrollere, hvordan adgangen til din Subsonic server skal v\u00e6re over internettet.
+networksettings.portforwardingenabled = Automatisk konfigurere routeren til at tillade indg\u00e5ende forbindelser til Subsonic (UPnP port forwarding).
+networksettings.portforwardinghelp = Hvis din router ikke kan ops\u00e6ttes automatisk, skal du s\u00e6tte den op manuelt. \
+ F\u00f8lg vejledningen p\u00e5 <a href="http://portforward.com/" target="_blank">portforward.com </a>. \
+ Du skal forwarde port {0} til computeren, som k\u00f8rer Subsonic server.
+networksettings.urlredirectionenabled = F\u00e5 adgang til din server over internettet ved hj\u00e6lp af en adresse, der er let at huske.
+networksettings.status = Status:
+networksettings.trialexpired = Pr\u00f8veperioden udl\u00f8b den {0}. Venligst <b> <a href="donate.view?"> donere </ a> </ b> for at aktivere denne funktion permanent.
+networksettings.trialnotexpired = Denne funktion er tilg\u00e6ngelig indtil {0}. Herefter skal du <b><a href="donate.view?">donere</a></b> for at bruge den permanent.
+
+# CoverArtSettings.jsp
+coverartsettings.auto = Automatisk download manglede covers n\u00e5r s\u00f8geindeks er opdateret.
+coverartsettings.manual = Download manglede covers nu.
+coverartsettings.missing = {0} af {1} albums mangler i \u00f8jeblikket cover.
+coverartsettings.running = Downloader covers. Dette kan tage flere minutter, afh\u00e6ngigt af, hvor stort \
+ mediebibliotek er.
+coverartsettings.albumList = Liste over albums der mangler cover.
+
+# shareSettings.jsp
+sharesettings.name = Navn
+sharesettings.owner = Delt af
+sharesettings.description = Beskrivelse
+sharesettings.visits = Antal bes\u00f8g
+sharesettings.lastvisited = Seneste bes\u00f8g
+sharesettings.expires = Udl\u00f8ber
+sharesettings.files = Delte filer
+sharesettings.expirein = Udl\u00f8ber om
+sharesettings.expirein.week = 1u
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1\u00e5
+sharesettings.expirein.never = aldrig
+
+# UserSettings.jsp
+usersettings.title = V\u00e6lg brugertype
+usersettings.newuser = Ny bruger
+usersettings.admin = Bruger er administrator
+usersettings.settings = Brugeren har lov til at \u00e6ndre indstillinger og adgangskode
+usersettings.stream = Bruger har lov til at afspille filer
+usersettings.jukebox = Brugeren har lov til at afspille filer i jukebox mode
+usersettings.download = Bruger har lov til at hente filer
+usersettings.upload = Bruger har lov til at uploade filer
+usersettings.share = Bruger har lov til at dele filer med alle
+usersettings.playlist = Bruger har lov til at oprette og slette afspilningslister
+usersettings.coverart = Bruger har lov at \u00e6ndre Cover og tags
+usersettings.comment = Bruger har lov til at oprette og redigere kommentarer og ratings
+usersettings.podcast = Bruger har lov til at administrere Podcasts
+usersettings.username = Brugernavn
+usersettings.changepassword = Skift adgangskode
+usersettings.password = Password
+usersettings.newpassword = Ny adgangskode
+usersettings.confirmpassword = Bekr\u00e6ft adgangskode
+usersettings.delete = Slet denne bruger
+usersettings.ldap = Godkend bruger i LDAP
+usersettings.nousername = Intet brugernavn.
+usersettings.useralreadyexists = Bruger eksisterer allerede.
+usersettings.nopassword = Password er p\u00e5kr\u00e6vet.
+usersettings.wrongpassword = Passwords ukendt.
+usersettings.ldapdisabled = LDAP-godkendelse er ikke aktiveret. Se Avancerede indstillinger.
+usersettings.passwordnotsupportedforldap = Kan ikke indstille eller \u00e6ndre adgangskode til LDAP-godkendte brugere.
+usersettings.ok = Password blev \u00e6ndret for bruger {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = aldrig
+musicfoldersettings.interval.one = Hver dag
+musicfoldersettings.interval.many = Hver {0} dage
+musicfoldersettings.hour = p\u00e5 {0}: 00
+
+# main.jsp
+main.up = Niveau op
+main.playall = Spil alt
+main.playrandom = Spil blandet
+main.addall = Tilf\u00f8j alt
+main.tags = \u00c6ndre tags
+main.playcount = Spillet {0} gange.
+main.lastplayed = Sidst spillet {0}.
+main.comment = Kommentar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__tekst__</td><td>Fed tekst</td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Ny linje</td></tr>\
+ <tr><td style="padding-right:1em">~~tekst~~</td><td>Kursiv tekst</td><td style="padding-left:3em;padding-right:1em">(tom linje) </td><td>Nyt afsnit</td></tr>\
+ <tr><td style="padding-right:1em">* tekst </td><td>Opstilling med punkttegn </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. tekst </td><td>Opstilling med tal</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Navngivet link</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Don\u00e9r</a> til {1}!<br>(og fjern denne reklame)
+main.nowplaying = Spiller nu
+main.sharealbum = Del
+main.more.share = Del
+main.lyrics = Tekster
+main.minutesago = Minutter siden
+main.message = Skriv en meddelelse
+main.chat = Chat meddelelse
+main.clearchat = Slet chat meddelelse
+
+# gettingsStarted.jsp
+gettingStarted.title = Kom godt i gang
+gettingStarted.text = <p>Velkommen til Subsonic! Som ops\u00e6ttes p\u00e5 ingen tid, skal du blot f\u00f8lge de grundl\u00e6ggende trin nedenfor. <br> \
+ Klik p\u00e5 "Forside" knappen i v\u00e6rkt\u00f8jslinjen ovenfor for at komme tilbage til dette sk\u00e6rmbillede.</p>
+gettingStarted.step1.title = Skift administrator adgangskode.
+gettingStarted.step1.text = Sikre din server ved at \u00e6ndre standard password for administrator konto. \
+ Du kan ogs\u00e5 oprette nye brugerkonti med forskellige rettigheder.
+gettingStarted.step2.title = Indstil medie mapper.
+gettingStarted.step2.text = Fort\u00e6l Subsonic hvor du opbevarer din medie.
+gettingStarted.step3.title = Konfigurer netv\u00e6rksindstillinger.
+gettingStarted.step3.text = Nogle nyttige indstillinger, hvis du vil nyde din medie over internettet, \
+ eller dele det med familie og venner. F\u00e5 din personlige <b><em>ditnavn</em>.subsonic.org</b> adresse.
+gettingStarted.hide = Vis ikke denne igen
+gettingStarted.hidealert = For at vise dette sk\u00e6rmbillede igen, skal du g\u00e5 til Indstillinger &gt; Generel.
+
+# Rating.jsp
+rating.rating = Karakter
+rating.clearrating = Ryd karakter
+
+# CoverArt.jsp
+coverart.change = Skift
+coverart.zoom = Zoom
+
+# Allmusic.jsp
+allmusic.text = S\u00f8gning efter album <em> {0} </ em> p\u00e5 allmusic.com - Vent venligst.
+
+# ChangeCoverArt.jsp
+changecoverart.title = Skift omslagsbilleder
+changecoverart.address = Eller indtast image adresse
+changecoverart.artist = Kunstner
+changecoverart.album = Album
+changecoverart.searchdiscogs = S\u00f8g Discogs
+changecoverart.wait = Vent venligst ...
+changecoverart.success = Image blev hentet.
+changecoverart.error = Det lykkedes ikke at hente billedet.
+changecoverart.noimagesfound = Ingen billeder fundet.
+
+# ChangeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Det lykkedes ikke at \u00e6ndre omslagsbilleder: <br> <b> "{0}" </b>
+
+# EditTags.jsp
+edittags.title = Rediger tags
+edittags.file = File
+edittags.track = Spor
+edittags.songtitle = Titel
+edittags.artist = Kunstner
+edittags.album = Album
+edittags.year = \u00c5r
+edittags.genre = Genre
+edittags.status = Status
+edittags.suggest = Forslag
+edittags.reset = Nulstil
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Indstil
+edittags.working = Arbejde
+edittags.updated = Opdateret
+edittags.skipped = Sprunget over
+edittags.error = Fejl
+
+# share.jsp
+share.title = Del
+share.warning = <h2>VIGTIG INFORMATION!</h2><p>Undg\u00e5 at dele ophavsretligt beskyttet materiale p\u00e5 nogen m\u00e5de, der overtr\u00e6der loven.</p>
+share.facebook = Del p\u00e5 Facebook
+share.twitter = Del p\u00e5 Twitter
+share.link = Eller dele dette med nogen, ved at sende dem dette link: <a href="{0}" target="_blank">{0}</a>
+share.disabled = Hvis du vil dele din medie med nogen, skal du f\u00f8rst registrere din egen <em>subsonic.org</em> address.<br> \
+ G\u00e5 til <a href="networkSettings.view"><b>Settings &gt; Network</b></a> (administrative rights required).
+share.manage = Administrere mine delte medier
+
+
+# Donate.jsp
+donate.title = Donation
+donate.invalidlicense = Ugyldig licens n\u00f8gle.
+donate.amount = Donate {0}
+donate.textbefore = <p> Tak for behandlingen af en donation til st\u00f8tte for {0} projekt! \
+ Som donor vil du modtage en licens n\u00f8gle, som deaktiverer annoncer og donation anmodning beskeder. Licensen er gyldig i denne \
+ og alle fremtidige udgivelser af {0}. </p> \
+ <p> Den foresl\u00e5ede donation bel\u00f8b er <b> &euro; 20 </b>, men du kan give lige s\u00e5 meget eller s\u00e5 lidt som du har lyst. \
+ Bem\u00e6rk, at den licens n\u00f8glen vil blive sendt til den e-mail-adresse, du angiver, s\u00e5 s\u00f8rg for, at du giver en \
+ korrekte adresse, n\u00e5r du registrerer donation p\u00e5 PayPal. </p>
+donate.textafter = <p> Klik p\u00e5 en af knapperne for at g\u00e5 til PayPal, hvor du kan betale med kreditkort eller ved at bruge \
+ din PayPal-konto (hvis du har en). N\u00e5r tapningen er behandlet, vil du modtage licensn\u00f8glen via email. </p> \
+ <p> Hvis du har sp\u00f8rgsm\u00e5l, bedes du sende en email til \
+ <a href="mailto:subsonic_donation@activeobjects.no"> subsonic_donation@activeobjects.no</a>. </p>
+donate.licensed = Denne kopi af {2} blev givet i licens til {0} af {1}. Tak for din st\u00f8tte!
+donate.register = Efter du modtager din licens n\u00f8gle, kan du registrere den nedenfor.
+donate.register.email = Email
+donate.register.license = Licens
+
+# PodcastReceiver.jsp
+podcastreceiver.title = Podcast receiver
+podcastreceiver.expandall = Vis episoder
+podcastreceiver.collapseall = Skjul episoder
+podcastreceiver.status.new = Ny
+podcastreceiver.status.downloading = Hentning
+podcastreceiver.status.completed = Afsluttet
+podcastreceiver.status.error = Fejl
+podcastreceiver.status.deleted = Slettet
+podcastreceiver.status.skipped = Sprunget over
+podcastreceiver.downloadselected = Download udvalgte
+podcastreceiver.deleteselected = Slet valgte
+podcastreceiver.confirmdelete = Vil du slette valgte Podcasts?
+podcastreceiver.check = Kontroller for nye episoder
+podcastreceiver.refresh = Opdater side
+podcastreceiver.settings = Podcast indstillinger
+podcastreceiver.subscribe = Abonner p\u00e5 podcast
+
+# Lyrics.jsp
+lyrics.title = Lyrics
+lyrics.artist = Kunstner
+lyrics.song = Sang
+lyrics.search = S\u00f8g
+lyrics.wait = S\u00f8ger efter sangtekster, vent venligst ...
+lyrics.courtesy = (Lyrics ved <a href="http://www.chartlyrics.com/" target="_blank"> chartlyrics.com </a>)
+lyrics.nolyricsfound = Ingen sangtekster fundet.
+
+# HelpPopup.jsp
+helppopup.title = {0} Hj\u00e6lp
+helppopup.loginmessage.text = <p>Besked, der vises p\u00e5 loginsiden.</p>
+helppopup.loginmessage.title = Login besked
+helppopup.videomask.text = <p> Lader dig angive, hvilken type filer, der skal genkendes som video.</ p>
+helppopup.videomask.title = Video maske
+helppopup.cover.title = Cover st\u00f8rrelse
+helppopup.cover.text = <p> Lader dig angive st\u00f8rrelsen af det viste Cover, med mulighed for at slukke for den helt. </p>
+helppopup.transcode.title = Max bitrate
+helppopup.transcode.text = <p> Hvis du har begr\u00e6nset b\u00e5ndbredde, kan du s\u00e6tte en \u00f8vre gr\u00e6nse for bithastighed af musikken str\u00f8mme. \
+ For eksempel, hvis din oprindelige mp3 filer er kodet ved hj\u00e6lp af 256 Kbps (kilobit pr sekund), fasts\u00e6ttelse af max bitrate \
+ til 128 vil g\u00f8re {0} automatisk resample musikken fra 256 til 128 Kbps. </p> \
+ <p> Denne valgmulighed kr\u00e6ver, at LAME er installeret. LAME <a target="_blank" href="http://lame.sourceforge.net/"> (http://lame.sourceforge.net) </a> \
+ er et open source mp3 kodeenhed. Du kan <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/"> hente det her </a>. \
+ S\u00f8rg for at installere det i SUBSONIC_HOME / omkode eller i en mappe, der er til stede i din PATH milj\u00f8-variabel. </p>
+helppopup.playlistfolder.title = Playlist mappe
+helppopup.playlistfolder.text = <p> Lader dig angive den mappe, hvor dine spillelister er beliggende. </p>
+helppopup.musicmask.title = Music maske
+helppopup.musicmask.text = <p> Lader dig angive den type filer, der skal anerkendes som medie, n\u00e5r du browser gennem medie mappe. </p>
+helppopup.coverartmask.title = Cover maske
+helppopup.coverartmask.text = <p> Lader dig angive den type filer, der skal anerkendes som Cover n\u00e5r du browser gennem medie mappe. </p>
+helppopup.downsamplecommand.title = Downsample kommando
+helppopup.downsamplecommand.text = <p> Lader dig angiver kommandoen til at udf\u00f8re, n\u00e5r downsampling til lavere bitrates. </p> \
+ <p> (% s = Filen skal downsamplet,% b = Max bithastighed af afspilleren) </p>
+helppopup.index.title = Indeks
+helppopup.index.text = <p> Lader dig angive, hvordan indekset (placeret \u00f8verst p\u00e5 sk\u00e6rmen) skal se ud. Filer og mapper \
+ direkte i roden medie mappe er nemt tilg\u00e6ngelige ved hj\u00e6lp af dette indeks. </p> \
+ <p> Varespecifikationen er et rum-separeret liste over opslagsord. Normalt hver indrejse er blot et enkelt tegn, \
+ men du kan ogs\u00e5 angive flere tegn. F.eks indrejse <em> </ em> vil linke til alle filer og \
+ mapper, der begynder med "I". </p> \
+ <p> Du kan ogs\u00e5 oprette en post ved hj\u00e6lp af en gruppe af indeks tegn i parentes. F.eks indrejse \
+ <em> AE (ABCDE) </ em> vises som <em> AE </ em> og link til alle filer og mapper, der begynder med enten \
+ A, B, C, D eller E. Det kan v\u00e6re nyttigt til at samle de mindre hyppigt anvendte tegn (s\u00e5dan og X, Y og Z) eller \
+ for gruppering accent tegn (s\u00e5som A, \u00c0 og \u00c1) </p> \
+ <p> filer og mapper, der ikke er omfattet af et opslagsord vil blive placeret under opslagsord "#".</p>
+helppopup.ignoredarticles.title = Ord som skal ignoreres
+helppopup.ignoredarticles.text = <p> Lader dig angive en liste over artikler (s\u00e5som "De"), som vil blive ignoreret, n\u00e5r der skabes indekset. </p>
+helppopup.shortcuts.title = Genveje
+helppopup.shortcuts.text = <p> A space-separeret liste over \u00f8verste niveau mapper til at oprette genveje til. Brug anf\u00f8rselstegn til at gruppere ord, for eksempel: </p> \
+ <p> <em> New Indg\u00e5ende "lydspor" </ em> </p>
+helppopup.language.title = Sprog
+helppopup.language.text = <p> Lader dig v\u00e6lge sprog til brug. </p>
+helppopup.visibility.title = Synlighed
+helppopup.visibility.text = <p> V\u00e6lg, hvilke oplysninger der skal vises for hver sang, samt billedtekst cutoff. Dette er den maksimale \
+ Antallet af tegn til at vise for sang titel, album og kunstner. </p>
+helppopup.partymode.title = Fest indstilling
+helppopup.partymode.text = <p> N\u00e5r fest indstilling er aktiveret, brugergr\u00e6nsefladen er forenklet og lettere at betjene for ikke-erfarne brugere. \
+ Is\u00e6r utilsigtet Messing op afspilningslisten undg\u00e5s. </p>
+helppopup.theme.title = Tema
+helppopup.theme.text = <p> Lader dig v\u00e6lge tema til brug. Et tema definerer udseendet og fornemmelsen af {0} i form af farver, skrifter, billeder osv. </p>
+helppopup.welcomemessage.title = Velkomstmeddelelse
+helppopup.welcomemessage.text = <p> besked, der vises p\u00e5 hjemmesiden. </p>
+helppopup.coverartlimit.title = Cover gr\u00e6nse
+helppopup.coverartlimit.text = <p> Det maksimale antal Cover billeder skal vises p\u00e5 en enkelt side. </p>
+helppopup.downloadlimit.title = Download gr\u00e6nse
+helppopup.downloadlimit.text = <p> en \u00f8vre gr\u00e6nse for, hvor meget b\u00e5ndbredde vil blive brugt til at downloade filer. </p>
+helppopup.uploadlimit.title = Upload gr\u00e6nse
+helppopup.uploadlimit.text = <p> en \u00f8vre gr\u00e6nse for, hvor meget b\u00e5ndbredde vil blive brugt til at uploade filer. </p>
+helppopup.streamport.title = Ikke-SSL stream havn
+helppopup.streamport.text = <p> Denne valgmulighed er kun relevant, hvis du bruger {0} p\u00e5 en server med SSL (HTTPS). </p> <p> Nogle spillere \
+ (s\u00e5som Winamp) don''t st\u00f8tte streaming via SSL. Angiv portnummeret for regelm\u00e6ssig http (normalt 80 \
+ eller 4040), hvis du don''t \u00f8nsker streams, der skal sendes via SSL. Bem\u00e6rk, at streams ikke vil v\u00e6re krypteret. </p>
+helppopup.ldap.title = LDAP autentificering
+helppopup.ldap.text = <p> Brugere kan blive bekr\u00e6ftet af en ekstern LDAP server (herunder Windows Active Directory). \
+ N\u00e5r LDAP-aktiverede brugere logge p\u00e5 {0}, brugernavnet og adgangskoden er kontrolleret af den eksterne server, ikke af {0} selv. </p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p> Webadressen p\u00e5 LDAP-serveren. Protokollen skal enten v\u00e6re <em> ldap ://</ em> eller <em> ldaps ://</ em> \
+ (for LDAP over SSL). Se <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank"> her </a> \
+ for en mere detaljeret beskrivelse. </p>
+helppopup.ldapsearchfilter.title = LDAP s\u00f8gefilter
+helppopup.ldapsearchfilter.text = <p> Filteret udtryk anvendes i brugernes s\u00f8gning. Dette er et LDAP s\u00f8gefilter \
+ (som defineret i <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank"> RFC 2254 </a>). \
+ M\u00f8nsteret " '{0}" affattes brugernavn, for eksempel: \
+ <ul> \
+ <li> (uid = '{0'}) - dette vil s\u00f8ge efter et brugernavn passer p\u00e5 uid attribut. </ li> \
+ <li> (sAMAccountName = '{0'}) - typisk bruges til godkendelse i Microsoft Active Directory. </ li> \
+ </ ul> </p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p> Hvis LDAP-serveren doesn''t st\u00f8tte anonyme bindende skal du angive DN \
+ (<em> Distinguished Name </ em>) og din adgangskode i LDAP bruger til bruger, n\u00e5r bindende. </p>
+helppopup.ldapautoshadowing.title = automatisk oprette LDAP brugere i {0}
+helppopup.ldapautoshadowing.text = <p> Med denne indstilling v\u00e6lges, LDAP brugere don''t skal manuelt oprettede i {0} f\u00f8r logger p\u00e5. </p> \
+ <p> BEM\u00c6RK! Det betyder, at alle brugere med en gyldig LDAP brugernavn og kodeord kan logge p\u00e5 {0}, \
+ som m\u00e5ske ikke er, hvad du vil. </p>
+helppopup.playername.title = Spiller navn
+helppopup.playername.text = <p> Lader dig angiver en nem at huske navnet p\u00e5 en spiller, som "Work" eller "Stue". </p>
+helppopup.autocontrol.title = Control afspilleren automatisk
+helppopup.autocontrol.text = <p> Med denne indstilling v\u00e6lges, {0} vil automatisk starte afspilleren, n\u00e5r du klikker p\u00e5 "play" \
+ p\u00e5 afspilningslisten. Ellers skal du begynde og slutte spilleren selv. </p>
+helppopup.dynamicip.title = dynamisk IP-adresse
+helppopup.dynamicip.text = <p> Sl\u00e5 denne mulighed, hvis spilleren anvender en statisk IP-adresse. </p>
+
+# Wap / index.jsp
+wap.index.missing = Ingen medie fundet
+wap.index.playlist = Playlist
+wap.index.search = S\u00f8g
+wap.index.settings = Indstillinger
+
+# Wap / browse.jsp
+wap.browse.playone = Afspil sang
+wap.browse.playall = Afspil alle
+wap.browse.addone = Tilf\u00f8j sang
+wap.browse.addall = Tilf\u00f8j alle
+wap.browse.downloadone = Download sang
+wap.browse.downloadall = Download alle
+
+# Wap / playlist.jsp
+wap.playlist.title = Playlist
+wap.playlist.noplayer = Ingen afspiller tilsluttet
+wap.playlist.clear = Ryd
+wap.playlist.load = Indl\u00e6s
+wap.playlist.random = Random
+wap.playlist.play = Spil p\u00e5 telefon
+
+# Wap / search.jsp
+wap.search.title = S\u00f8g
+
+# Wap / searchResult.jsp
+wap.searchresult.index = S\u00f8geanmodningsparameteren indeks er i \u00f8jeblikket ved at blive oprettet. Pr\u00f8v igen senere.
+
+# Wap / settings.jsp
+wap.settings.selectplayer = V\u00e6lg afspiller
+wap.settings.allplayers = Alle
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_de.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_de.properties
new file mode 100644
index 00000000..c021e3d6
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_de.properties
@@ -0,0 +1,686 @@
+#
+# German localization.
+# Author: Harald Weiss (Hari1984 at gmx.at)
+# Last Updated by: deejay2302 (djdanby@googlemail.com)
+# Last Update Date: 08.05.2011
+
+common.home = Home
+common.back = Zur\u00fcck
+common.help = Hilfe
+common.play = Abspielen
+common.add = Hinzuf\u00fcgen
+common.download = Download
+common.close = Schlie\u00dfen
+common.refresh = Neu Laden
+common.next = N\u00e4chste Seite
+common.previous = Vorige Seite
+common.more = Mehr
+common.ok = OK
+common.cancel = Abbrechen
+common.save = Speichern
+common.create = Erzeugen
+common.delete = L\u00f6schen
+common.unknown = (Unbekannt)
+common.default = (Standard)
+
+# login.jsp
+login.username = Benutzername
+login.password = Passwort
+login.login = Einloggen
+login.remember = Login speichern
+login.logout = Ausgeloggt
+login.error = Falscher Benutzername oder Passwort
+
+# accessDenied.jsp
+accessDenied.title = Zugang verweigert
+accessDenied.text = Entschuldigung, du bist nicht authorisiert die angeforderte Funktion zu nutzen.
+
+# top.jsp
+top.home = Startseite
+top.now_playing = Jetzt&nbsp;l\u00e4uft
+top.settings = Einstellungen
+top.status = Status
+top.more = Mehr
+top.help = Hilfe
+top.search = Suchen
+top.upgrade = <b>Note!</b> Eine neue Version ist erh\u00e4ltlich.<br>Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">hier</a>.
+top.missing = Keine Musikordner gefunden. Bitte die Einstellungen \u00fcberpr\u00fcfen.
+top.logout = Ausloggen {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;K\u00fcnstler<br>\
+ {1}&nbsp;Alben<br>\
+ {2}&nbsp;Songs<br>\
+ {3} (&#126; {4} Stunden)
+left.shortcut = Spezialordner
+left.radio = Internet TV/Radio
+left.allfolders = Alle Ordner
+
+# playlist.jsp
+playlist.stop = Stop
+playlist.start = Play
+playlist.confirmclear = Wirklich die Playliste leeren?
+playlist.clear = Leeren
+playlist.shuffle = Mischen
+playlist.repeat_on = Wiederholung ist an
+playlist.repeat_off = Wiederholung ist aus
+playlist.undo = R\u00fcckg\u00e4ngig
+playlist.settings = Einstellungen
+playlist.more = Weitere Aktionen...
+playlist.more.playlist = Playlist
+playlist.more.sortbytrack = Nach Titel sortieren
+playlist.more.sortbyartist = Nach K\u00fcnstler sortieren
+playlist.more.sortbyalbum = Nach Album sortieren
+playlist.more.selection = Auswahl
+playlist.more.selectall = Alle ausw\u00e4hlen
+playlist.more.selectnone = Nichts ausw\u00e4hlen
+playlist.getflash = Download Flash player
+playlist.load = Laden
+playlist.save = Speichern
+playlist.append = Zur Playlist hinzuf\u00fcgen
+playlist.remove = L\u00f6schen
+playlist.up = Auf
+playlist.down = Ab
+playlist.empty = Playlist ist leer
+
+# status.jsp
+status.title = Status
+status.type = Typ
+status.stream = Stream
+status.download = Download
+status.upload = Upload
+status.player = Player
+status.user = Benutzer
+status.current = Aktuelle Datei
+status.transmitted = \u00dcbertragen
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Suchen
+search.search = Suchen
+search.query = Interpret, Album oder Songtitel
+search.index = Der Suchindex wird gerade erstellt. Bitte sp\u00e4ter probieren.
+search.hits.none = Keine \u00dcbereinstimmungen gefunden.
+search.hits.more = Mehr
+search.hits.artists = Interpreten
+search.hits.albums = Alben
+search.hits.songs = Songs
+
+# gettingStarted.jsp
+gettingStarted.title = Erster Start
+gettingStarted.text = <p>Willkommen in Subsonic! Wir werden in K\u00fcrze soweit sein, folgen sie dazu den unten aufgef\u00fchrten grundlegenden Schritten.<br> \
+ Klicke den "Startseite" Button oberhalb der Toolbar um zu diesem Bildschirm zur\u00fcckzukehren.</p>
+gettingStarted.step1.title = \u00c4ndere das Administrator Kennwort.
+gettingStarted.step1.text = Sichere deinen Server indem du das Standard Passwort f\u00fcr den Administrator Account \u00e4nderst. \
+ Sie k\u00f6nnen auch Benutzerkonten mit unterschiedlichen Berechtigungen erstellen.
+gettingStarted.step2.title = Musikordner einrichten.
+gettingStarted.step2.text = Zeige Subsonic wo sich deine Musik befindet.
+gettingStarted.step3.title = Konfiguriere Netzwerk Einstellungen.
+gettingStarted.step3.text = Einige n\u00fctzliche Einstellungen um ihre Musik \u00fcber das Internet geniessen zu k\u00f6nnen, \
+ oder um sie mit Freunden oder Familie zu teilen. Holen sie sich ihre pers\u00f6nliche <b><em>DeinName</em>.subsonic.org</b> \
+ Addresse.
+gettingStarted.hide = Nicht wieder anzeigen
+gettingStarted.hidealert = Um diesen Bildschirm wieder anzuzeigen, gehe zu Einstellungen > Allgemein.
+
+# home.jsp
+home.random.title = Zuf\u00e4llig
+home.newest.title = Neueste
+home.highest.title = Am besten bewertet
+home.frequent.title = Am h\u00e4ufigsten gespielt
+home.recent.title = Zuletzt gespielt
+home.users.title = Benutzerstatistik
+home.random.text = Zuf\u00e4llige Alben
+home.newest.text = Zuletzt hinzugef\u00fcgt oder ge\u00e4nderte Alben
+home.highest.text = Am besten bewertete Alben
+home.frequent.text = Am h\u00e4ufigsten gespielte Alben
+home.recent.text = Zuletzt gespielte Alben
+home.users.text = Benutzerstatistiken
+home.scan = Der Musik Ordner wird gerade gescannt. Es sind noch nicht alle Funktionen verf\u00fcgbar.
+home.listsize = {0} Alben pro Seite
+home.albums = Alben {0} - {1}
+home.playcount = Gespielte {0} Songs
+home.lastplayed = {0} gespielt
+home.created = {0} ge\u00e4ndert
+home.chart.total = Gesamt (MB)
+home.chart.stream = Gestreamt (MB)
+home.chart.download = Heruntergeladen (MB)
+home.chart.upload = Hochgeladen (MB)
+
+# more.jsp
+more.title = Mehr
+more.random.title = Zuf\u00e4llige Playliste
+more.random.text = Erzeuge zuf\u00e4llige Playlist mit
+more.random.songs = {0} Songs
+more.random.auto = Spiele weitere zuf\u00e4llige Songs wenn das Ende der Playlist erreicht wurde.
+more.random.ok = OK
+more.random.genre = aus dem Genre
+more.random.anygenre = Alle
+more.random.year = und dem Jahr
+more.random.anyyear = Alle
+more.random.folder = im Verzeichnis
+more.random.anyfolder = Alle
+more.mobile.title = Handy
+more.mobile.text = <p>Du kannst {0} mit einem WAP f\u00e4higen Handy oder PDA kontrollieren.<br> \
+ Einfach folgende URL mit dem Handy \u00f6ffnen: <b>http://yourhostname/wap</b></p> \
+ <p>Dazu muss dein Server im Internet erreichbar sein.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Gespeicherte Playlists sind als Podcast verf\u00fcgbar.<br>\
+ Benutze folgende URL: <b>http://yourhostname/podcast</b>, \
+ oder <b><a href="podcast.view?suffix=.rss">Klick hier</a>.</b></p>
+more.upload.title = Datei hochladen
+more.upload.source = Datei ausw\u00e4hlen
+more.upload.target = Hochladen nach
+more.upload.browse = Suchen
+more.upload.ok = Hochladen
+more.upload.unzip = Zip-Datei automatisch entpacken.
+more.upload.progress = % fertig gestellt. Bitte warten...
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic apps</a> ist verf\u00fcgbar f\u00fcr <b>iPhone</b>, <b>Android</b> und <b>AIR</b>.</p>
+
+
+# upload.jsp
+upload.title = Datei wird hochgeladen
+upload.success = Erfolgreich hochgeladen <b>{0}</b>
+upload.empty = Keine Dateien zum hochladen.
+upload.failed = Hochladen war nicht erfolgreich, Fehler:<br><b>"{0}"</b>
+upload.unzipped = Entpackt {0}
+
+# help.jsp
+help.title = \u00dcber {0}
+help.upgrade = <b>Note!</b> Eine neue Version ist erh\u00e4ltlich. Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">hier</a>.
+help.version.title = Version
+help.builddate.title = Erstellungs Datum
+help.server.title = Server
+help.license.title = Lizenz
+help.license.text = {0} ist eine frei vertriebene Software unter der <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source Lizenz.
+help.homepage.title = Homepage
+help.forum.title = Forum
+help.shop.title = Fan-Artikel Shop
+help.contact.title = Kontakt
+help.contact.text = {0} ist von Sindre Mehus entwickelt worden \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Wenn du Fragen, Kommentare oder Vorschl\u00e4ge f\u00fcr Verbesserungen hast, dann besuche bitte das \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} ist gratis, aber du kannst das Projekt mit einer Spende unterst\u00fctzen.
+help.log = Log
+help.logfile = Der komplette Log ist gespeichert in {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Einstellungen
+settingsheader.general = Allgemein
+settingsheader.advanced = Erweitert
+settingsheader.personal = Erscheinung
+settingsheader.musicFolder = Musikordner
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.player = Player
+settingsheader.share = Geteilte Medien
+settingsheader.network = Netzwerk
+settingsheader.transcoding = Transcoding
+settingsheader.user = Benutzer
+settingsheader.search = Suchen
+settingsheader.coverArt = Album Covers
+settingsheader.password = Passwort
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Playlistordner
+generalsettings.musicmask = Musikdateitypen
+generalsettings.coverartmask = Coverdateitypen
+generalsettings.videomask = Videodateitypen
+generalsettings.index = Index
+generalsettings.ignoredarticles =Ignorierte W\u00f6rter
+generalsettings.shortcuts = Verkn\u00fcpfung (Shortcuts)
+generalsettings.showgettingstarted = Zeige "Erste Schritte" beim Start
+generalsettings.welcomemessage = Willkommensmeldung
+generalsettings.welcometitle = Willkommenstext
+generalsettings.welcomesubtitle = Willkommens-Untertitel
+generalsettings.loginmessage = Login Nachricht
+generalsettings.language = Voreingestellte Sprache
+generalsettings.theme = Voreingestelltes Layout
+
+# advancedSettings.jsp
+advancedsettings.coverartlimit = Cover Limit<br><div class="detail">(0 = Unbegrenzt)</div>
+advancedsettings.downloadlimit = Download Limit (Kbps)<br><div class="detail">(0 = Unbegrenzt)</div>
+advancedsettings.uploadlimit = Upload Limit (Kbps)<br><div class="detail">(0 = Unbegrenzt)</div>
+advancedsettings.streamport = Kein-SSL Stream Port<br><div class="detail">(0 = Deaktiviert)</div>
+advancedsettings.ldapenabled = Erlaube LDAP Authentifizierung
+advancedsettings.ldapsearchfilter = LDAP Suchfilter
+advancedsettings.ldapmanagerdn = LDAP Manager DN<br><div class="detail">(Optional)</div>
+advancedsettings.downsamplecommand = Heruntertaktung (Downsample) Befehl
+advancedsettings.ldapautoshadowing = Automatische Erstellung von Benutzern in {0}
+
+# personalSettings.jsp
+personalsettings.title = Layout Einstellungen f\u00fcr {0}
+personalsettings.language = Sprache
+personalsettings.theme = Layout
+personalsettings.display = Anzeigen
+personalsettings.browse = Browser
+personalsettings.playlist = Playlist
+personalsettings.tracknumber = Song #
+personalsettings.artist = K\u00fcnstler
+personalsettings.album = Album
+personalsettings.genre = Genre
+personalsettings.year = Jahr
+personalsettings.bitrate = Bitrate
+personalsettings.duration = Dauer
+personalsettings.format = Format
+personalsettings.filesize = Dateigr\u00f6\u00dfe
+personalsettings.captioncutoff = Spaltenbreite
+personalsettings.shownowplaying = Zeige was andere h\u00f6ren
+personalsettings.showchat = Zeige Chat Nachrichten
+personalsettings.finalversionnotification = Informiere mich \u00fcber neue Versionen
+personalsettings.betaversionnotification = Informiere mich \u00fcber neue Beta Versionen
+personalsettings.lastfmenabled = Registriere was ich h\u00f6re bei <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm Benutzername
+personalsettings.lastfmpassword = Last.fm Passwort
+personalsettings.avatar.title = Pers\u00f6nliches Bild
+personalsettings.avatar.none = Kein Bild
+personalsettings.avatar.changecustom = Eigenes Bild w\u00e4hlen
+personalsettings.avatar.custom = Eigenes Bild
+personalsettings.nowplayingallowed = Zeige anderen was ich h\u00f6re
+personalsettings.partymode = Party Modus
+
+# avatarUploadResult.jsp
+avataruploadresult.title = W\u00e4hle pers\u00f6nliches Bild
+avataruploadresult.success = Pers\u00f6nliches Bild erfolgreich hochgeladen "{0}".
+avataruploadresult.failure = Hochladen des pers\u00f6nlichen Bildes fehlgeschlagen. Schaue unter <a href="help.view?">log</a> f\u00fcr Details.
+
+# passwordSettings.jsp
+passwordsettings.title = \u00c4ndere Passwort f\u00fcr {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Ordner
+musicfoldersettings.name = Name
+musicfoldersettings.enabled = Aktiviert
+musicfoldersettings.nopath = Bitte einen Ordner angeben.
+musicfoldersettings.add = Musikordner hinzuf\u00fcgen
+
+# networkSettings.jsp
+networksettings.text = Verwenden die die folgenden Einstellungen um den Zugang zu ihrem Subsonic-Server \u00fcber das Internet einzurichten.
+networksettings.portforwardingenabled = Erlaube automatische Konfiguration des Routers f\u00fcr eingehende Verbindungen zu Subsonic (UPnP Port forwarding).
+networksettings.urlredirectionenabled = Greifen sie \u00fcber das Internet auf ihren Server zu, mithilfe einer "leicht-zu-merken" Adresse.
+networksettings.status = Status:
+networksettings.trialexpired = Die Trial Periode endet am {0}. Bitte <b><a href="donate.view?">spenden</a></b> um diese Feature dauerhaft zu aktivieren.
+networksettings.trialnotexpired = Dieses Feature ist verf\u00fcgbar bis zum {0}. Danach m\u00fcssen sie <b><a href="donate.view?">spenden</a></b> um es weiterhin nutzen zu k\u00f6nnen.
+networksettings.portforwardinghelp = Wenn Ihr Router nicht automatisch konfiguriert wird k\u00f6nnen sie dies auch manuell tun. \
+ Folgen sie der Anleitung unter <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Sie m\u00fcssen den Port {0} auf ihren Computer weiterleiten um den Subsonic Server zu erreichen.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Name
+transcodingsettings.sourceformat = Konvertieren von
+transcodingsettings.targetformat = Konvertieren zu
+transcodingsettings.step1 = Schritt 1
+transcodingsettings.step2 = Schritt 2
+transcodingsettings.step3 = Schritt 3
+transcodingsettings.defaultactive = Standard
+transcodingsettings.enabled = Aktiviert
+transcodingsettings.add = Transcoding hinzuf\u00fcgen
+transcodingsettings.recommended = Empfohlene Konfiguratiom
+transcodingsettings.noname = Bitte einen Namen eingeben.
+transcodingsettings.nosourceformat = Bitte das Format angeben von dem konvertiert wird.
+transcodingsettings.notargetformat = Bitte das Format angeben in das konvertiert wird.
+transcodingsettings.nostep1 = Bitte mindestens einen Transcoding Schritt angeben.
+transcodingsettings.info = <p class="Detail">(%s = Die Datei die transcodiert wird, %b = Maximale Bitrate des Players)</p> \
+ <p>Beim transcodieren wird eine Datei von einem Format in ein anderes umgewandelt. {1}''s Transcoding \
+ Engine erlaubt Streaming von Dateien, die normalerweise nicht streambar sind. Es wird beim Abspielen transcodiert und braucht keinen \
+ Festplattenspeicher.<p/> \
+ <p>Das Transcoding wird von Drittanbieter-Kommandozeilenprogrammen \u00fcbernommen, welche in {0} installiert sein m\u00fcssen. \
+ Ein Transcoding-Pack f\u00fcr Windows \
+ ist <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>hier</b></a>. Du kannst deinen eigenen Transcoder verwenden, wenn er \
+ folgende Funktionen erf\u00fcllt: \
+ <ul> \
+ <li>Er muss ein Kommandozeilen-Interface haben.</li> \
+ <li>Er muss die Ausgabe an stdout senden k\u00f6nnen.</li> \
+ <li>Wenn er in Schritt 2 und 3 gebraucht wird, muss er den Input von stdin lesen k\u00f6nnen.</li> \
+ </ul> \
+ </p> \
+ <p> Beachte, dass transcoding f\u00fcr jeden Player in den Einstellungen einzeln aktiviert werden muss.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradiosettings.homepageurl = Homepage
+internetradiosettings.name = Name
+internetradiosettings.enabled = Aktiviert
+internetradiosettings.add = Internet TV/Radio hinzuf\u00fcgen
+internetradiosettings.nourl = Bitte eine URL angeben.
+internetradiosettings.noname = Bitte einen Namen angeben.
+
+# podcastSettings.jsp
+podcastsettings.update = Suche nach neuen Episoden
+podcastsettings.keep = Erhalte
+podcastsettings.keep.all = Alle Episoden
+podcastsettings.keep.one = Neuesten Episoden
+podcastsettings.keep.many = Letzte {0} Episoden
+podcastsettings.download = Wenn neue Episoden verf\u00fcgbar sind
+podcastsettings.download.all = Downloade alle
+podcastsettings.download.one = Downloade die neuesten einzeln
+podcastsettings.download.many = Downloade letzte {0} Episoden
+podcastsettings.download.none = Nichts machen
+podcastsettings.interval.manually = Manuell
+podcastsettings.interval.hourly = St\u00fcndlich
+podcastsettings.interval.daily = T\u00e4glich
+podcastsettings.interval.weekly = W\u00f6chentlich
+podcastsettings.folder = Speichere Podcasts unter
+
+# playerSettings.jsp
+playersettings.noplayers = Keine Player gefunden.
+playersettings.type = Typ
+playersettings.lastseen = Zuletzt gesehen
+playersettings.title = Player ausw\u00e4hlen
+playersettings.name = Player Name
+playersettings.coverartsize = Cover Gr\u00f6\u00dfe
+playersettings.maxbitrate = Maximale Bitrate
+playersettings.nolame = <em>Hinweis:</em> LAME ist nicht installiert.<br>Klicke auf Hilfe f\u00fcr weitere Informationen.
+playersettings.autocontrol = Kontrolliere Player automatisch
+playersettings.dynamicip = Player hat eine dynamische IP Adressse
+playersettings.transcodings = Aktiviere Transcoding
+playersettings.ok = Speichern
+playersettings.forget = L\u00f6sche Player
+playersettings.clone = Dupliziere Player
+playersettings.technology.web.title = Web Player
+playersettings.technology.external.title = Externer Player
+playersettings.technology.external_with_playlist.title = Externer Player mit Playlist
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Spiele Musik direkt im Browser mit integrierten Flash-Player.
+playersettings.technology.external.text = Spielt Musik in ihrem Lieblingsplayer ab, wie etwa Winamp oder Mediaplayer.
+playersettings.technology.external_with_playlist.text = Wie oben, aber die Playlist wird durch den Player verwaltet statt dem Subsonic-Server. In diesem Modus ist das \u00fcberspringen von Songs m\u00f6glich.
+playersettings.technology.jukebox.text = Spielt die Musik direkt auf dem Audioger\u00e4t des Subsonic-Servers ab. (Nur f\u00fcr autorisierte User).
+
+# shareSettings.jsp
+sharesettings.name = Name
+sharesettings.owner = Geteilt von
+sharesettings.description = Beschreibung
+sharesettings.visits = Besucher
+sharesettings.lastvisited = Letzte Besucher
+sharesettings.expires = L\u00e4uft ab am
+sharesettings.files = Freigegebene Dateien
+sharesettings.expirein = Endet in
+sharesettings.expirein.week = 1W
+sharesettings.expirein.month = 1M
+sharesettings.expirein.year = 1J
+sharesettings.expirein.never = Niemals
+
+# share.jsp
+share.title = Teilen
+share.warning = <h2>WICHTIGER HINWEIS!</h2><p>Play fair &ndash; Teile in keinster Weise urheberrechtliches gesch\u00fctztes Material da es gegen das Gesetz verst\u00f6sst.</p>
+share.facebook = Teile auf Facebook
+share.twitter = Teile auf Twitter
+share.link = Oder teile es mit jemanden indem du diesen Link weitergibst: <a href="{0}" target="_blank">{0}</a>
+share.disabled = Um Musik mit jemanden teilen zu k\u00f6nnen musst du erst deine eigene <em>subsonic.org</em> Addresse registrieren.<br> \
+ Bitte wechsle zu <a href="networkSettings.view"><b>Settings &gt; Network</b></a> (Administratorrechte werden ben\u00f6tigt).
+share.manage = Meine geteilten Medien verwalten
+
+
+# userSettings.jsp
+usersettings.title = Benutzer ausw\u00e4hlen
+usersettings.newuser = Neuer Benutzer
+usersettings.admin = Benutzer ist Administrator
+usersettings.settings = Benutzer darf Einstellungen und Passw\u00f6rter \u00e4ndern
+usersettings.stream = Benutzer darf Dateien abspielen.
+usersettings.jukebox = Benutzer darf Dateien im Jukebox Modus abspielen.
+usersettings.download = Benutzer darf Dateien herunterladen
+usersettings.upload = Benutzer darf Dateien hochladen
+usersettings.share = Benutzer darf Dateien mit anderen teilen
+
+usersettings.playlist= Benutzer darf Playlists erstellen und l\u00f6schen
+usersettings.coverart = Benutzer darf Covers und Tags \u00e4ndern
+usersettings.comment= Benutzer darf Kommentare und Bewertungen erstellen
+usersettings.podcast = Benutzer darf Podcasts administrieren.
+usersettings.username = Benutzername
+usersettings.changepassword = Passwort \u00e4ndern
+usersettings.password = Passwort
+usersettings.newpassword = Neues Passwort
+usersettings.confirmpassword = Passwort best\u00e4tigen
+usersettings.delete = L\u00f6sche diesen Benutzer
+usersettings.nousername = Benutzername fehlt.
+usersettings.useralreadyexists = Benutzer existiert schon.
+usersettings.nopassword = Passwort wird ben\u00f6tigt.
+usersettings.wrongpassword = Passw\u00f6rter stimmen nich \u00fcberein.
+usersettings.ldapdisabled = LDAP Authentifizierung ist nicht aktiviert. Siehe Erweiterte Einstellungen.
+usersettings.passwordnotsupportedforldap = Konnte Passwort nicht einstellen oder \u00e4ndern f\u00fcr die LDAP-Authentifizierung des Benutzers.
+usersettings.ok = Passwort erfolgreich ge\u00e4ndert f\u00fcr {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Nie
+musicfoldersettings.interval.one = Jeden Tag
+musicfoldersettings.interval.many = Jeden {0} Tag
+musicfoldersettings.hour = um {0}:00
+
+# coverArtSettings.jsp
+coverartsettings.auto = Downloade automatisch fehlende Cover nachdem der Suchindex erneuert wurde.
+coverartsettings.manual = Fehlende Cover downloaden.
+coverartsettings.missing = In {0} von {1} Alben fehlen die Cover.
+coverartsettings.running = Downloade Cover. Dies kann mehrere Minuten dauern, jeh nach Gr\u00f6sse \
+ deiner Musikdatenbank.
+coverartsettings.albumList = Zeige fehlende Cover.
+
+# main.jsp
+main.up = Hoch
+main.playall = Spiele alle
+main.playrandom = Spiele gemischt
+main.addall = Alle hinzuf\u00fcgen
+main.tags = Tags editieren
+main.playcount = {0} mal gespielt.
+main.lastplayed = Zuletzt am {0} gespielt.
+main.comment = Kommentar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Fetter Text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Zeilenumbruch</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Kursiver Text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>Neuer Absatz</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>Listeneintrag </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Nummerierter Listeneintrag</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Name als Link</td></tr>\
+ </table>
+main.sharealbum = Teilen
+main.more = Mehr Aktionen...
+main.more.selection = Ausgew\u00e4hlte Songs
+main.more.share = Teilen
+main.donate = <a href="{0}" style="text-decoration:underline">Spenden</a> f\u00fcr {1}!<br>(damit wird diese Anzeige entfernt)
+main.nowplaying = Es l\u00e4uft gerade
+main.lyrics = Lyrics
+main.minutesago = Minuten vergangen
+main.chat = Chat Nachrichten
+main.message = Schreibe eine Nachricht
+main.clearchat = Leere Chatfenster
+
+
+# rating.jsp
+rating.rating = Bewertung
+rating.clearrating = L\u00f6sche Bewertung
+
+# coverArt.jsp
+coverart.change = \u00c4ndern
+coverart.zoom = Vergr\u00f6ssern
+
+# allmusic.jsp
+allmusic.text = Suche Album <em>{0}</em> auf allmusic.com - Bitte warten.
+
+# changeCoverArt.jsp
+changecoverart.title = \u00c4ndere Cover
+changecoverart.address = Adresse des Bildes
+changecoverart.artist = K\u00fcnstler
+changecoverart.album = Album
+changecoverart.wait = Bitte warten...
+changecoverart.noimagesfound = Keine Bilder gefunden.
+changecoverart.error = Download des Bildes fehlgeschlagen.
+changecoverart.success = Bild wurde erfolgreich heruntergeladen.
+changecoverart.searchdiscogs = Durchsuche Discogs
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Cover konnte nicht ge\u00e4ndert werden:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Tags bearbeiten
+edittags.file = Datei
+edittags.track = Song
+edittags.songtitle = Titel
+edittags.artist = K\u00fcnstler
+edittags.album = Album
+edittags.year = Jahr
+edittags.status = Status
+edittags.suggest = Vorschlagen
+edittags.reset = Zur\u00fccksetzen
+edittags.set = Setzen
+edittags.working = Arbeitet
+edittags.updated = Erneuert
+edittags.skipped = \u00dcbersprungen
+edittags.error = Fehler
+
+# donate.jsp
+donate.title = Spende
+donate.invalidlicense = Falscher Lizenz Key.
+donate.register.license = Lizenz
+donate.amount = Spenden {0}
+
+donate.textbefore = <p>Danke das sie eine Spende f\u00fcr das {0} Projekt in Betracht gezogen haben! \
+ Spender erhalten Zugang zu Premium-Funktionen wie:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> f\u00fcr Android, iPhone and Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> f\u00fcr PlayBook, Roku, Mac, Chrome und mehr*.</li> \
+ <li>Video Streaming.</li> \
+ <li>Deine pers\u00f6nliche Serveradresse: <em>DeineName</em>.subsonic.org (siehe <a href="networkSettings.view">Einstellungen &gt; Netzwerk</a>).</li> \
+ <li>Teile deine Medien auf Facebook, Twitter, Google+.</li> \
+ <li>Keine Werbung auf der Benutzeroberfl\u00e4che.</li> \
+ <li>Zuk\u00fcnftig ver\u00f6ffentlichte Features.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Einige Anwendungen werden von von Drittanbieter-Entwicklern vertrieben.</p>\
+ <p>Als Spender erhalten sie eine Lizenzschl\u00fcssel welcher g\u00fcltig f\u00fcr die private, nicht-kommerzielle Nutzung, und f\u00fcr alle zuk\u00fcnftigen Versionen von {0} ist. F\u00fcr die kommerzielle Nutzung <a href="mailto:subsonic_donation@activeobjects.no">kontaktieren</a> sie uns bitte f\u00fcr weitere Lizenz Optionen.</p> \
+ <p>Die vorgeschlagene H\u00f6he der Spende ist <b>&euro;20</b>, aber sie k\u00f6nnen auch einen beliebigen Betrag w\u00e4hlen.</p>
+donate.textafter = <p>Klicken Sie auf eine Schaltfl\u00e4che, um zu PayPal zu wechseln, wo Sie unter Verwendung einer Kreditkarte \
+ oder mit Hilfe ihres Paypal Kontos (falls sie eines besitzen) bezahlen k\u00f6nnen. Sie erhalten dann den Lizenzschl\u00fcssel innerhalb weniger Minuten.</p> \
+ <p>Wenn sie noch Fragen haben, senden sie bitte eine Email an: \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+
+donate.licensed = Diese Kopie von {2} wurde am {0} {1} lizenziert. Vielen Dank f\u00fcr Ihre Unterst\u00fctzung!
+donate.register = Nachdem sie ihren Lizenzschl\u00fcssel erhalten haben, aktivieren sie ihn hier in den darunterliegenden Feldern.
+donate.resend = Bereits eine Lizenz erhalten, aber verloren? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Erneut senden</a>.
+donate.register.email = Email
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast Empf\u00e4nger
+podcastreceiver.expandall = Zeige Episoden
+podcastreceiver.collapseall = Verstecke Episoden
+podcastreceiver.status.new = Neu
+podcastreceiver.status.downloading = Am herunterladen
+podcastreceiver.status.completed = Fertig
+podcastreceiver.status.error = Fehler
+podcastreceiver.status.deleted = Gel\u00f6scht
+podcastreceiver.status.skipped = Abgebrochen
+podcastreceiver.downloadselected= Ausgew\u00e4hlte downloaden
+podcastreceiver.deleteselected= L\u00f6sche ausgew\u00e4hlte
+podcastreceiver.confirmdelete= Ausgew\u00c4hlte Podcasts wirklich l\u00f6schen?
+podcastreceiver.check = Suche nach neuen Episoden
+podcastreceiver.refresh = Seite neu laden
+podcastreceiver.settings = Podcast Einstellungen
+podcastreceiver.subscribe = Abonniere Podcast
+
+# lyrics.jsp
+lyrics.title = Lyrics
+lyrics.artist = K\u00fcnstler
+lyrics.song = Song
+lyrics.search = Suchen
+lyrics.wait = Suche nach lyrics, bitte warten...
+lyrics.courtesy = (Lyrics by <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Keine Lyrics gefunden.
+
+# helpPopup.jsp
+helppopup.title = {0} Hilfe
+helppopup.cover.title = Cover Gr\u00f6\u00dfe
+helppopup.cover.text = <p>Hier kannst du die Gr\u00f6\u00dfe des Covers einstellen, mit der Option es komplett zu deaktivieren.</p>
+helppopup.transcode.title = Maximale bitrate
+helppopup.transcode.text = <p>Wenn du eine konstante Bitrate hast, solltest du ein h\u00f6heres Limit f\u00fcr den Stream setzen. \
+ Zum Beispiel, wenn deine originalen mp3 Dateien eine Bitrate von 256 kbps (kilobits per second / kilobits pro Sekunde) haben, und du die Bitrate auf \
+ 128 kbps \u00e4nderst wird {0} die Songs automatisch auf 128 kbps umkodieren.</p> \
+ <p>Dazu muss LAME installiert sein. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ ist ein open source mp3 encoder. Du kannst ihn <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">hier runterladen</a>. \
+ Du musst ihn unter SUBSONIC_HOME/transcode installieren, oder in einem Verzeichnis, dass in deiner PATH environment variable vorkommt.</p>
+helppopup.playlistfolder.title = Playlist Ordner
+helppopup.playlistfolder.text = <p>Hier gibst du den Ordner an, indem sich die Playlists befinden.</p>
+helppopup.musicmask.title = Musikdateitypen
+helppopup.musicmask.text = <p>Hier kannst du einstellen, welche Typen von Dateien in deinem Musikordner von {0} verwendet werden.</p>
+helppopup.videomask.title = Videodateitypen
+helppopup.videomask.text = <p>Hier kannst festlegen welcher Dateityp als Video erkannt werden soll.</p>
+helppopup.coverartmask.title = Coverdateitypen
+helppopup.downsamplecommand.text = <p>Erlaubt eigenen Befehl f\u00fcr Neuberechnung einer niedrigeren Bitrate.</p>\
+ <p>(%s = Die Datei wird neu berechnet zu, %b = Max. Bitrate des Players)</p>
+helppopup.coverartmask.text = <p>Hier kannst du angeben, welche Typen von Dateien f\u00fcr Album Covers verwendet werden.</p>
+helppopup.index.title = Index
+helppopup.index.text = <p>Hier kannst du angeben, wie der Index (ganz oben) aussehen soll. Dateien und Verzeichnisse \
+ direkt im root Musik Verzeichniss (also ganz "oben") k\u00f6nnen durch diesen Index einfach erreicht werden.</p> \
+ <p>Es ist eine Leerraum getrennte Liste von Eintr\u00e4gen. Normalerweise ist ein Eintrag ein einzelner Buchstabe, \
+ aber du kannst auch mehrere verwenden. Zum Beispiel, der Eintrag <em>The</em> zeigt auf alle Dateien und Verzeichnisse \
+ die mit "The" starten.</p> \
+ <p>Du kannst auch einen Eintrag erstellen, der eine Gruppe von Buchstaben in alphabetischer Reihenfolge enth\u00e4lt. Zum Beispiel, der Eintrag \
+ <em>A-E(ABCDE)</em> wird als <em>A-E</em> angezeigt und zeigt auf alle Dateien und Verzeichnisse die mit \
+ A, B, C, D or E beginnen. Das kann f\u00fcr wenig genutzte Anfangsbuchstaben sinnvoll sein (wie X, Y und Z), oder \
+ f\u00fcr Gruppierung f\u00fcr Buchstaben (wie A, \u00c0 und \u00c1)</p> \
+ <p>Dateien und Verzeichnisse die keinen Buchstaben zugeordnet werden k\u00f6nnen, sind unter "#" zu finden.</p>
+helppopup.ignoredarticles.title = W\u00f6rter die ignoriert werden
+helppopup.ignoredarticles.text = <p>Hier kannst du W\u00f6rter angeben (wie "The") welche beim Index erstellen ignoriert werden. Es wird dann der Anfangsbuchstabe danach benutzt, z.B. "The Beatles" unter "B".</p>
+helppopup.shortcuts.title = Verkn\u00fcpfungen (Shortcuts)
+helppopup.shortcuts.text = <p>Eine durch Leerzeichen getrennte Liste von Gruppenw\u00f6rtern um Verkn\u00fcpfungen zwischen Top-Ordnern zu erstellen. zum Beispiel:</p> \
+ <p><em>Neue "Sound Tracks"</em></p>
+helppopup.language.title = Sprache
+helppopup.language.text = <p>Hier kannst du die Sprache ausw\u00e4hlen.</p>
+helppopup.visibility.title = Anzeige
+helppopup.visibility.text = <p>Hier kannst du anzeigen, welche Details bei einem Song angezeigt werden soll und auch die maximale angezeigte L\u00e4nge des ganzen Titels.</p>
+
+helppopup.theme.title = Layout
+helppopup.theme.text = <p>Hier kannst du das Layout angeben. Ein Layout gibt die Farben, die Bilder und die Schrift von {0} an.</p>
+helppopup.welcomemessage.title = Willkommensmeldung
+helppopup.welcomemessage.text = <p>Die Meldung die auf der Homepage angezeigt wird.</p>
+helppopup.loginmessage.text = <p>Die Nachricht die auf der Loginseite angezeigt wird.</p>
+helppopup.coverartlimit.title = Cover Limit
+helppopup.coverartlimit.text = <p>Die Maximale Anzahl von Covers, die auf einer Seite angezeigt werden.</p>
+helppopup.downloadlimit.title = Download Limit
+helppopup.downloadlimit.text = <p>Das obere Limit an Bandbreite, dass f\u00fcr einen Download verwendet wird.</p>
+helppopup.uploadlimit.title = Upload Limit
+helppopup.uploadlimit.text = <p>Das obere Limit an Bandbreite, dass f\u00fcr einen Upload verwendet wird.</p>
+helppopup.streamport.title = Kein-SSL Stream Port
+helppopup.streamport.text = <p>Diese Option ist nur relevant, wenn du {0} mit einem Server mit SSL (HTTPS) betreibst.</p><p>Einige Player \
+ (wie Winamp) unterst\u00fctzen das Streaming \u00fcber SSL nicht. Gib hier die regul\u00e4re Portnummer von http (normalerweise 80 \
+ oder 8080) wenn du den Stream nicht \u00fcber SSL ausgeben willst. Beachte, dass Streams nicht verschl\u00fcsselt werden.</p>
+helppopup.ldap.text = <p>Benutzer k\u00f6nnen von einem externen LDAP-Server authentifiziert werden. (inklusive Windows Active Directory). \
+ Wenn LDAP akiviert ist, sind Benutzer eingeloggt bei {0}, der Username und das Passwort werden vom externen Server gecheckt, nicht von {0} selbst.</p>
+
+helppopup.playername.title = Playername
+helppopup.playername.text = <p>Hier kannst du einen einfach zu merkenden Playernamen angeben, wie "Arbeitszimmer" oder "Wohnzimmer".</p>
+helppopup.autocontrol.title = Player automatisch kontrollieren
+helppopup.autocontrol.text = <p>Wenn du diese Option aktiviert hast, startet {0} den Player automatisch wenn du auf "Play" dr\u00fcckst. \
+ Sonst musst du den Player selbst starten.</p>
+helppopup.dynamicip.title = Dynamische IP Adresse
+helppopup.dynamicip.text = <p>Verwende diese Option, wenn der Player eine dynamische IP Adresse hat.</p>
+helppopup.partymode.title = Party Modus
+helppopup.partymode.text = <p>Wenn der Party Modus aktviert ist wird die Benutzeroberfl\u00e4che vereinfacht und ist f\u00fcr nicht-erfahrene Benutzer leichter zu bedienen. \
+ Vor allem wird die zuf\u00e4llige Anordnung der Songs in der Playlist dadurch vermieden.</p>
+
+# wap/index.jsp
+wap.index.missing = Keine Musik gefunden
+wap.index.playlist = Playlist
+wap.index.search = Suche
+wap.index.settings = Einstellungen
+
+# wap/browse.jsp
+wap.browse.playone = Spiele Song
+wap.browse.playall = Spiele alles
+wap.browse.addone = Song hinzuf\u00fcgen
+wap.browse.addall = Alles hinzuf\u00fcgen
+wap.browse.downloadone = Downloade Song
+wap.browse.downloadall = Downloade Alle
+
+# wap/playlist.jsp
+wap.playlist.title = Playlist
+wap.playlist.noplayer = Kein Player verbunden
+wap.playlist.clear = Leeren
+wap.playlist.load = Laden
+wap.playlist.random = Durcheinander
+wap.playlist.play = Auf Handy abspielen
+
+# wap/search.jsp
+wap.search.title = Suchen
+
+# wap/searchResult.jsp
+wap.searchresult.index = Der Suchindex wird gerade erstellt. Bitte probiere es sp\u00e4ter noch einmal.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Player ausw\u00e4hlen
+wap.settings.allplayers = Alle
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_el.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_el.properties
new file mode 100644
index 00000000..8f599529
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_el.properties
@@ -0,0 +1,698 @@
+# Greek localization.
+# Author: Constantine Samaklis
+#
+
+common.home = \u0391\u03c1\u03c7\u03b9\u03ba\u03ae
+common.back = \u03a0\u03af\u03c3\u03c9
+common.help = \u0392\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1
+common.play = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae
+common.add = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7
+common.download = Download
+common.close = \u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf
+common.refresh = \u0391\u03bd\u03b1\u03bd\u03ad\u03c9\u03c3\u03b7
+common.next = \u0395\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf
+common.previous = \u03a0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf
+common.more = \u03a0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1
+common.ok = OK
+common.cancel = \u0386\u03ba\u03c5\u03c1\u03bf
+common.save = \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7
+common.create = \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1
+common.delete = \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae
+common.unknown = (\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf)
+common.default = (\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)
+
+# login.jsp
+login.username = \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2
+login.password = \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2
+login.login = \u0395\u03af\u03c3\u03bf\u03b4\u03bf\u03c2
+login.remember = \u0391\u03c0\u03bf\u03bc\u03bd\u03b7\u03bc\u03cc\u03bd\u03b5\u03c5\u03c3\u03b7 \u039f\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7
+login.logout = \u0395\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af.
+login.error = \u039b\u03ac\u03b8\u03bf\u03c2 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd.
+login.insecure = {0} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9<br>\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc "admin", \u03ae \u03c0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 <a href="login.view?user=admin&amp;password=admin">here</a>. \u039c\u03b5\u03c4\u03ac \u03b1\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b1\u03bc\u03ad\u03c3\u03c9\u03c2.
+
+# accessDenied.jsp
+accessDenied.title = \u0391\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.
+accessDenied.text = \u03a3\u03c5\u03b3\u03bd\u03ce\u03bc\u03b7, \u03b4\u03b5\u03bd \u03b5\u03af\u03c3\u03c4\u03b5 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03bd\u03b1 \u03b4\u03b9\u03b5\u03ba\u03c0\u03b5\u03c1\u03b1\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.
+
+# top.jsp
+top.home = \u0391\u03c1\u03c7\u03b9\u03ba\u03ae
+top.now_playing = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03c4\u03b1\u03b9 \u03c4\u03ce\u03c1\u03b1
+top.settings = \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2
+top.status = \u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7
+top.podcast = Podcast
+top.more = \u03a0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1
+top.help = \u03a0\u03b5\u03c1\u03af
+top.search = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+top.upgrade = <b>Note!</b> \u039c\u03af\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7.<br>Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u03b5\u03b4\u03ce</a>.
+top.missing = \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03b9 \u03bc\u03b5 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ac \u03b1\u03c1\u03c7\u03b5\u03af\u03b1. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b1\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2.
+top.logout = \u0391\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 {0}
+
+# left.jsp
+left.statistics = {0}&nbsp; \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b5\u03c2 <br>\
+ {1}&nbsp; \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2 <br>\
+ {2}&nbsp; \u03a4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1 <br>\
+ {3} (&#126; {4} \u03ce\u03c1\u03b5\u03c2)
+left.shortcut = \u03a3\u03c5\u03bd\u03c4\u03bf\u03bc\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2
+left.radio = \u0394\u03b9\u03b1\u03b4\u03b9\u03ba\u03c4\u03c5\u03b1\u03ba\u03ae \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7/\u03c1\u03b1\u03b4\u03b9\u03cc\u03c6\u03c9\u03bd\u03bf
+left.allfolders = \u038c\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03b9
+
+# playlist.jsp
+playlist.stop = \u03a0\u03b1\u03cd\u03c3\u03b7
+playlist.start = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae
+playlist.confirmclear = \u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03cc\u03bd\u03c4\u03c9\u03c2 \u03bd\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03b5\u03ba\u03ba\u03b1\u03b8\u03ac\u03c1\u03b9\u03c3\u03b7 \u03c4\u03b9\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2;
+playlist.clear = \u0395\u03ba\u03ba\u03b1\u03b8\u03ac\u03c1\u03b9\u03c3\u03b7
+playlist.shuffle = \u0391\u03bd\u03b1\u03ba\u03ac\u03c4\u03b5\u03bc\u03b1
+playlist.repeat_on = \u0395\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae
+playlist.repeat_off = \u0395\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7
+playlist.undo = \u0391\u03bd\u03b1\u03af\u03c1\u03b5\u03c3\u03b7
+playlist.settings = \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2
+playlist.more = \u03a0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2...
+playlist.more.playlist = \u039b\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playlist.more.sortbytrack = \u03a4\u03b1\u03be\u03b9\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac \u03c4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9
+playlist.more.sortbyartist = \u03a4\u03b1\u03be\u03b9\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac \u03ba\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7
+playlist.more.sortbyalbum = \u03a4\u03b1\u03be\u03b9\u03bd\u03cc\u03bc\u03b7\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae
+playlist.more.selection = \u0395\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b1 \u03c4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1
+playlist.more.selectall = \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03cc\u03bb\u03c9\u03bd
+playlist.more.selectnone = \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03ba\u03b1\u03bd\u03b5\u03bd\u03cc\u03c2
+playlist.getflash = \u039a\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Flash player
+playlist.load = \u03a6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7
+playlist.save = \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7
+playlist.append = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playlist.remove = \u0391\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7
+playlist.up = \u03a0\u03ac\u03bd\u03c9
+playlist.down = \u039a\u03ac\u03c4\u03c9
+playlist.empty = \u0397 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03b4\u03b5\u03b9\u03b1
+
+# videoPlayer.jsp
+videoPlayer.getflash = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Flash Player
+videoPlayer.popout = \u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03c3\u03b5 \u03bd\u03ad\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf
+
+# status.jsp
+status.title = \u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7
+status.type = \u03a4\u03cd\u03c0\u03bf\u03c2
+status.stream = \u03a1\u03bf\u03ae
+status.download = \u03a6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7
+status.upload = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1
+status.player = \u039b\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+status.user = \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2
+status.current = \u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03c1\u03c7\u03b5\u03af\u03bf
+status.transmitted = \u0394\u03b9\u03b1\u03b2\u03b9\u03b2\u03b1\u03c3\u03bc\u03ad\u03bd\u03b1
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+search.query = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2, \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae \u03ae \u03c4\u03af\u03c4\u03bb\u03bf\u03c2 \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03bf\u03cd
+search.search = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+search.index = \u03a4\u03bf \u03b5\u03c5\u03c1\u03b5\u03c4\u03ae\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.
+search.hits.none = \u0394\u03ad\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1.
+search.hits.more = \u03a0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1
+search.hits.artists = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b5\u03c2
+search.hits.albums = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2
+search.hits.songs = \u03a4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1
+
+# gettingStarted.jsp
+gettingStarted.title = \u039e\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03b5\u03b4\u03ce
+gettingStarted.text = <p>\u039a\u03b1\u03bb\u03ce\u03c2 \u03ae\u03c1\u03b8\u03b1\u03c4\u03b5 \u03c3\u03c4\u03bf Subsonic! \u0398\u03b1 \u03b5\u03af\u03c3\u03c4\u03b5 \u03ad\u03c4\u03bf\u03b9\u03bc\u03bf\u03b9 \u03c3\u03b5 \u03bb\u03af\u03b3\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1, \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2<br> \
+ \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af "\u0388\u03bd\u03b1\u03c1\u03be\u03b7" \u03c3\u03c4\u03bf \u03c0\u03bb\u03b1\u03af\u03c3\u03b9\u03bf \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03ad\u03bb\u03b8\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03c0\u03b1\u03c1\u03bf\u03cd\u03c3\u03b1 \u03bf\u03b8\u03cc\u03bd\u03b7.</p> \
+ <p>\u0393\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2, \u03ba\u03bf\u03b9\u03c4\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd<a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Getting started</b></a> \u03bf\u03b4\u03b7\u03b3\u03cc.</p>
+gettingStarted.step1.title = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae.
+gettingStarted.step1.text = \u0391\u03c3\u03c6\u03b1\u03bb\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2, \u03b1\u03bb\u03bb\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae. \
+ \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03bf\u03c5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2 \u03bc\u03b5 \u03b4\u03b9\u03ac\u03c6\u03bf\u03c1\u03b1 \u03c0\u03c1\u03bf\u03bd\u03cc\u03bc\u03b9\u03b1.
+gettingStarted.step2.title = \u03a3\u03cd\u03c3\u03c4\u03b1\u03c3\u03b7 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ce\u03bd \u03c6\u03b1\u03ba\u03ad\u03bb\u03c9\u03bd.
+gettingStarted.step2.text = \u03a5\u03c0\u03bf\u03b4\u03b5\u03af\u03be\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf Subsonic \u03c0\u03bf\u03c5 \u03ba\u03c1\u03b1\u03c4\u03ac\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c3\u03b1\u03c2 \u03c6\u03b1\u03ba\u03ad\u03bb\u03bf\u03c5\u03c2.
+gettingStarted.step3.title = \u039c\u03bf\u03c1\u03c6\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5.
+gettingStarted.step3.text = \u039c\u03b5\u03c1\u03b9\u03ba\u03bf\u03af \u03c7\u03c1\u03ae\u03c3\u03b9\u03bc\u03bf\u03b9 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf\u03b9 \u03b5\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03bb\u03b1\u03cd\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03c3\u03b1\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b1 \u03bc\u03ad\u03c3\u03c9 \u03af\u03bd\u03c4\u03b5\u03c1\u03bd\u03b5\u03c4, \
+ \u03ae \u03bc\u03bf\u03b9\u03c1\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03bf\u03b9\u03ba\u03bf\u03b3\u03ad\u03bd\u03b5\u03b9\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03c5\u03c2 \u03c6\u03af\u03bb\u03bf\u03c5\u03c2 \u03c3\u03b1\u03c2. \u03a0\u03c1\u03bf\u03bc\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae \u03c3\u03b1\u03c2 <b><em>\u03c4\u03bf_\u03cc\u03bd\u03bf\u03bc\u03b1_\u03c3\u03b1\u03c2</em>.subsonic.org</b> \
+ \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7.
+gettingStarted.hide = \u039d\u03b1 \u03bc\u03b7\u03bd \u03c6\u03b1\u03bd\u03b5\u03af \u03be\u03b1\u03bd\u03ac \u03b1\u03c5\u03c4\u03cc.
+gettingStarted.hidealert = \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03b8\u03cc\u03bd\u03b7, \u03c0\u03b7\u03b3\u03b1\u03af\u03bd\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u0393\u03b5\u03bd\u03b9\u03ba\u03ac.
+
+# home.jsp
+home.random.title = \u03a4\u03c5\u03c7\u03b1\u03af\u03b1
+home.newest.title = \u03a0\u03c1\u03bf\u03c3\u03b8\u03b5\u03bc\u03ad\u03bd\u03b1 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1
+home.highest.title = \u03a0\u03b9\u03bf \u03c5\u03c8\u03b7\u03bb\u03cc\u03b2\u03b1\u03b8\u03bc\u03b1
+home.frequent.title = \u03a0\u03b9\u03bf \u03c3\u03c5\u03c7\u03bd\u03ac
+home.recent.title = \u03a0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1
+home.users.title = \u03a7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2
+home.random.text = \u03a4\u03c5\u03c7\u03b1\u03af\u03b1
+home.newest.text = \u03a4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b5\u03c2 \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2
+home.highest.text = \u03a0\u03b9\u03bf \u03c5\u03c8\u03b7\u03bb\u03cc\u03b2\u03b1\u03b8\u03bc\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2
+home.frequent.text = \u03a0\u03b9\u03bf \u03c3\u03c5\u03c7\u03bd\u03ac \u03c0\u03b1\u03b9\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2
+home.recent.text = \u03a4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b5\u03c2 \u03c0\u03b1\u03b9\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2
+home.users.text = \u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7
+home.scan = \u039f \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03cc\u03c2 \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03c4\u03b1\u03b9. \u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03b1\u03ba\u03cc\u03bc\u03b1.
+home.listsize = {0} \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b1\u03bd\u03ac \u03c3\u03b5\u03bb\u03af\u03b4\u03b1
+home.albums = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ad\u03c2 {0} - {1}
+home.playcount = \u03a0\u03b1\u03b9\u03b3\u03bc\u03ad\u03bd\u03b1 {0} \u03c4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1
+home.lastplayed = \u03a0\u03b1\u03b9\u03b3\u03bc\u03ad\u03bd\u03b1 {0}
+home.created = \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03bc\u03ad\u03bd\u03b1 {0}
+home.chart.total = \u03a3\u03cd\u03bd\u03bf\u03bb\u03bf (MB)
+home.chart.stream = \u03a1\u03bf\u03ae \u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd (MB)
+home.chart.download = \u039a\u03b1\u03c4\u03b5\u03b2\u03b1\u03c3\u03bc\u03ad\u03bd\u03b1 (MB)
+home.chart.upload = \u0391\u03bd\u03b5\u03b2\u03b1\u03c3\u03bc\u03ad\u03bd\u03b1 (MB)
+
+# more.jsp
+more.title = \u03a0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1
+more.random.title = \u03a4\u03c5\u03c7\u03b1\u03af\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+more.random.text = \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03c5\u03c7\u03b1\u03af\u03b1\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+more.random.songs = {0} \u03c4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1
+more.random.auto = \u03a0\u03b1\u03af\u03be\u03b5 \u03b5\u03c0\u03b9\u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1 \u03c4\u03c5\u03c7\u03b1\u03af\u03b1 \u03c4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac \u03c4\u03bf \u03c4\u03ad\u03bb\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2.
+more.random.ok = OK
+more.random.genre = \u03b1\u03c0\u03cc \u03b5\u03af\u03b4\u03bf\u03c2
+more.random.anygenre = \u039f\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5
+more.random.year = \u03ba\u03b1\u03b9 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ac
+more.random.anyyear = \u039f\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5
+more.random.folder = \u03c3\u03c4\u03bf\u03bd \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf
+more.random.anyfolder = \u039f\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5
+more.apps.title = Subsonic \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">\u039f\u03b9 Subsonic \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2</a> \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03b3\u03b9\u03b1 <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone</b> and <b>AIR</b>.</p>
+more.mobile.title = \u039a\u03b9\u03bd\u03b7\u03c4\u03cc \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf
+more.mobile.text = <p>\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03ba\u03bf\u03bd\u03c4\u03c1\u03bf\u03bb\u03ac\u03c1\u03b5\u03c4\u03b5 {0} \u03b1\u03c0\u03cc \u03ba\u03ac\u03b8\u03b5 \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf \u03ae PDA \u03bc\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03b5 WAP.<br> \
+ \u0391\u03c0\u03bb\u03ac \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf URL \u03b1\u03c0\u03cc \u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf \u03c3\u03b1\u03c2: <b>http://yourhostname/wap</b></p> \
+ <p>\u0395\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b1\u03c1\u03b1\u03af\u03c4\u03b7\u03c4\u03bf \u03bf \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03af\u03bd\u03c4\u03b5\u03c1\u03bd\u03b5\u03c4.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>\u0391\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bb\u03af\u03c3\u03c4\u03b5\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03c9\u03c2 Podcasts.<br>\
+ \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03c3\u03c4\u03bf\u03bd Podcast \u03b1\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7 \u03c3\u03b1\u03c2: <b>http://yourhostname/podcast</b>, \
+ \u03ae <b><a href="podcast.view?suffix=.rss">\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03b4\u03ce</a>.</b></p>
+more.upload.title = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5
+more.upload.source = \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5
+more.upload.target = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c3\u03b5
+more.upload.browse = \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae
+more.upload.ok = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1
+more.upload.unzip = \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03c0\u03bf\u03c3\u03c5\u03bc\u03c0\u03af\u03b5\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 zip.
+more.upload.progress = % \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03bc\u03ad\u03bd\u03bf. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5...
+
+# upload.jsp
+upload.title = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5
+upload.success = \u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ad\u03c2 \u03b1\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 <b>{0}</b>
+upload.empty = \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1.
+upload.failed = \u03a4\u03bf \u03b1\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1:<br><b>"{0}"</b>
+upload.unzipped = \u0391\u03c0\u03bf\u03c3\u03c5\u03bc\u03c0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd {0}
+
+# help.jsp
+help.title = \u03a0\u03b5\u03c1\u03af {0}
+help.upgrade = <b>Note!</b> \u039c\u03b9\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7. \u039a\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03c4\u03b5 {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+help.version.title = \u0388\u03ba\u03b4\u03bf\u03c3\u03b7
+help.builddate.title = \u0397\u03bc\u03b5\u03c1\u03bf\u03bc\u03b7\u03bd\u03af\u03b1 \u03a0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+help.server.title = \u03a5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2
+help.license.title = \u03a0\u03c1\u03bf\u03cb\u03c0\u03bf\u03b8\u03ad\u03c3\u03b5\u03b9\u03c2&nbsp;\u03c4\u03b7\u03c2&nbsp;\u03c7\u03c1\u03ae\u03c3\u03b7\u03c2
+help.license.text = {0} is free software distributed under the <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source license. \
+ {0} uses <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>. Please note that {0} is <em>not</em> \
+ a tool for illegal distribution of copyrighted material. Always pay attention to and follow the relevant laws specific to your country.
+help.homepage.title = \u0391\u03c1\u03c7\u03b9\u03ba\u03ae
+help.forum.title = \u03a6\u03cc\u03c1\u03bf\u03c5\u03bc
+help.shop.title = \u0395\u03bc\u03c0\u03cc\u03c1\u03b5\u03c5\u03bc\u03b1
+help.contact.title = \u0395\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1
+help.contact.text = {0} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03b5\u03c0\u03c4\u03c5\u03b3\u03bc\u03ad\u03bd\u03bf \u03ba\u03b1\u03b9 \u03c3\u03c5\u03bd\u03c4\u03b7\u03c1\u03ae\u03c4\u03b1\u03b9 \u03b1\u03c0\u03bf \u03c4\u03bf\u03bd Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ \u0391\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03c1\u03c9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2, \u03c3\u03c7\u03cc\u03bb\u03b9\u03b1, \u03ae \u03c0\u03c1\u03bf\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 \u03b2\u03b5\u03bb\u03c4\u03af\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03c3\u03ac\u03bc\u03c0\u03b1, \u03b1\u03bb\u03bb\u03ac \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03b9\u03c3\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03af\u03c1\u03b7\u03bc\u03b1 \u03b4\u03af\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 <b><a href="donate.view?">\u03b4\u03c9\u03c1\u03b5\u03ac</a></b>.
+help.log = \u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2
+help.logfile = \u038c\u03bb\u03bf \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf {0}.
+
+# settingsHeader.jsp
+settingsheader.title = \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2
+settingsheader.general = \u0393\u03b5\u03bd\u03b9\u03ba\u03ac
+settingsheader.advanced = \u03a0\u03c1\u03bf\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03b1
+settingsheader.personal = \u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ac
+settingsheader.musicFolder = \u039c\u03bf\u03c5\u03c3\u03b9\u03ba\u03ac \u03b1\u03c1\u03c7\u03b5\u03af\u03b1
+settingsheader.internetRadio = \u03af\u03bd\u03c4\u03b5\u03c1\u03bd\u03b5\u03c4 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7/\u03c1\u03b1\u03b4\u03b9\u03cc\u03c6\u03c9\u03bd\u03bf
+settingsheader.podcast = Podcast
+settingsheader.player = \u039b\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03ac \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+settingsheader.network = \u0394\u03af\u03ba\u03c4\u03c5\u03bf
+settingsheader.transcoding = \u0395\u03c0\u03b1\u03bd\u03b1\u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7
+settingsheader.user = \u03a7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2
+settingsheader.search = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+settingsheader.coverArt = \u0395\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2
+settingsheader.password = \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \u03a6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2
+generalsettings.musicmask = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2
+generalsettings.videomask = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf Video
+generalsettings.coverartmask = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf \u03ba\u03b1\u03bb\u03cd\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2
+generalsettings.index = \u0395\u03c5\u03c1\u03b5\u03c4\u03ae\u03c1\u03b9\u03bf
+generalsettings.ignoredarticles = \u03a0\u03c1\u03bf\u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03c0\u03c1\u03bf\u03c2 \u03b1\u03b3\u03bd\u03cc\u03b7\u03c3\u03ae
+generalsettings.shortcuts = \u03a3\u03c5\u03bd\u03c4\u03bf\u03bc\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2
+generalsettings.showgettingstarted = \u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 "\u03a0\u03ce\u03c2 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03c4\u03b5" \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03ad\u03bd\u03b1\u03c1\u03be\u03b7
+generalsettings.welcometitle = \u03a4\u03af\u03c4\u03bb\u03bf\u03c2 \u03ba\u03b1\u03bb\u03c9\u03c3\u03bf\u03c1\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2
+generalsettings.welcomesubtitle = \u03a5\u03c0\u03cc\u03c4\u03b9\u03c4\u03bb\u03bf\u03c2 \u03ba\u03b1\u03bb\u03c9\u03c3\u03bf\u03c1\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2
+generalsettings.welcomemessage = \u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae\u03c2
+generalsettings.loginmessage = \u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03bf\u03b8\u03cc\u03bd\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+generalsettings.language = \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1
+generalsettings.theme = \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03b8\u03ad\u03bc\u03b1
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Downsample \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae
+advancedsettings.coverartlimit = \u0395\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2 \u03cc\u03c1\u03b9\u03bf<br><div class="detail">(0 = \u0391\u03c0\u03b5\u03c1\u03b9\u03cc\u03c1\u03b9\u03c3\u03c4\u03bf)</div>
+advancedsettings.downloadlimit = \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03cc\u03c1\u03b9\u03bf (Kbps)<br><div class="detail">(0 = \u0391\u03c0\u03b5\u03c1\u03b9\u03cc\u03c1\u03b9\u03c3\u03c4\u03bf)</div>
+advancedsettings.uploadlimit = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03cc\u03c1\u03b9\u03bf (Kbps)<br><div class="detail">(0 = \u0391\u03c0\u03b5\u03c1\u03b9\u03cc\u03c1\u03b9\u03c3\u03c4\u03bf)</div>
+advancedsettings.streamport = Non-SSL \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9 \u03c1\u03bf\u03ae\u03c2<br><div class="detail">(0 = Disabled)</div>
+advancedsettings.ldapenabled = \u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 LDAP \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP \u03c6\u03af\u03bb\u03c4\u03c1\u03bf \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7\u03c2
+advancedsettings.ldapmanagerdn = LDAP \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2 DN<br><div class="detail">(\u03a0\u03c1\u03bf\u03b5\u03c1\u03b1\u03b9\u03c4\u03b9\u03ba\u03cc)</div>
+advancedsettings.ldapmanagerpassword = \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+advancedsettings.ldapautoshadowing = \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03c1\u03b7\u03c3\u03c4\u03ce\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03bf {0}
+
+# personalSettings.jsp
+personalsettings.title = \u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 {0}
+personalsettings.language = \u0393\u03bb\u03ce\u03c3\u03c3\u03b1
+personalsettings.theme = \u0398\u03ad\u03bc\u03b1
+personalsettings.display = \u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7
+personalsettings.browse = \u03a0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7
+personalsettings.playlist = \u039b\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+personalsettings.tracknumber = \u03a4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9 #
+personalsettings.artist = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2
+personalsettings.album = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae
+personalsettings.genre = Genre
+personalsettings.year = \u03a7\u03c1\u03bf\u03bd\u03b9\u03ac
+personalsettings.bitrate = Bit rate
+personalsettings.duration = \u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1
+personalsettings.format = \u03a4\u03cd\u03c0\u03bf\u03c2
+personalsettings.filesize = \u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5
+personalsettings.captioncutoff = \u0391\u03c0\u03bf\u03ba\u03bf\u03c0\u03ae \u03c4\u03af\u03c4\u03bb\u03bf\u03c5
+personalsettings.partymode = \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03a0\u03ac\u03c1\u03c4\u03b9
+personalsettings.shownowplaying = \u03a0\u03c1\u03bf\u03b2\u03bf\u03bb\u03ae \u03c4\u03bf\u03c5 \u03c4\u03b9 \u03b1\u03ba\u03bf\u03cd\u03bd \u03bf\u03b9 \u03ac\u03bb\u03bb\u03bf\u03b9
+personalsettings.nowplayingallowed = \u039d\u03b1 \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b2\u03bb\u03ad\u03c0\u03bf\u03c5\u03bd \u03bf\u03b9 \u03ac\u03bb\u03bb\u03bf\u03b9 \u03c4\u03b9 \u03b1\u03ba\u03bf\u03cd\u03c9
+personalsettings.showchat = \u03a0\u03c1\u03bf\u03b2\u03bf\u03bb\u03ae \u03bc\u03b7\u03bd\u03c5\u03bc\u03ac\u03c4\u03c9\u03bd chat
+personalsettings.finalversionnotification = \u039d\u03b1 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b5\u03c2 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2
+personalsettings.betaversionnotification = \u039d\u03b1 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b5\u03c2 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2 beta
+personalsettings.lastfmenabled = \u039a\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03bf \u03c4\u03af \u03b1\u03ba\u03bf\u03cd\u03c9 \u03c3\u03c4\u03bf <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7
+personalsettings.lastfmpassword = Last.fm \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2
+personalsettings.avatar.title = \u03a0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03cc \u03b5\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf
+personalsettings.avatar.none = \u039a\u03b1\u03bd\u03ad\u03bd\u03b1 \u03b5\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf
+personalsettings.avatar.custom = \u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf
+personalsettings.avatar.changecustom = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5
+personalsettings.avatar.upload = \u0391\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1
+personalsettings.avatar.courtesy = \u0395\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c6\u03bf\u03c1\u03ac \u03c4\u03bf\u03c5 <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5
+avataruploadresult.success = \u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03b7\u03bc\u03ad\u03bd\u03bf \u03b1\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5 "{0}".
+avataruploadresult.failure = \u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03b5\u03b2\u03ac\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5. \u0394\u03b5\u03af\u03c4\u03b5 <a href="help.view?">log</a> \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2.
+
+# passwordSettings.jsp
+passwordsettings.title = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \u03a6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2
+musicfoldersettings.name = \u038c\u03bd\u03bf\u03bc\u03b1
+musicfoldersettings.enabled = \u0395\u03bd\u03b5\u03c1\u03b3\u03cc
+musicfoldersettings.add = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03bf\u03cd \u03c6\u03b1\u03ba\u03ad\u03bb\u03bf\u03c5
+musicfoldersettings.nopath = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03c0\u03b9\u03b4\u03b5\u03af\u03be\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf.
+
+# networkSettings.jsp
+networksettings.text = \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd Subsonic \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03af\u03bd\u03c4\u03b5\u03c1\u03bd\u03b5\u03c4.<br> \
+ \u0391\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1, \u03c3\u03c5\u03bc\u03b2\u03bf\u03c5\u03bb\u03b5\u03c5\u03c4\u03ae\u03c4\u03b5 \u03c4\u03b9\u03c2 <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\u03a0\u03ce\u03c2 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03c4\u03b5</b></a> \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2.
+networksettings.portforwardingenabled = \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03bf Subsonic (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 UPnP or NAT-PMP \u03c0\u03c1\u03bf\u03ce\u03b8\u03b7\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b8\u03c5\u03c1\u03ce\u03bd).
+networksettings.portforwardinghelp = \u0391\u03bd \u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03c3\u03b5\u03af\u03c2. \
+ \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03c3\u03c4\u03bf <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c9\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b8\u03cd\u03c1\u03b1 {0} \u03c3\u03c4\u03bf\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c0\u03bf\u03c5 \u03c4\u03c1\u03ad\u03c7\u03b5\u03b9 \u03c4\u03bf Subsonic \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc.
+networksettings.urlredirectionenabled = \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03af\u03bd\u03c4\u03b5\u03c1\u03bd\u03b5\u03c4 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03cc\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03b5\u03cd\u03ba\u03bf\u03bb\u03b7 \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c0\u03bf\u03bc\u03bd\u03b7\u03bc\u03cc\u03bd\u03b5\u03c5\u03c3\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7.
+networksettings.status = \u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7:
+networksettings.trialexpired = \u0397 \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf\u03c2 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ae\u03c2 \u03ad\u03bb\u03b7\u03be\u03b5 \u03c3\u03c4\u03b9\u03c2 {0}. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce <b><a href="donate.view?">\u03b4\u03c9\u03c1\u03af\u03c3\u03c4\u03b5</a></b> \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03ae \u03b7 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b1.
+networksettings.trialnotexpired = \u0391\u03c5\u03c4\u03ae \u03b7 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03b7 \u03bc\u03ad\u03c7\u03c1\u03b9 {0}. \u039c\u03b5\u03c4\u03ac \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 <b><a href="donate.view?">\u03b4\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5</a></b> \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b1.
+
+# transcodingSettings.jsp
+transcodingsettings.name = \u038c\u03bd\u03bf\u03bc\u03b1
+transcodingsettings.sourceformat = \u039c\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae \u03b1\u03c0\u03cc
+transcodingsettings.targetformat = \u039c\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae \u03c3\u03b5
+transcodingsettings.step1 = \u0392\u03ae\u03bc\u03b1 1
+transcodingsettings.step2 = \u0392\u03ae\u03bc\u03b1 2
+transcodingsettings.step3 = \u0392\u03ae\u03bc\u03b1 3
+transcodingsettings.defaultactive = \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae
+transcodingsettings.enabled = \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf
+transcodingsettings.add = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7 \u03b5\u03c0\u03b1\u03bd\u03b1\u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2
+transcodingsettings.recommended = \u03a0\u03c1\u03bf\u03c4\u03b5\u03b9\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7
+transcodingsettings.noname = \u03a0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1
+transcodingsettings.nosourceformat = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae\u03c2 \u03b1\u03c0\u03cc.
+transcodingsettings.notargetformat = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae\u03c2 \u03c0\u03c1\u03bf\u03c2.
+transcodingsettings.nostep1 = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03ad\u03bd\u03b1 \u03b2\u03ae\u03bc\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae\u03c2.
+transcodingsettings.info = <p class="detail">(%s = \u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03b1\u03c0\u03b5\u03af, %b = \u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf bitrate \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2, %t = \u03a4\u03af\u03c4\u03bb\u03bf\u03c2, %a = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2, %l = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae)</p> \
+ <p>\u039c\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03bc\u03b5\u03c4\u03bf\u03c5\u03c3\u03af\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03bf \u03ad\u03bd\u03b1 \u03b5\u03af\u03b4\u03bf\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c0\u03bf\u03bb\u03bb\u03cd\u03bc\u03b5\u03c3\u03c9\u03bd \u03c3\u03b5 \u03ac\u03bb\u03bb\u03bf. {1} \u03b7 \u03bc\u03b7\u03c7\u03b1\u03bd\u03ae \
+ \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c1\u03bf\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03ba\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03ac \u03b4\u03b5\u03bd \u03b8\u03b1 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c6\u03b9\u03ba\u03c4\u03ae. \u0397 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \
+ \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c3\u03ba\u03bb\u03b7\u03c1\u03bf\u03cd \u03b4\u03af\u03c3\u03ba\u03bf\u03c5.<p/> \
+ <p>\u0397 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03ac\u03c4\u03c9\u03bd \u03b3\u03c1\u03b1\u03bc\u03bc\u03ae\u03c2, \u03c4\u03c1\u03af\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c3\u03ce\u03c0\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c4\u03bf {0}. \
+ \u0388\u03bd\u03b1 \u03c0\u03b1\u03ba\u03ad\u03c4\u03bf \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ce\u03bd \u03b3\u03b9\u03b1 Windows \
+ \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf \u03c3\u03c4\u03bf <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>\u03b5\u03b4\u03ce</b></a>. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ce\u03bd \u03b5\u03ac\u03bd \
+ \u03c0\u03bb\u03b7\u03c1\u03b5\u03af \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03c1\u03bf\u03cb\u03c0\u03bf\u03b8\u03ad\u03c3\u03b5\u03b9\u03c2: \
+ <ul> \
+ <li>\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03b1\u03c0\u03cc \u03b3\u03c1\u03b1\u03bc\u03bc\u03ae \u03b5\u03bd\u03c4\u03bf\u03bb\u03ce\u03bd.</li> \
+ <li>\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c4\u03bf stdout.</li> \
+ <li>\u0395\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03bf \u03b2\u03ae\u03bc\u03b1 2 \u03ae 3, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b5\u03c7\u03b8\u03b5\u03af \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf stdin.</li> \
+ </ul> \
+ </p> \
+ <p> \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03c2 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c3\u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03ce\u03bd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2. \u0395\u03ac\u03bd "\u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf, \u03b7 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae \
+ \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03ac \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL \u03c1\u03bf\u03ae\u03c2
+internetradiosettings.homepageurl = \u0391\u03c1\u03c7\u03b9\u03ba\u03ae
+internetradiosettings.name = \u038c\u03bd\u03bf\u03bc\u03b1
+internetradiosettings.enabled = \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf
+internetradiosettings.add = \u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u038a\u03bd\u03c4\u03b5\u03c1\u03bd\u03b5\u03c4 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2/\u03c1\u03b1\u03b4\u03b9\u03bf\u03c6\u03ce\u03bd\u03bf\u03c5
+internetradiosettings.nourl = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c5\u03c0\u03bf\u03b4\u03b5\u03af\u03be\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 URL.
+internetradiosettings.noname = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c5\u03c0\u03bf\u03b4\u03b5\u03af\u03be\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1.
+
+# podcastSettings.jsp
+podcastsettings.update = \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1
+podcastsettings.keep = \u039a\u03c1\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5
+podcastsettings.keep.all = \u038c\u03bb\u03b1 \u03c4\u03b1 \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1
+podcastsettings.keep.one = \u03a4\u03b1 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1 \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1
+podcastsettings.keep.many = \u03a4\u03b1 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 {0} \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1
+podcastsettings.download = \u038c\u03c4\u03b1\u03bd \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b1
+podcastsettings.download.all = \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03cc\u03bb\u03c9\u03bd
+podcastsettings.download.one = \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03bf\u03c5
+podcastsettings.download.many = \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c4\u03c9\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03c9\u03bd {0} \u03b5\u03c0\u03b5\u03b9\u03c3\u03bf\u03b4\u03af\u03c9\u03bd
+podcastsettings.download.none = \u039c\u03b7\u03bd \u03ba\u03ac\u03bd\u03b5\u03b9\u03c2 \u03c4\u03af\u03c0\u03bf\u03c4\u03b1
+podcastsettings.interval.manually = \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1
+podcastsettings.interval.hourly = \u039a\u03ac\u03b8\u03b5 \u03ce\u03c1\u03b1
+podcastsettings.interval.daily = \u039a\u03ac\u03b8\u03b5 \u03b7\u03bc\u03ad\u03c1\u03b1
+podcastsettings.interval.weekly = \u039a\u03ac\u03b8\u03b5 \u03b5\u03b2\u03b4\u03bf\u03bc\u03ac\u03b4\u03b1
+podcastsettings.folder = \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03c4\u03c9\u03bd Podcasts \u03c3\u03b5
+
+# playerSettings.jsp
+playersettings.noplayers = \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03ac \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2.
+playersettings.type = \u03a4\u03cd\u03c0\u03bf\u03c2
+playersettings.lastseen = \u0395\u03b9\u03b4\u03ce\u03b8\u03b7\u03ba\u03b5 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c6\u03bf\u03c1\u03ac
+playersettings.title = \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+
+playersettings.technology.web.title = \u039b\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u0399\u03c3\u03c4\u03bf\u03cd (web player)
+playersettings.technology.external.title = \u0395\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playersettings.technology.external_with_playlist.title = \u0395\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03bc\u03b5 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playersettings.technology.jukebox.title = \u03a4\u03b6\u03bf\u03cd\u03ba \u03bc\u03c0\u03cc\u03c7 (Jukebox)
+playersettings.technology.web.text = \u03a0\u03b1\u03af\u03be\u03c4\u03b5 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03b1\u03c0\u03b5\u03c5\u03b8\u03b5\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03c0\u03bb\u03bf\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03b9\u03c3\u03c4\u03bf\u03cd (browser) \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03cc\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03bf\u03bc\u03ad\u03bd\u03bf Flash player.
+playersettings.technology.external.text = \u03a0\u03b1\u03af\u03be\u03c4\u03b5 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03c3\u03c4\u03bf\u03bd \u03b1\u03b3\u03b1\u03c0\u03b7\u03bc\u03ad\u03bd\u03bf \u03c3\u03b1\u03c2 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2, \u03cc\u03c0\u03c9\u03c2 \u03c4\u03bf WinAmp \u03ae \u03c4\u03bf Windows Media Player.
+playersettings.technology.external_with_playlist.text = \u038a\u03b4\u03b9\u03bf \u03cc\u03c0\u03c9\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9, \u03bc\u03b5 \u03c4\u03b7\u03bd \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03bf \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b1\u03bd\u03c4\u03af \
+ \u03b1\u03c0\u03cc \u03c4\u03bf Subsonic. \u039c\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b7 \u03bc\u03b5\u03c4\u03b1\u03c0\u03ae\u03b4\u03b7\u03c3\u03b7 \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03ce\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.
+playersettings.technology.jukebox.text = \u03a0\u03b1\u03af\u03be\u03c4\u03b5 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae\u03c7\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03cc\u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf Subsonic. (\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bc\u03cc\u03bd\u03bf).
+playersettings.name = \u038c\u03bd\u03bf\u03bc\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playersettings.coverartsize = \u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03c9\u03bd \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2
+playersettings.maxbitrate = \u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf bitrate
+playersettings.coverart.off = \u0391\u03cc\u03c1\u03b1\u03c4\u03bf
+playersettings.coverart.small = \u039c\u03b9\u03ba\u03c1\u03cc
+playersettings.coverart.medium = \u039c\u03b5\u03c3\u03b1\u03af\u03bf
+playersettings.coverart.large = \u039c\u03b5\u03b3\u03ac\u03bb\u03bf
+playersettings.nolame = <em>\u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7:</em> LAME \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf.<br>\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.
+playersettings.autocontrol = \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playersettings.dynamicip = \u039b\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP
+playersettings.transcodings = \u0395\u03bd\u03b5\u03c1\u03b3\u03ad\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03c2
+playersettings.ok = \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7
+playersettings.forget = \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+playersettings.clone = \u0391\u03bd\u03c4\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+
+# userSettings.jsp
+usersettings.title = \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7
+usersettings.newuser = \u039a\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2
+usersettings.admin = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2
+usersettings.settings = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+usersettings.stream = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03b9 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1
+usersettings.jukebox = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03b9 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03c3\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 jukebox
+usersettings.download = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03b6\u03b5\u03b9 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1
+usersettings.upload = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03bd\u03b5\u03b2\u03ac\u03b6\u03b5\u03b9 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1
+usersettings.playlist= \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03c6\u03b5\u03b9 \u03bb\u03af\u03c3\u03c4\u03b5\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+usersettings.coverart = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ce\u03bd \u03ba\u03b1\u03b9 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b5\u03c2
+usersettings.comment= \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c3\u03c7\u03cc\u03bb\u03b9\u03b1 \u03ba\u03b1\u03b9 \u03b2\u03b1\u03b8\u03bc\u03bf\u03bb\u03bf\u03b3\u03af\u03b5\u03c2
+usersettings.podcast= \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c4\u03b1 Podcasts
+usersettings.username = \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2
+usersettings.email = \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb. \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5
+usersettings.changepassword = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+usersettings.password = \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+usersettings.newpassword = \u039a\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+usersettings.confirmpassword = \u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+usersettings.delete = \u0394\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7
+usersettings.ldap = \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03bc\u03ad\u03c3\u03c9 LDAP
+usersettings.nousername = \u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2.
+usersettings.noemail= \u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb. \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5.
+usersettings.useralreadyexists = \u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7.
+usersettings.nopassword = \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03cc\u03c2.
+usersettings.wrongpassword = \u039f\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd.
+usersettings.ldapdisabled = \u0397 LDAP \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae. \u039a\u03bf\u03b9\u03c4\u03ac\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2.
+usersettings.passwordnotsupportedforldap = \u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 LDAP.
+usersettings.ok = \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ce\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 {0}.
+
+# musicFolderSettings.jsp
+
+
+musicfoldersettings.interval.never = \u03a0\u03bf\u03c4\u03ad
+musicfoldersettings.interval.one = \u039a\u03b1\u03b8\u03b7\u03bc\u03b5\u03c1\u03b9\u03bd\u03ac
+musicfoldersettings.interval.many = \u039a\u03ac\u03b8\u03b5 {0} \u03b7\u03bc\u03ad\u03c1\u03b5\u03c2
+musicfoldersettings.hour = \u03c3\u03c4\u03b9\u03c2 {0}:00
+
+ <br>\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {0} \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b4\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2.
+
+# main.jsp
+main.up = \u03a0\u03ac\u03bd\u03c9
+main.playall = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03cc\u03bb\u03c9\u03bd
+main.playrandom = \u03a4\u03c5\u03c7\u03b1\u03af\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae
+main.addall = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7 \u03cc\u03bb\u03c9\u03bd
+main.tags = \u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b5\u03c4\u03b9\u03ba\u03b5\u03c4\u03ce\u03bd
+main.playcount = \u03a0\u03b1\u03b9\u03b3\u03bc\u03ad\u03bd\u03bf {0} \u03c6\u03bf\u03c1\u03ad\u03c2.
+main.lastplayed = \u03a4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c0\u03b1\u03b9\u03b3\u03bc\u03ad\u03bd\u03bf {0}.
+main.comment = \u03a3\u03c7\u03cc\u03bb\u03b9\u03bf
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf__</td><td>\u0388\u03bd\u03c4\u03bf\u03bd\u03bf \u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>\u039a\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03b3\u03c1\u03b1\u03bc\u03bc\u03ae</td></tr>\
+ <tr><td style="padding-right:1em">~~\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf~~</td><td>\u03a0\u03bb\u03ac\u03b3\u03b9\u03bf \u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>\u039a\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c1\u03b1\u03c6\u03bf\u03c2</td></tr>\
+ <tr><td style="padding-right:1em">* \u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf </td><td>\u03a3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf\u03c2</td></tr>\
+ <tr><td style="padding-right:1em">1. \u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf </td><td>\u03a3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03b7\u03bc\u03ad\u03bd\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf\u03c2 \u03bc\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">\u0394\u03c9\u03c1\u03af\u03c3\u03c4\u03b5</a> to {1}!<br>(\u03ba\u03b1\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c6\u03ae\u03bc\u03b9\u03c3\u03b7)
+main.nowplaying = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03c4\u03b1\u03b9 \u03c4\u03ce\u03c1\u03b1
+main.lyrics = \u03a3\u03c4\u03af\u03c7\u03bf\u03b9
+main.minutesago = \u03bb\u03b5\u03c0\u03c4\u03ac \u03c0\u03c1\u03b9\u03bd
+main.chat = \u039c\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1 chat
+main.message = \u0393\u03c1\u03ac\u03c8\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1
+main.clearchat = \u0394\u03b9\u03b1\u03b3\u03c1\u03ac\u03c8\u03c4\u03b5 \u03c4\u03b1 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03b1
+
+# rating.jsp
+rating.rating = \u0392\u03b1\u03b8\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7
+rating.clearrating = \u039a\u03ac\u03b8\u03b1\u03c1\u03c3\u03b7 \u03b2\u03b1\u03b8\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2
+
+# coverArt.jsp
+coverart.change = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae
+coverart.zoom = \u039c\u03b5\u03b3\u03ad\u03b8\u03c5\u03bd\u03c3\u03b7
+
+# allmusic.jsp
+allmusic.text = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2 <em>{0}</em> \u03c3\u03c4\u03bf allmusic.com - \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5.
+
+# changeCoverArt.jsp
+changecoverart.title = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03c9\u03bd \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2
+changecoverart.address = \u0389 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2
+changecoverart.artist = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2
+changecoverart.album = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae
+changecoverart.search = \u0395\u03cd\u03c1\u03b5\u03c3\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03bc\u03ad\u03c3\u03c9 Google Image Search
+changecoverart.wait = \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5...
+changecoverart.success = \u0397 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ba\u03b1\u03c4\u03ad\u03b2\u03b7\u03ba\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ce\u03c2.
+changecoverart.error = \u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2.
+changecoverart.noimagesfound = \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03b5\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = \u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b5\u03c4\u03b9\u03ba\u03b5\u03c4\u03ce\u03bd
+edittags.file = \u0391\u03c1\u03c7\u03b5\u03af\u03bf
+edittags.track = \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03bf\u03cd
+edittags.songtitle = \u03a4\u03af\u03c4\u03bb\u03bf\u03c2
+edittags.artist = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2
+edittags.album = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae
+edittags.year = \u03a7\u03c1\u03bf\u03bd\u03b9\u03ac
+edittags.genre = Genre
+edittags.status = \u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7
+edittags.suggest = \u03a0\u03c1\u03cc\u03c4\u03b5\u03b9\u03bd\u03b5 \u03bc\u03bf\u03c5
+edittags.reset = \u0395\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac
+edittags.suggest.short = \u03a0
+edittags.reset.short = \u0395
+edittags.set = \u0391\u03bb\u03bb\u03b1\u03b3\u03ae
+edittags.working = \u03a3\u03b5 \u03c0\u03c1\u03cc\u03bf\u03b4\u03bf
+edittags.updated = \u0391\u03bb\u03bb\u03ac\u03c7\u03c4\u03b7\u03ba\u03b1\u03bd
+edittags.skipped = \u03a5\u03c0\u03b5\u03c1\u03c0\u03b7\u03b4\u03ae\u03b8\u03b7\u03ba\u03b1\u03bd
+edittags.error = \u03a3\u03c6\u03ac\u03bb\u03bc\u03b1
+
+# donate.jsp
+donate.title = \u0394\u03c9\u03c1\u03af\u03c3\u03c4\u03b5
+donate.invalidlicense = \u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b1\u03b4\u03b5\u03af\u03b1\u03c2.
+donate.amount = \u0394\u03c9\u03c1\u03af\u03c3\u03c4\u03b5 {0}
+
+donate.textbefore = <p>\u0395\u03c5\u03c7\u03b1\u03c1\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03b8\u03b5\u03c3\u03b7 \u03c3\u03b1\u03c2 \u03bd\u03b1 \u03b4\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c4\u03bf\u03c5 {0} \u03ad\u03c1\u03b3\u03bf\u03c5! \
+ \u039f\u03b9 \u03b4\u03c9\u03c1\u03b9\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03bd\u03bf\u03bc\u03b9\u03bf\u03cd\u03c7\u03b5\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03c2 \u03cc\u03c0\u03c9\u03c2:</p> \
+ <ul> \
+ <li>\u03a1\u03bf\u03ae \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2 \u03c3\u03c4\u03bf <a href="http://subsonic.org/pages/apps.jsp" target="blank">Android, iPhone \u03ba\u03b1\u03b9 Windows Phone</a>.</li> \
+ <li>\u03a1\u03bf\u03ae \u0392\u03af\u03bd\u03c4\u03b5\u03bf.</li> \
+ <li>\u03a4\u03b7\u03bd \u03b4\u03b9\u03ba\u03ae \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: <em>yourname</em>.subsonic.org (see <a href="networkSettings.view">Settings &gt; Network</a>).</li> \
+ <li>\u0391\u03c0\u03bf\u03bc\u03ac\u03ba\u03c1\u03c5\u03bd\u03c3\u03b7 \u03b4\u03b9\u03b1\u03c6\u03b7\u03bc\u03af\u03c3\u03b5\u03c9\u03bd.</li> \
+ <li>\u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf <a href="http://subsonic.org/pages/apps.jsp" target="blank">SubAir</a> \u03bc\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae desktop.</li> \
+ <li>\u0386\u03bb\u03bb\u03b5\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03b4\u03bf\u03b8\u03bf\u03cd\u03bd \u03bc\u03b5\u03bb\u03bb\u03bf\u03bd\u03c4\u03b9\u03ba\u03ac.</li> \
+ </ul> \
+ <p> \
+ \u03a3\u03b1\u03bd \u03b4\u03c9\u03c1\u03b7\u03c4\u03ae\u03c2 \u03b8\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b1\u03b4\u03b5\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \
+ \u03ba\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 \u03bc\u03b5\u03bb\u03bb\u03bf\u03bd\u03c4\u03b9\u03ba\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.</p> \
+ <p>\u03a4\u03bf \u03c0\u03c1\u03bf\u03c4\u03b5\u03b9\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03bf\u03c3\u03cc \u03b4\u03c9\u03c1\u03b5\u03ac\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 <b>&euro;20</b>, \u03b1\u03bb\u03bb\u03ac \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03cc\u03c0\u03bf\u03b9\u03bf \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03bf\u03c3\u03cc \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5:</p>
+donate.textafter = <p>\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03b3\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf PayPal \u03cc\u03c0\u03bf\u03c5 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c3\u03c9 \u03c0\u03b9\u03c3\u03c4\u03c9\u03c4\u03b9\u03ba\u03ae\u03c2 \u03ba\u03ac\u03c1\u03c4\u03b1\u03c2 \u03ae \u03bc\u03ad\u03c3\u03c9 \
+ \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 PayPal (\u03b1\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5). \u0398\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b1\u03b4\u03b5\u03af\u03b1\u03c2 \u03c3\u03b1\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03bc\u03ad\u03c3\u03b1 \u03c3\u03b5 \u03bb\u03af\u03b3\u03b1 \u03bc\u03cc\u03bd\u03bf \u03bb\u03b5\u03c0\u03c4\u03ac.</p> \
+ <p>\u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03c1\u03c9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c3\u03c4\u03b5\u03af\u03bb\u03c4\u03b5 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf \u03c0\u03c1\u03bf\u03c2 \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = \u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b1\u03bd\u03c4\u03af\u03b3\u03c1\u03b1\u03c6\u03bf \u03c4\u03bf\u03c5 {2} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03b4\u03b5\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c3\u03c4\u03bf\u03bd/\u03b7 {0} \u03c3\u03c4\u03b9\u03c2 {1}. \u0395\u03c5\u03c7\u03b1\u03c1\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c3\u03b1\u03c2!
+donate.register = \u039c\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03bb\u03b1\u03b2\u03ae \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b1\u03b4\u03b5\u03af\u03b1\u03c2 \u03c3\u03b1\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.
+donate.register.email = \u0397\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf
+donate.register.license = \u0386\u03b4\u03b5\u03b9\u03b1
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast \u03b1\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7\u03c2
+podcastreceiver.expandall = \u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b5\u03b9\u03c3\u03bf\u03b4\u03af\u03c9\u03bd
+podcastreceiver.collapseall = \u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03b5\u03c0\u03b5\u03b9\u03c3\u03bf\u03b4\u03af\u03c9\u03bd
+podcastreceiver.status.new = \u039a\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03bf
+podcastreceiver.status.downloading = \u039a\u03b1\u03c4\u03b5\u03b2\u03b1\u03af\u03bd\u03b5\u03b9
+podcastreceiver.status.completed = \u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03bc\u03ad\u03bd\u03bf
+podcastreceiver.status.error = \u03a3\u03c6\u03ac\u03bb\u03bc\u03b1
+podcastreceiver.status.deleted = \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf
+podcastreceiver.status.skipped = \u03a5\u03c0\u03b5\u03c1\u03c0\u03b7\u03b4\u03b7\u03bc\u03ad\u03bd\u03bf
+podcastreceiver.downloadselected= \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03c9\u03bd
+podcastreceiver.deleteselected= \u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03c9\u03bd
+podcastreceiver.confirmdelete= \u039d\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03bf\u03cd\u03bd \u03cc\u03bd\u03c4\u03c9\u03c2 \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b1 Podcasts?
+podcastreceiver.check = \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03b5\u03c0\u03b5\u03b9\u03c3\u03cc\u03b4\u03b9\u03b1
+podcastreceiver.refresh = \u0391\u03bd\u03b1\u03bd\u03ad\u03c9\u03c3\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1\u03c2
+podcastreceiver.settings = Podcast \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2
+podcastreceiver.subscribe = \u0393\u03af\u03bd\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03c1\u03bf\u03bc\u03b7\u03c4\u03ad\u03c2 \u03c3\u03b5 Podcast
+
+# lyrics.jsp
+lyrics.title = \u03a3\u03c4\u03af\u03c7\u03bf\u03b9
+lyrics.artist = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2
+lyrics.song = \u03a4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9
+lyrics.search = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+lyrics.wait = \u0393\u03af\u03bd\u03b5\u03c4\u03b5\u03b1\u03b9 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03c3\u03c4\u03af\u03c7\u03c9\u03bd, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5...
+lyrics.courtesy = (\u03a3\u03c4\u03af\u03c7\u03bf\u03b9 \u03b1\u03c0\u03bf <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c4\u03af\u03c7\u03bf\u03b9.
+
+# helpPopup.jsp
+helppopup.title = {0} \u0392\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1
+helppopup.cover.title = \u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03c9\u03bd \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2
+helppopup.cover.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bc\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2, \u03bc\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bd\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03cd\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b5\u03bb\u03b5\u03af\u03c9\u03c2.</p>
+helppopup.transcode.title = \u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf bitrate
+helppopup.transcode.text = <p>\u0391\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf \u03cc\u03c1\u03b9\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c1\u03bf\u03ae \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2. \
+ \u0393\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1, \u03b1\u03bd \u03b7 \u03c4\u03b1 mp3 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1 \u03c3\u03b5 256 Kbps (kilobits \u03b1\u03bd\u03b1 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf), \u03b1\u03bb\u03bb\u03ac\u03b6\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf bitrate \
+ \u03c3\u03b5 128 \u03b8\u03b1 \u03ad\u03c7\u03b5 \u03c3\u03b1\u03bd \u03b1\u03c0\u03bf\u03c4\u03ad\u03bb\u03b5\u03c3\u03bc\u03b1 \u03c4\u03bf {0} \u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03ad\u03c8\u03b5\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c4\u03b7\u03bd \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae \u03b1\u03c0\u03cc 256 \u03c3\u03b5 128 Kbps.</p> \
+ <p>\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03cb\u03c0\u03bf\u03b8\u03ad\u03c4\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 LAME. \u03a4\u03bf LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ae\u03c2 mp3. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">\u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03b5\u03b4\u03ce</a>. \
+ \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03bd\u03b1 \u03c4\u03bf \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf SUBSONIC_HOME/transcode \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf.</p>
+helppopup.playlistfolder.title = \u039a\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+helppopup.playlistfolder.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c4\u03b7\u03b8\u03bf\u03cd\u03bd \u03bf\u03b9 \u03bb\u03af\u03c3\u03c4\u03b5\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c3\u03b1\u03c2.</p>
+helppopup.musicmask.title = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2
+helppopup.musicmask.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c5\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03b1\u03bd \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae.</p>
+helppopup.videomask.title = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf \u0392\u03af\u03bd\u03c4\u03b5\u03bf
+helppopup.videomask.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c5\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03b1\u03bd \u03b2\u03af\u03bd\u03c4\u03b5\u03bf.</p>
+helppopup.coverartmask.title = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf \u03b5\u03b9\u03ba\u03cc\u03bd\u03c9\u03bd \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2
+helppopup.coverartmask.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c5\u03c2 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03b1\u03bd \u03b5\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2 \u03cc\u03c4\u03b1\u03bd \u03c0\u03b5\u03c1\u03b9\u03c6\u03ad\u03c1\u03b5\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03c4\u03b7\u03c2 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2.</p>
+helppopup.downsamplecommand.title = \u0395\u03bd\u03c4\u03bf\u03bb\u03ae downsample
+helppopup.downsamplecommand.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03ba\u03ac\u03bd\u03b5\u03b9 downsampling \u03b3\u03b9\u03b1 \u03c7\u03b1\u03bc\u03ae\u03bb\u03c9\u03bc\u03b1 \u03c4\u03c9\u03bd bitrates.</p>\
+ <p>(%s = \u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03bd\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 downsampled, %b = \u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf bitrate \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2, %t = \u03a4\u03af\u03c4\u03bb\u03bf\u03c2, %a = \u039a\u03b1\u03bb\u03bb\u03b9\u03c4\u03ad\u03c7\u03bd\u03b7\u03c2, %l = \u03a3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae)</p>
+helppopup.index.title = \u0395\u03c5\u03c1\u03b5\u03c4\u03ae\u03c1\u03b9\u03bf
+helppopup.index.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c0\u03ce\u03c2 \u03c4\u03bf \u03b5\u03c5\u03c1\u03b5\u03c4\u03ae\u03c1\u03b9\u03bf (\u03c3\u03c4\u03b1 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac \u03c4\u03b7\u03c2 \u03bf\u03b8\u03cc\u03bd\u03b7\u03c2) \u03b8\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u0391\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03b9 \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03b9 \
+ \u03ba\u03ac\u03c4\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03cc \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b5\u03bb\u03b1\u03c3\u03c4\u03bf\u03cd\u03bd \u03b5\u03cd\u03ba\u03bf\u03bb\u03b1 \u03ba\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b5\u03c5\u03c1\u03b5\u03c4\u03b7\u03c1\u03af\u03bf\u03c5 \u03b1\u03c5\u03c4\u03bf\u03cd.</p> \
+ <p>\u039f\u03b9 \u03c0\u03c1\u03bf\u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ad\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c5\u03c1\u03b5\u03c4\u03b7\u03c1\u03af\u03bf\u03c5 \u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03b1\u03c0\u03cc \u03b4\u03b9\u03b1\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1. \u03a3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2, \u03ba\u03ac\u03b8\u03b5 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03bc\u03cc\u03bd\u03bf \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b1\u03c2, \
+ \u03b1\u03bb\u03bb\u03ac \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03bf\u03cd\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2. \u0393\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1, \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 <em>\u03a4\u03bf</em> \u03b8\u03b1 \u03b4\u03b5\u03af\u03c7\u03bd\u03b5\u03b9 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03cd\u03c2 \u03c6\u03b1\u03ba\u03ad\u03bb\u03bf\u03c5\u03c2 \
+ \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b1\u03c1\u03c7\u03af\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 "\u03a4\u03bf".</p> \
+ <p>\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b5\u03c5\u03c1\u03b5\u03c4\u03b7\u03c1\u03af\u03bf\u03c5 \u03b1\u03bd\u03ac\u03bc\u03b5\u03c3\u03b1 \u03c3\u03b5 \u03c0\u03b1\u03c1\u03b5\u03bd\u03b8\u03ad\u03c3\u03b5\u03b9\u03c2. \u0393\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1, \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \
+ <em>\u0391-\u0395(\u0391\u0392\u0393\u0394\u0395)</em> \u03b8\u03b1 \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b1\u03bd <em>A-E</em> \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03b4\u03b5\u03af\u03c7\u03bd\u03b5\u03b9 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae \u03c3\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c6\u03b1\u03ba\u03ad\u03bb\u03bf\u03c5\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03c1\u03c7\u03af\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \
+ A, B, \u0393, \u0393 \u03ae \u0395. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03ad\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c7\u03c1\u03ae\u03c3\u03b9\u03bc\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03bf\u03bc\u03b1\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03bb\u03b9\u03b3\u03cc\u03c4\u03b5\u03c1\u03bf (\u03cc\u03c0\u03c9\u03c2 X, \u03a8 \u03ba\u03b1\u03b9 \u03a9), \u03ae \
+ \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03bf\u03bc\u03b1\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03bf\u03bb\u03c5\u03c4\u03bf\u03bd\u03b9\u03ba\u03ce\u03bd \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd (\u03cc\u03c0\u03c9\u03c2 A, \u00c0 \u03ba\u03b1\u03b9 \u00c1)</p> \
+ <p>\u0391\u03c1\u03c7\u03b5\u03af\u03b1 \u03ba\u03b1\u03b9 \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03b9 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03ba\u03b1\u03bb\u03cd\u03c0\u03c4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03b9\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03c5\u03c1\u03b5\u03c4\u03b7\u03c1\u03af\u03bf\u03c5 \u03b8\u03b1 \u03bc\u03c0\u03bf\u03cd\u03bd \u03ba\u03ac\u03c4\u03c9 \u03b1\u03c0\u03bf \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 "#".</p>
+helppopup.ignoredarticles.title = \u0386\u03c1\u03b8\u03c1\u03b1 \u03c0\u03c1\u03bf\u03c2 \u03b1\u03b3\u03bd\u03cc\u03b7\u03c3\u03b7
+helppopup.ignoredarticles.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03ac\u03c1\u03b8\u03c1\u03c9\u03bd (\u03cc\u03c0\u03c9\u03c2 "T\u03bf") \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd \u03cc\u03c4\u03b1\u03bd \u03b3\u03af\u03bd\u03b5\u03b9 \u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03bf\u03c5 \u03b5\u03c5\u03c1\u03b5\u03c4\u03b7\u03c1\u03af\u03bf\u03c5.</p>
+helppopup.shortcuts.title = \u03a3\u03c5\u03bd\u03c4\u03bf\u03bc\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2
+helppopup.shortcuts.text = <p>\u039c\u03af\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03b4\u03b9\u03b1\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c6\u03b1\u03ba\u03ad\u03bb\u03c9\u03bd \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 '\u03c0\u03c1\u03ce\u03c4\u03bf\u03c5' \u03b5\u03c0\u03b9\u03c0\u03ad\u03b4\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03c4\u03bf\u03bc\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03bf\u03c2. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u0391\u03b3\u03b3\u03bb\u03b9\u03ba\u03ac \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03b9\u03ba\u03ac \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03bf\u03bc\u03b1\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bb\u03ad\u03be\u03b5\u03c9\u03bd, \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1:</p> \
+ <p><em>\u039a\u03b1\u03b9\u03bd\u03bf\u03cd\u03c1\u03b9\u03b1 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 "\u039c\u03bf\u03c5\u03c3\u03b9\u03ba\u03ac \u03ba\u03bf\u03bc\u03bc\u03ac\u03c4\u03b9\u03b1"</em></p>
+helppopup.language.title = \u0393\u03bb\u03ce\u03c3\u03c3\u03b1
+helppopup.language.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2.</p>
+helppopup.visibility.title = \u039f\u03c1\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1
+helppopup.visibility.text = <p>\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bc\u03c6\u03b1\u03bd\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03c1\u03b1\u03b3\u03bf\u03cd\u03b4\u03b9, \u03cc\u03c0\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03c4\u03b7\u03c2 \u03bb\u03b5\u03b6\u03ac\u03bd\u03c4\u03b1\u03c2. \u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \
+ \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bc\u03c6\u03b1\u03bd\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03c4\u03af\u03c4\u03bb\u03bf\u03c5\u03c2 \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03ce\u03bd, \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ce\u03bd \u03ba\u03b1\u03b9 \u03ba\u03b1\u03bb\u03bb\u03b9\u03c4\u03b5\u03c7\u03bd\u03ce\u03bd.</p>
+helppopup.partymode.title = \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03ac\u03c1\u03c4\u03b7
+helppopup.partymode.text = <p>\u038c\u03c4\u03b1\u03bd \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03ac\u03c1\u03c4\u03b9 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae, \u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03b1\u03c0\u03bb\u03bf\u03c0\u03bf\u03b9\u03ae\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03ac\u03c0\u03b5\u03b9\u03c1\u03bf\u03c5\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2. \
+ \u03a0\u03b9\u03bf \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b1, \u03b7 \u03ba\u03b1\u03c4\u03ac \u03bb\u03ac\u03b8\u03bf\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b1\u03c0\u03bf\u03c6\u03b5\u03cd\u03b3\u03b5\u03c4\u03b1\u03b9.</p>
+helppopup.theme.title = \u0398\u03ad\u03bc\u03b1
+helppopup.theme.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03c0\u03c1\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7. \u03a4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03c0\u03c1\u03c3\u03bf\u03b4\u03b9\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c5\u03c6\u03ae \u03c4\u03bf\u03c5 {0} \u03cc\u03c3\u03bf\u03bd \u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03b1, \u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03bf\u03c3\u03b5\u03b9\u03c1\u03ad\u03c2, \u03b5\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03ba\u03bb\u03c0.</p>
+helppopup.welcomemessage.title = \u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae\u03c2
+helppopup.welcomemessage.text = <p>\u03a4\u03bf \u03bc\u03c5\u03bd\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03b1\u03c1\u03c7\u03b9\u03ba\u03ae \u03c3\u03b5\u03bb\u03af\u03b4\u03b1.</p>
+helppopup.loginmessage.title = \u039c\u03cd\u03bd\u03b7\u03bc\u03b1 \u03bf\u03b8\u03cc\u03bd\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2
+helppopup.loginmessage.text = <p>T\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03bf\u03b8\u03cc\u03bd\u03b7 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.</p>
+helppopup.coverartlimit.title = \u0395\u03b9\u03ba\u03cc\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2 \u03cc\u03c1\u03b9\u03bf
+helppopup.coverartlimit.text = <p>\u039f \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03c9\u03bd \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1.</p>
+helppopup.downloadlimit.title = \u038c\u03c1\u03b9\u03bf \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2
+helppopup.downloadlimit.text = <p>\u03a4\u03bf \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf \u03cc\u03c1\u03b9\u03bf \u03b5\u03cd\u03c1\u03bf\u03c5\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf \u03ba\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd.</p>
+helppopup.uploadlimit.title = \u038c\u03c1\u03b9\u03bf \u03b1\u03bd\u03b5\u03b2\u03ac\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2
+helppopup.uploadlimit.text = <p>\u03a4\u03bf \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf \u03cc\u03c1\u03b9\u03bf \u03b5\u03cd\u03c1\u03bf\u03c5\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03bd\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03c9\u03bd.</p>
+helppopup.streamport.title = Non-SSL \u03b8\u03cd\u03c1\u03b1 \u03c1\u03bf\u03ae\u03c2
+helppopup.streamport.text = <p>\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b1\u03c6\u03bf\u03c1\u03ac \u03bc\u03cc\u03bd\u03bf \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 {0} \u03c3\u03b5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03bc\u03b5 SSL (HTTPS).</p><p>\u039c\u03b5\u03c1\u03b9\u03ba\u03ac \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03ac \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \
+ (\u03cc\u03c0\u03c9\u03c2 \u03c4\u03bf Winamp) \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03c5\u03bd \u03c1\u03bf\u03ae \u03bc\u03ad\u03c3\u03c9 over SSL. \u03a5\u03c0\u03bf\u03b4\u03b5\u03af\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b8\u03cd\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03b1\u03c0\u03bb\u03cc http (\u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 80 \
+ \u03ae 4040) \u03b1\u03bd \u03b4\u03b5\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03b7 \u03c1\u03bf\u03ae \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 SSL. \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b7.</p>
+helppopup.ldap.title = LDAP \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7
+helppopup.ldap.text = <p>\u039f\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1\u03bd \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc LDAP \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae (\u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03b1\u03bd\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03c5 Windows Active Directory). \
+ \u038c\u03c4\u03b1\u03bd \u03bf\u03b9 LDAP \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf {0}, \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03bb\u03ad\u03bd\u03b3\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03bf \u03c4\u03bf\u03bd \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae, \u03ba\u03b1\u03b9 \u03cc\u03c7\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf {0}.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>\u03a4\u03bf URL \u03c4\u03bf\u03c5 LDAP \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae. \u03a4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 <em>ldap://</em> \u03ae <em>ldaps://</em> \
+ (\u03b3\u03b9\u03b1 LDAP \u03bc\u03ad\u03c3\u03c9 SSL). \u0394\u03b5\u03af\u03c4\u03b5 <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">here</a> \
+ \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2.</p>
+helppopup.ldapsearchfilter.title = \u03a6\u03af\u03bb\u03c4\u03c1\u03bf \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7\u03c2 LDAP
+helppopup.ldapsearchfilter.text = <p>\u0397 \u03ad\u03ba\u03c6\u03c1\u03b1\u03c3\u03b7 \u03c6\u03af\u03bb\u03c4\u03c1\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c6\u03af\u03bb\u03c4\u03c1\u03bf \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7\u03c2 LDAP \
+ (\u03cc\u03c0\u03c9\u03c2 \u03b1\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ \u03a4\u03bf \u03bc\u03bf\u03c4\u03af\u03b2\u03bf "'{0'}" \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03b8\u03ae\u03c3\u03c4\u03b1\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: \
+ <ul>\
+ <li>(uid='{0'}) - \u03b1\u03c5\u03c4\u03cc \u03b8\u03b1 \u03c8\u03ac\u03be\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf uid \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc.</li> \
+ <li>(sAMAccountName='{0'}) - \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c4\u03b1\u03b9 \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2 DN
+helppopup.ldapmanagerdn.text = <p>\u0391\u03bd \u03bf LDAP \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03b1\u03bd\u03ce\u03bd\u03c5\u03bc\u03b7 \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03b4\u03b5\u03af\u03be\u03b5\u03c4\u03b5 \u03c4\u03bf DN \
+ (<em>\u0391\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u038c\u03bd\u03bf\u03bc\u03b1 (Distinguished Name)</em>) \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 LDAP \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b4\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03c4\u03b7\u03c2 \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2.</p>
+helppopup.ldapautoshadowing.title = \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 LDAP \u03c7\u03c1\u03b7\u03c3\u03c4\u03ce\u03bd \u03c3\u03c4\u03bf {0}
+helppopup.ldapautoshadowing.text = <p>\u038c\u03c4\u03b1\u03bd \u03b4\u03b9\u03b1\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae, \u03bf\u03b9 LDAP \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03bf\u03cd\u03bd \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03c3\u03c4\u03bf {0} \u03c0\u03c1\u03b9\u03bd \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03bf\u03cd\u03bd.</p> \
+ <p>\u03a3\u0397\u039c\u0395\u0399\u03a9\u03a3\u0397! \u0391\u03c5\u03c4\u03cc \u03c3\u03b7\u03bc\u03b1\u03af\u03bd\u03b5\u03b9 \u03cc\u03c4\u03b9 \u03bf\u03c0\u03bf\u03b9\u03bf\u03c3\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03bc\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf LDAP \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf {0}, \
+ \u03c0\u03c1\u03ac\u03b3\u03bc\u03b1 \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5.</p>
+helppopup.playername.title = \u038c\u03bd\u03bf\u03bc\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+helppopup.playername.text = <p>\u03a3\u03b1\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b5\u03cd\u03ba\u03bf\u03bb\u03bf \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c0\u03bf\u03bc\u03bd\u03b7\u03bc\u03cc\u03bd\u03b5\u03c5\u03c3\u03b7 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2, \u03cc\u03c0\u03c9\u03c2 "\u0394\u03bf\u03c5\u03bb\u03b5\u03b9\u03ac" \u03ae "\u03a3\u03b1\u03bb\u03cc\u03bd\u03b9".</p>
+helppopup.autocontrol.title = \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+helppopup.autocontrol.text = <p>\u039c\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b4\u03b9\u03b1\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03bf, {0} \u03b8\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03cc\u03c4\u03b1\u03bd \u03c0\u03b1\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 "\u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae" \
+ \u03c3\u03c4\u03b7\u03bd \u03bb\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2. \u0395\u03b9\u03b4\u03ac\u03bb\u03bb\u03c9\u03c2, \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03b5\u03c3\u03b5\u03af\u03c2.</p>
+helppopup.dynamicip.title = \u0394\u03c5\u03bd\u03b1\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP
+helppopup.dynamicip.text = <p>\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b1\u03bd \u03c4\u03bf \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP.</p>
+
+# wap/index.jsp
+wap.index.missing = \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03b8\u03cc\u03bb\u03bf\u03c5 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae
+wap.index.playlist = \u039b\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+wap.index.search = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+wap.index.settings = \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2
+
+# wap/browse.jsp
+wap.browse.playone = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03bf\u03cd
+wap.browse.playall = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03cc\u03bb\u03c9\u03bd
+wap.browse.addone = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7 \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03bf\u03cd
+wap.browse.addall = \u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c3\u03b7 \u03cc\u03bb\u03c9\u03bd
+wap.browse.downloadone = \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03c4\u03c1\u03b1\u03b3\u03bf\u03c5\u03b4\u03b9\u03bf\u03cd
+wap.browse.downloadall = \u039a\u03b1\u03c4\u03ad\u03b2\u03b1\u03c3\u03bc\u03b1 \u03cc\u03bb\u03c9\u03bd
+
+# wap/playlist.jsp
+wap.playlist.title = \u039b\u03af\u03c3\u03c4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+wap.playlist.noplayer = \u039a\u03b1\u03bd\u03ad\u03bd\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf
+wap.playlist.clear = \u039a\u03ac\u03b8\u03b1\u03c1\u03c3\u03b7
+wap.playlist.load = \u03a6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7
+wap.playlist.random = \u03a4\u03c5\u03c7\u03b1\u03af\u03b1
+wap.playlist.play = \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03c3\u03b5 \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf
+
+# wap/search.jsp
+wap.search.title = \u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7
+
+# wap/searchResult.jsp
+wap.searchresult.index = \u03a4\u03bf \u03b5\u03c5\u03c1\u03b5\u03c4\u03ae\u03c1\u03b9\u03bf \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.
+
+# wap/settings.jsp
+wap.settings.selectplayer = \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2
+wap.settings.allplayers = \u038c\u03bb\u03b1
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en.properties
new file mode 100644
index 00000000..938d971d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en.properties
@@ -0,0 +1,785 @@
+#
+# English localization.
+# Author: Sindre Mehus
+#
+
+common.home = Home
+common.back = Back
+common.help = Help
+common.play = Play
+common.add = Add
+common.download = Download
+common.close = Close
+common.refresh = Refresh
+common.next = Next
+common.previous = Previous
+common.more = More
+common.ok = OK
+common.cancel = Cancel
+common.save = Save
+common.create = Create
+common.delete = Delete
+common.edit = Edit
+common.confirm = Please confirm
+common.unknown = (Unknown)
+common.default = (Default)
+
+# login.jsp
+login.username = Username
+login.password = Password
+login.login = Log in
+login.remember = Remember me
+login.logout = You are now logged out.
+login.error = Wrong username or password.
+login.insecure = {0} is not secured. Please log in with username and<br>password "admin", or click <a href="login.view?user=admin&amp;password=admin">here</a>. Then change password immediately.
+login.recover = Forgotten your password?
+
+# recover.jsp
+recover.title = Forgotten your password?
+recover.text = To reset your password, please enter your <b>username</b> or <b>email address</b> below.
+recover.username = Username or email address
+recover.send = Reset the password and send it to me
+recover.success = Your password was reset and sent to {0}.
+recover.error.usernotfound = Sorry, user not found.
+recover.error.noemail = Sorry, no email address is registered for this user.
+recover.error.sendfailed = Failed to send email, please try again later.
+
+# accessDenied.jsp
+accessDenied.title = Access denied
+accessDenied.text = Sorry, you are not authorized to perform the requested operation.
+
+# notFound.jsp
+notFound.title = Not found
+notFound.text = <p>Sorry, we could not find what you were looking for.</p><p>Try reloading the web page. If that doesn't help, \
+ try scanning the media folders again.</p>
+notFound.reload = Reload page
+notFound.scan = Media folders settings
+
+# top.jsp
+top.home = Home
+top.now_playing = Playing
+top.starred = Starred
+top.settings = Settings
+top.status = Status
+top.podcast = Podcast
+top.more = More
+top.help = About
+top.search = Search
+top.upgrade = <b>Note!</b> A new version is available.<br>Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+top.missing = No media folders found. Please change the settings.
+top.logout = Log out {0}
+
+# left.jsp
+left.scanning = Scanning media folders...
+left.statistics = {0}&nbsp;artists<br>\
+ {1}&nbsp;albums<br>\
+ {2}&nbsp;songs<br>\
+ {3}<br>\
+ {4}&nbsp;hours
+left.shortcut = Shortcuts
+left.playlists = Playlists
+left.radio = Internet TV/radio
+left.allfolders = All folders
+left.createplaylist = Create new playlist
+left.importplaylist = Import playlist
+
+# playQueue.jsp
+playlist.stop = Stop
+playlist.start = Play
+playlist.confirmclear = Really clear play queue?
+playlist.clear = Clear
+playlist.shuffle = Shuffle
+playlist.repeat_on = Repeat is on
+playlist.repeat_off = Repeat is off
+playlist.undo = Undo
+playlist.settings = Settings
+playlist.more = More actions...
+playlist.more.playlist = Play queue
+playlist.more.sortbytrack = Sort by track
+playlist.more.sortbyartist = Sort by artist
+playlist.more.sortbyalbum = Sort by album
+playlist.more.selection = Selected songs
+playlist.more.selectall = Select all
+playlist.more.selectnone = Select none
+playlist.getflash = Get Flash player
+playlist.save = Save as playlist
+playlist.append = Add to playlist
+playlist.remove = Remove
+playlist.up = Up
+playlist.down = Down
+playlist.empty = Play queue is empty
+
+# playlist.jsp
+playlist2.created = Created by {0} on {1}
+playlist2.songs = songs
+playlist2.shared = Shared
+playlist2.notshared = Not shared
+playlist2.name = Playlist name
+playlist2.comment = Playlist comment
+playlist2.public = Share this playlist with other users.
+playlist2.confirmdelete = Are you sure you want to delete this playlist?
+playlist2.empty = Playlist is empty
+playlist2.export = Export
+
+# importPlaylist.jsp
+importPlaylist.title = Import playlist
+importPlaylist.text = Select playlist to import (m3u, pls, xspf)
+importPlaylist.success = Successfully imported playlist "{0}".
+importPlaylist.error = Failed to import playlist. {0}
+
+# videoPlayer.jsp
+videoPlayer.getflash = Please install Flash Player
+videoPlayer.popout = Open in new window
+
+# status.jsp
+status.title = Status
+status.type = Type
+status.stream = Stream
+status.download = Download
+status.upload = Upload
+status.player = Player
+status.user = User
+status.current = Current file
+status.transmitted = Transmitted
+status.bitrate = Bitrate (Kbps)
+
+# starred.jsp
+starred.title = My starred items
+starred.empty = Click the star icons to mark your favorite artist, albums and songs.
+
+# search.jsp
+search.title = Search
+search.query = Artist, album or song title
+search.search = Search
+search.index = The search index is now being created. Please try again later.
+search.hits.none = No matches found.
+search.hits.more = More
+search.hits.artists = Artists
+search.hits.albums = Albums
+search.hits.songs = Songs
+
+# gettingStarted.jsp
+gettingStarted.title = Getting started
+gettingStarted.text = <p>Welcome to Subsonic! We'll set you up in no time, just follow the basic steps below.<br> \
+ Click the "Home" button in the toolbar above to return to this screen.</p> \
+ <p>For more information, please consult the <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Getting started</b></a> guide.</p>
+gettingStarted.root = Warning! The Subsonic process is running as the root user. Please consider to \
+ <a href="http://subsonic.org/pages/installation.jsp" target="_blank">change this.</a>
+gettingStarted.step1.title = Change administrator password.
+gettingStarted.step1.text = Secure your server by changing the default password for the administrator account. \
+ You can also create new user accounts with different privileges.
+gettingStarted.step2.title = Set up media folders.
+gettingStarted.step2.text = Tell Subsonic where you keep your music and videos.
+gettingStarted.step3.title = Configure network settings.
+gettingStarted.step3.text = Some useful settings if you want to enjoy your music remotely over the Internet, \
+ or share it with family and friends. Get your personal <b><em>yourname</em>.subsonic.org</b> \
+ address.
+gettingStarted.hide = Don't show this again
+gettingStarted.hidealert = To show this screen again, go to Settings > General.
+
+# home.jsp
+home.random.title = Random
+home.alphabetical.title = All
+home.newest.title = Recently added
+home.starred.title = Starred
+home.highest.title = Top rated
+home.frequent.title = Most played
+home.recent.title = Recently played
+home.users.title = Users
+home.random.text = Random albums
+home.alphabetical.text = All albums
+home.newest.text = Recently added albums
+home.starred.text = Albums starred by you
+home.highest.text = Top rated albums
+home.frequent.text = Most played albums
+home.recent.text = Recently played albums
+home.users.text = User statistics
+home.scan = The media folder is now being scanned. Some features are not available.
+home.listsize = {0} albums per page
+home.albums = Albums {0} - {1}
+home.playcount = Played {0} songs
+home.lastplayed = Played {0}
+home.created = Created {0}
+home.chart.total = Total (MB)
+home.chart.stream = Streamed (MB)
+home.chart.download = Downloaded (MB)
+home.chart.upload = Uploaded (MB)
+
+# more.jsp
+more.title = More
+more.random.title = Random play queue
+more.random.text = Create random play queue with
+more.random.songs = {0} songs
+more.random.auto = Play more random songs when end of play queue is reached.
+more.random.ok = OK
+more.random.genre = from genre
+more.random.anygenre = Any
+more.random.year = and year
+more.random.anyyear = Any
+more.random.folder = in folder
+more.random.anyfolder = Any
+more.apps.title = Subsonic Apps
+more.apps.text = <p>Check out the steadily growing list of <a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic apps</a>. \
+ These provide fun and alternative ways to enjoy your media collection - no matter where you are. \
+ Apps are available for Android, iPhone, Windows Phone, PlayBook, Roku and many more.</p>
+more.mobile.title = Mobile phone
+more.mobile.text = <p>You can control {0} from any WAP-enabled mobile phone or PDA.<br> \
+ Simply visit the following URL from your phone: <b>http://yourhostname/wap</b></p> \
+ <p>This requires that your server can be reached from the Internet.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Saved playlists are available as Podcasts.<br>\
+ Use the following URL in your Podcast receiver: <b>http://yourhostname/podcast</b>, \
+ or <b><a href="podcast.view?suffix=.rss">click here</a>.</b></p>
+more.upload.title = Upload file
+more.upload.source = Select file
+more.upload.target = Upload to
+more.upload.browse = Choose
+more.upload.ok = Upload
+more.upload.unzip = Automatically unpack zip-file.
+more.upload.progress = % complete. Please wait...
+
+# upload.jsp
+upload.title = Uploading file
+upload.success = Successfully uploaded <b>{0}</b>
+upload.empty = No files to upload.
+upload.failed = Uploading failed with the following error:<br><b>"{0}"</b>
+upload.unzipped = Unzipped {0}
+
+# help.jsp
+help.title = About {0}
+help.upgrade = <b>Note!</b> A new version is available. Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+help.version.title = Version
+help.builddate.title = Build date
+help.server.title = Server
+help.license.title = Terms&nbsp;of&nbsp;use
+help.license.text = {0} is free software distributed under the <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source license. \
+ {0} uses <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>. Please note that {0} is <em>not</em> \
+ a tool for illegal distribution of copyrighted material. Always pay attention to and follow the relevant laws specific to your country.
+help.homepage.title = Homepage
+help.forum.title = Forum
+help.shop.title = Merchandise
+help.contact.title = Contact
+help.contact.text = {0} is developed and maintained by Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ If you have any questions, comments or suggestions for improvements, please visit the \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = The basic version of {0} is free, but you can get lots of <a href="donate.view">premium features</a> by giving a <b><a href="donate.view?">donation</a></b>.
+help.log = Log
+help.logfile = The complete log is saved in {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Settings
+settingsheader.general = General
+settingsheader.advanced = Advanced
+settingsheader.personal = Personal
+settingsheader.musicFolder = Media folders
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Players
+settingsheader.share = Shared media
+settingsheader.network = Network
+settingsheader.transcoding = Transcoding
+settingsheader.user = Users
+settingsheader.search = Search/caching
+settingsheader.coverArt = Cover art
+settingsheader.password = Password
+
+# generalSettings.jsp
+generalsettings.musicmask = Music files
+generalsettings.videomask = Video files
+generalsettings.coverartmask = Cover art files
+generalsettings.index = Index
+generalsettings.ignoredarticles = Articles to ignore
+generalsettings.shortcuts = Shortcuts
+generalsettings.sortalbumsbyyear = Sort albums by year
+generalsettings.showgettingstarted = Show "Getting started" on startup
+generalsettings.welcometitle = Welcome title
+generalsettings.welcomesubtitle = Welcome subtitle
+generalsettings.welcomemessage = Welcome message
+generalsettings.loginmessage = Login message
+generalsettings.language = Default language
+generalsettings.theme = Default theme
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Downsample command
+advancedsettings.coverartlimit = Cover art limit<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.downloadlimit = Download limit (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.uploadlimit = Upload limit (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.streamport = Non-SSL stream port<br><div class="detail">(0 = Disabled)</div>
+advancedsettings.ldapenabled = Enable LDAP authentication
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP search filter
+advancedsettings.ldapmanagerdn = LDAP manager DN<br><div class="detail">(Optional)</div>
+advancedsettings.ldapmanagerpassword = Password
+advancedsettings.ldapautoshadowing = Automatically create users in {0}
+
+# personalSettings.jsp
+personalsettings.title = Personal settings for {0}
+personalsettings.language = Language
+personalsettings.theme = Theme
+personalsettings.display = Display
+personalsettings.browse = Browse
+personalsettings.playlist = Playlist
+personalsettings.tracknumber = Track #
+personalsettings.artist = Artist
+personalsettings.album = Album
+personalsettings.genre = Genre
+personalsettings.year = Year
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Duration
+personalsettings.format = Format
+personalsettings.filesize = File size
+personalsettings.captioncutoff = Caption cutoff
+personalsettings.partymode = Party mode
+personalsettings.shownowplaying = Show what others are playing
+personalsettings.nowplayingallowed = Let others see what I am playing
+personalsettings.showchat = Show chat messages
+personalsettings.finalversionnotification = Notify me about new versions
+personalsettings.betaversionnotification = Notify me about new beta versions
+personalsettings.lastfmenabled = Register what I'm playing at <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm username
+personalsettings.lastfmpassword = Last.fm password
+personalsettings.avatar.title = Personal image
+personalsettings.avatar.none = No image
+personalsettings.avatar.custom = Custom image
+personalsettings.avatar.changecustom = Change custom image
+personalsettings.avatar.upload = Upload
+personalsettings.avatar.courtesy = Icons courtesy of <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Change personal image
+avataruploadresult.success = Successfully uploaded personal image "{0}".
+avataruploadresult.failure = Failed to upload personal image. See <a href="help.view?">log</a> for details.
+
+# passwordSettings.jsp
+passwordsettings.title = Change password for {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Folder
+musicfoldersettings.name = Name
+musicfoldersettings.enabled = Enabled
+musicfoldersettings.add = Add media folder
+musicfoldersettings.nopath = Please specify a folder.
+musicfoldersettings.notfound = Folder not found
+musicfoldersettings.scan = Scan media folders
+musicfoldersettings.interval.never = Never
+musicfoldersettings.interval.one = Every day
+musicfoldersettings.interval.many = Every {0} days
+musicfoldersettings.hour = at {0}:00
+musicfoldersettings.nowscanning = The media folders are now being scanned. This operation may take several minutes, depending on \
+ the size of your media library.
+musicfoldersettings.scannow = Scan media folders now
+musicfoldersettings.fastcache = Fast access mode
+musicfoldersettings.fastcache.description = Use this option to minimize disk access, for instance if your media files are located on a network disk. \
+ Note: Changes to files will only be visible after your media folders are scanned.
+musicfoldersettings.expunge = Clean-up database
+musicfoldersettings.expunge.description = Subsonic stores information about all media files ever encountered. By cleaning up the database, information about \
+ files that are no longer in your media collection is permanently removed.
+musicfoldersettings.organizebyfolderstructure = Organize by folder structure
+musicfoldersettings.organizebyfolderstructure.description = Use this option to browse through your media library using the directory structure rather than using artist/album info in ID3 tags.
+
+# networkSettings.jsp
+networksettings.text = Use the settings below to control how to access your Subsonic server over the Internet.<br> \
+ If you experience difficulties, please consult the <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Getting started</b></a> guide.
+networksettings.portforwardingenabled = Automatically configure your router to allow incoming connections to Subsonic (using UPnP or NAT-PMP port forwarding).
+networksettings.portforwardinghelp = If your router can''t be configured automatically you can set it up manually. \
+ Follow the instructions on <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ You must forward port {0} to the computer running the Subsonic server.
+networksettings.urlredirectionenabled = Access your server over the Internet using an easy-to-remember address.
+networksettings.status = Status:
+networksettings.trialexpired = The trial period expired on {0}. Please <b><a href="donate.view?">donate</a></b> to enable this feature permanently.
+networksettings.trialnotexpired = This feature is available until {0}. After that you must <b><a href="donate.view?">donate</a></b> to use it permanently.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Name
+transcodingsettings.sourceformat = Convert from
+transcodingsettings.targetformat = Convert to
+transcodingsettings.step1 = Step 1
+transcodingsettings.step2 = Step 2
+transcodingsettings.step3 = Step 3
+transcodingsettings.add = Add transcoding
+transcodingsettings.defaultactive = Enable this transcoding for all existing and new players.
+transcodingsettings.recommended = Recommended configuration
+transcodingsettings.noname = Please specify a name.
+transcodingsettings.nosourceformat = Please specify the format from which to convert.
+transcodingsettings.notargetformat = Please specify the format to which to convert.
+transcodingsettings.nostep1 = Please specify at least one transcoding step.
+transcodingsettings.info = <p class="detail">(%s = The file to be transcoded, %b = Max bitrate of the player, %t = Title, %a = Artist, %l = Album)</p> \
+ <p>Transcoding is the process of converting from one media format to another. {1}''s transcoding \
+ engine allows for the streaming of media that would not otherwise be streamable. The transcoding is performed on-the-fly and doesn''t \
+ require any disk usage.<p/> \
+ <p>The actual transcoding is done by third-party command line programs which must be installed in {0}. \
+ You may add your own custom transcoder given that it fulfills the following requirements: \
+ <ul> \
+ <li>It must have a command line interface.</li> \
+ <li>It must be able to send output to stdout.</li> \
+ <li>If used in step 2 it must be able to read input from stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Note that transcodings are activated on a per-player basis from <b>Settings &gt; Players</b>.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradiosettings.homepageurl = Homepage
+internetradiosettings.name = Name
+internetradiosettings.enabled = Enabled
+internetradiosettings.add = Add Internet TV/radio
+internetradiosettings.nourl = Please specify a URL.
+internetradiosettings.noname = Please specify a name.
+
+# podcastSettings.jsp
+podcastsettings.update = Check for new episodes
+podcastsettings.keep = Keep
+podcastsettings.keep.all = All episodes
+podcastsettings.keep.one = Most recent episode
+podcastsettings.keep.many = Last {0} episodes
+podcastsettings.download = When new episodes are available
+podcastsettings.download.all = Download all
+podcastsettings.download.one = Download the most recent one
+podcastsettings.download.many = Download last {0} episodes
+podcastsettings.download.none = Do nothing
+podcastsettings.interval.manually = Manually
+podcastsettings.interval.hourly = Every hour
+podcastsettings.interval.daily = Every day
+podcastsettings.interval.weekly = Every week
+podcastsettings.folder = Save Podcasts in
+
+# playerSettings.jsp
+playersettings.noplayers = No players found.
+playersettings.type = Type
+playersettings.lastseen = Last seen
+playersettings.title = Select player
+playersettings.technology.web.title = Web player
+playersettings.technology.external.title = External player
+playersettings.technology.external_with_playlist.title = External player with playlist
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Play music directly in the web browser using the integrated Flash player.
+playersettings.technology.external.text = Play music in your favorite player, such as WinAmp or Windows Media Player.
+playersettings.technology.external_with_playlist.text = Same as above, but the playlist is managed by the player, rather \
+ than the Subsonic server. In this mode, skipping within songs is possible.
+playersettings.technology.jukebox.text = Play music directly on the audio device of the Subsonic server. (Authorized users only).
+playersettings.name = Player name
+playersettings.coverartsize = Cover art size
+playersettings.maxbitrate = Max bitrate
+playersettings.coverart.off = Off
+playersettings.coverart.small = Small
+playersettings.coverart.medium = Medium
+playersettings.coverart.large = Large
+playersettings.nolame = <em>Notice:</em> Transcoders does not appear to be installed.<br>Click Help button for more information.
+playersettings.autocontrol = Control player automatically
+playersettings.dynamicip = Player has dynamic IP address
+playersettings.transcodings = Active transcodings
+playersettings.ok = Save
+playersettings.forget = Delete player
+playersettings.clone = Clone player
+
+# shareSettings.jsp
+sharesettings.name = Name
+sharesettings.owner = Shared by
+sharesettings.description = Description
+sharesettings.visits = Visits
+sharesettings.lastvisited = Last visited
+sharesettings.expires = Expires
+sharesettings.files = Shared files
+sharesettings.expirein = Expire in
+sharesettings.expirein.week = 1w
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1y
+sharesettings.expirein.never = never
+
+# userSettings.jsp
+usersettings.title = Select user
+usersettings.newuser = New user
+usersettings.admin = User is administrator
+usersettings.settings = User is allowed to change settings and password
+usersettings.stream = User is allowed to play files
+usersettings.jukebox = User is allowed to play files in jukebox mode
+usersettings.download = User is allowed to download files
+usersettings.upload = User is allowed to upload files
+usersettings.share = User is allowed to share files with anyone
+usersettings.coverart = User is allowed to change cover art and tags
+usersettings.comment= User is allowed to create and edit comments and ratings
+usersettings.podcast= User is allowed to administrate Podcasts
+usersettings.username = Username
+usersettings.email = Email
+usersettings.changepassword = Change password
+usersettings.password = Password
+usersettings.newpassword = New password
+usersettings.confirmpassword = Confirm password
+usersettings.delete = Delete this user
+usersettings.ldap = Authenticate user in LDAP
+usersettings.nousername = Missing username.
+usersettings.noemail= Invalid email address.
+usersettings.useralreadyexists = User already exists.
+usersettings.nopassword = Password is required.
+usersettings.wrongpassword = Passwords did not match.
+usersettings.ldapdisabled = LDAP authentication is not enabled. See Advanced settings.
+usersettings.passwordnotsupportedforldap = Can't set or change password for LDAP-authenticated users.
+usersettings.ok = Password successfully changed for user {0}.
+
+# main.jsp
+main.up = Up
+main.playall = Play all
+main.playrandom = Play random
+main.addall = Add all
+main.tags = Edit tags
+main.playcount = Played {0} times.
+main.lastplayed = Last played {0}.
+main.comment = Comment
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>New paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Named link</td></tr>\
+ </table>
+main.sharealbum = Share
+main.more = More actions...
+main.more.selection = Selected songs
+main.more.share = Share
+main.donate = <a href="{0}" style="text-decoration:underline">Donate</a> to {1}!<br>(and remove this ad)
+main.nowplaying = Now playing
+main.lyrics = Lyrics
+main.minutesago = minutes ago
+main.chat = Chat messages
+main.scanning = Scanning files:
+main.message = Write a message
+main.clearchat = Clear messages
+main.addtoplaylist.title = Add to playlist
+main.addtoplaylist.text = Add selected songs to this playlist:
+
+# rating.jsp
+rating.rating = Rating
+rating.clearrating = Clear rating
+
+# coverArt.jsp
+coverart.change = Change
+coverart.zoom = Zoom
+
+# allmusic.jsp
+allmusic.text = Searching for album <em>{0}</em> at allmusic.com - Please wait.
+
+# changeCoverArt.jsp
+changecoverart.title = Change cover art
+changecoverart.address = Or enter image address
+changecoverart.artist = Artist
+changecoverart.album = Album
+changecoverart.search = Google Image Search
+changecoverart.wait = Please wait...
+changecoverart.success = Image was successfully downloaded.
+changecoverart.error = Failed to download image.
+changecoverart.noimagesfound = No images found.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Failed to change cover art:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Edit tags
+edittags.file = File
+edittags.track = Track
+edittags.songtitle = Title
+edittags.artist = Artist
+edittags.album = Album
+edittags.year = Year
+edittags.genre = Genre
+edittags.status = Status
+edittags.suggest = Suggest
+edittags.reset = Reset
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Set
+edittags.working = Working
+edittags.updated = Updated
+edittags.skipped = Skipped
+edittags.error = Error
+
+# share.jsp
+share.title = Share
+share.warning = <h2>IMPORTANT NOTICE!</h2><p>Play fair &ndash; Don't share copyrighted material in any manner that violates the law.</p>
+share.facebook = Share on Facebook
+share.twitter = Share on Twitter
+share.googleplus = Share on Google+
+share.link = Or share this with someone by sending them this link: <a href="{0}" target="_blank">{0}</a>
+share.disabled = To share your music with someone you must first register your own <em>subsonic.org</em> address.<br> \
+ Please go to <a href="networkSettings.view"><b>Settings &gt; Network</b></a> (administrative rights required).
+share.manage = Manage my shared media
+
+# donate.jsp
+donate.title = Donate
+donate.invalidlicense = Invalid license key.
+donate.amount = Donate {0}
+
+donate.textbefore = <p>Thank you for considering a donation to support the {0} project! \
+ Donors get access to premium features like:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> for Android, iPhone and Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> for PlayBook, Roku, Mac, Chrome and more*.</li> \
+ <li>Video streaming.</li> \
+ <li>Your personal server address: <em>yourname</em>.subsonic.org (see <a href="networkSettings.view">Settings &gt; Network</a>).</li> \
+ <li>Share your media on Facebook, Twitter, Google+.</li> \
+ <li>No ads in the web interface.</li> \
+ <li>Other features to be released later.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Some apps are sold by third-party developers.</p>\
+ <p>As a donor you will receive a license key which is valid for personal, non-commercial use for this \
+ and all future releases of {0}. For commercial use, please <a href="mailto:subsonic_donation@activeobjects.no">contact</a> us for licensing options.</p> \
+ <p>The suggested donation amount is <b>&euro;20</b>, but you can select any amount you like:</p>
+donate.textafter = <p>Click a button to go to PayPal where you can pay by credit card or by using \
+ your PayPal account (if you have one). You'll receive the license key by email within a few minutes.</p> \
+ <p>If you have any questions, please send an email to \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = This copy of {2} was licensed to {0} on {1}. Thank you for your support!
+donate.register = After you receive your license key, please register it below.
+donate.resend = Already purchased a license but lost the key? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Send it again</a>.
+donate.register.email = Email
+donate.register.license = License
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast receiver
+podcastreceiver.expandall = Show episodes
+podcastreceiver.collapseall = Hide episodes
+podcastreceiver.status.new = New
+podcastreceiver.status.downloading = Downloading
+podcastreceiver.status.completed = Completed
+podcastreceiver.status.error = Error
+podcastreceiver.status.deleted = Deleted
+podcastreceiver.status.skipped = Skipped
+podcastreceiver.downloadselected= Download selected
+podcastreceiver.deleteselected= Delete selected
+podcastreceiver.confirmdelete= Really delete selected Podcasts?
+podcastreceiver.check = Check for new episodes
+podcastreceiver.refresh = Refresh page
+podcastreceiver.settings = Podcast settings
+podcastreceiver.subscribe = Subscribe to Podcast
+
+# lyrics.jsp
+lyrics.title = Lyrics
+lyrics.artist = Artist
+lyrics.song = Song
+lyrics.search = Search
+lyrics.wait = Searching for lyrics, please wait...
+lyrics.courtesy = (Lyrics by <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = No lyrics found.
+
+# helpPopup.jsp
+helppopup.title = {0} Help
+helppopup.cover.title = Cover art size
+helppopup.cover.text = <p>Allows you to specify the size of the displayed cover art, with the option to turn it off entirely.</p>
+helppopup.transcode.title = Max bitrate
+helppopup.transcode.text = <p>If you have constrained bandwidth, you may set an upper limit for the bitrate of the music streams. \
+ For instance, if your original mp3 files are encoded using 256 Kbps (kilobits per second), setting max bitrate \
+ to 128 will make {0} automatically resample the music from 256 to 128 Kbps.</p>
+helppopup.musicmask.title = Music files
+helppopup.musicmask.text = <p>Allows you to specify the type of files that should be recognized as music.</p>
+helppopup.videomask.title = Video files
+helppopup.videomask.text = <p>Allows you to specify the type of files that should be recognized as video.</p>
+helppopup.coverartmask.title = Cover art files
+helppopup.coverartmask.text = <p>Allows you to specify the type of files that should be recognized as cover art when browsing through the media folder.</p>
+helppopup.downsamplecommand.title = Downsample command
+helppopup.downsamplecommand.text = <p>Allows you to specify the command to execute when downsampling to lower bitrates.</p>\
+ <p>(%s = The file to be downsampled, %b = Max bitrate of the player, %t = Title, %a = Artist, %l = Album)</p>
+helppopup.index.title = Index
+helppopup.index.text = <p>Allows you to specify how the index (located on the left side of the screen) should appear. Files and directories \
+ directly in the root media folder can be easily accessed using this index.</p> \
+ <p>The specification is a space-separated list of index entries. Typically, each entry is simply a single character, \
+ but you may also specify multiple characters. For instance, the entry <em>The</em> will link to all files and \
+ folders starting with "The".</p> \
+ <p>You may also create an entry using a group of index characters in parentheses. For instance, the entry \
+ <em>A-E(ABCDE)</em> will display as <em>A-E</em> and link to all files and folders starting with either \
+ A, B, C, D or E. This may be useful for grouping less-frequently used characters (such and X, Y and Z), or \
+ for grouping accented characters (such as A, \u00c0 and \u00c1)</p> \
+ <p>Files and folders that are not covered by an index entry will be placed under the index entry "#".</p>
+helppopup.ignoredarticles.title = Articles to ignore
+helppopup.ignoredarticles.text = <p>Allows you to specify a list of articles (such as "The") that will be ignored when creating the index.</p>
+helppopup.shortcuts.title = Shortcuts
+helppopup.shortcuts.text = <p>A space-separated list of top-level folders to which to create shortcuts. Use quotes to group words, for instance:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = Language
+helppopup.language.text = <p>Allows you to select the language to use.</p>
+helppopup.visibility.title = Visibility
+helppopup.visibility.text = <p>Select which details should be displayed for each song, as well as the caption cutoff. This is the maximum \
+ number of characters to display for song title, album and artist.</p>
+helppopup.partymode.title = Party mode
+helppopup.partymode.text = <p>When party mode is enabled, the user interface is simplified and easier to operate for non-experienced users. \
+ In particular, accidental botching of playlists is avoided.</p>
+helppopup.theme.title = Theme
+helppopup.theme.text = <p>Allows you to select the theme to use. A theme defines the look and feel of {0} in terms of colors, fonts, images etc.</p>
+helppopup.welcomemessage.title = Welcome message
+helppopup.welcomemessage.text = <p>The message that is displayed on the home page.</p>
+helppopup.loginmessage.title = Login message
+helppopup.loginmessage.text = <p>The message that is displayed on the login page.</p>
+helppopup.coverartlimit.title = Cover art limit
+helppopup.coverartlimit.text = <p>The maximum number of cover art images to display on a single page.</p>
+helppopup.downloadlimit.title = Download limit
+helppopup.downloadlimit.text = <p>An upper limit for how much bandwidth will be used for downloading files.</p>
+helppopup.uploadlimit.title = Upload limit
+helppopup.uploadlimit.text = <p>An upper limit for how much bandwidth will be used for uploading files.</p>
+helppopup.streamport.title = Non-SSL stream port
+helppopup.streamport.text = <p>This option is relevant only if you use {0} on a server with SSL (HTTPS).</p><p>Some players \
+ (such as Winamp) don''t support streaming over SSL. Specify the port number for regular http (usually 80 \
+ or 4040) if you don''t want the streams to be transmitted over SSL. Note that the streams will not be encrypted.</p>
+helppopup.ldap.title = LDAP authentication
+helppopup.ldap.text = <p>Users can be authenticated by an external LDAP server (including Windows Active Directory). \
+ When LDAP-enabled users log on to {0}, the username and password are checked by the external server, not by {0} itself.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>The URL of the LDAP server. The protocol must be either <em>ldap://</em> or <em>ldaps://</em> \
+ (for LDAP over SSL). See <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">here</a> \
+ for a more detailed description.</p>
+helppopup.ldapsearchfilter.title = LDAP search filter
+helppopup.ldapsearchfilter.text = <p>The filter expression used in the user search. This is an LDAP search filter \
+ (as defined in <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ The pattern "'{0'}" is replaced by the username, for instance: \
+ <ul>\
+ <li>(uid='{0'}) - this would search for a username match on the uid attribute.</li> \
+ <li>(sAMAccountName='{0'}) - typically used for authentication in Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>If the LDAP server doesn''t support anonymous binding you must specify the DN \
+ (<em>Distinguished Name</em>) and password of the LDAP user to use when binding.</p>
+helppopup.ldapautoshadowing.title = Automatically create LDAP users in {0}
+helppopup.ldapautoshadowing.text = <p>With this option selected, LDAP users don''t have to be manually created in {0} before logging on.</p> \
+ <p>NOTE! This means that any user with a valid LDAP username and password can log on to {0}, \
+ which may not be what you want.</p>
+helppopup.playername.title = Player name
+helppopup.playername.text = <p>Allows you to specify an easy-to-remember name for a player, such as "Work" or "Living room".</p>
+helppopup.autocontrol.title = Control player automatically
+helppopup.autocontrol.text = <p>With this option selected, {0} will automatically start the player when you click "Play" \
+ in the playlist. Otherwise, you must start and connect the player yourself.</p>
+helppopup.dynamicip.title = Dynamic IP address
+helppopup.dynamicip.text = <p>Turn off this option if the player uses a static IP address.</p>
+
+# wap/index.jsp
+wap.index.missing = No music found
+wap.index.playlist = Playlist
+wap.index.search = Search
+wap.index.settings = Settings
+
+# wap/browse.jsp
+wap.browse.playone = Play song
+wap.browse.playall = Play all
+wap.browse.addone = Add song
+wap.browse.addall = Add all
+wap.browse.downloadone = Download song
+wap.browse.downloadall = Download all
+
+# wap/playlist.jsp
+wap.playlist.title = Playlist
+wap.playlist.noplayer = No player connected
+wap.playlist.clear = Clear
+wap.playlist.load = Load
+wap.playlist.random = Random
+wap.playlist.play = Play on phone
+
+# wap/search.jsp
+wap.search.title = Search
+
+# wap/searchResult.jsp
+wap.searchresult.index = The search index is now being created. Please try again later.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Select player
+wap.settings.allplayers = All
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en_GB.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en_GB.properties
new file mode 100644
index 00000000..1bde160e
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_en_GB.properties
@@ -0,0 +1,51 @@
+#
+# British English localisation.
+# Author: Brian Aust
+#
+
+# accessDenied.jsp
+accessDenied.text = Sorry, you are not authorised to perform the requested operation.
+
+
+# help.jsp
+help.license.text = {0} is free software distributed under the <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source licence. \
+ {0} uses <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>. Please note that {0} is <em>not</em> \
+ a tool for illegal distribution of copyrighted material. Always pay attention to and follow the relevant laws specific to your country.
+
+# playerSettings.jsp
+playersettings.technology.jukebox.text = Play music directly on the audio device of the Subsonic server. (Authorised users only).
+
+# main.jsp
+main.donate = <a href="{0}" style="text-decoration:underline">Donate</a> to {1}!<br>(and remove this advert)
+
+# donate.jsp
+donate.invalidlicense = Invalid licence key.
+donate.textbefore = <p>Thank you for considering a donation to support the {0} project! \
+ Donors receive access to premium features such as:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> for Android, iPhone and Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> for PlayBook, Roku, Mac, Chrome and more*.</li> \
+ <li>Video streaming.</li> \
+ <li>Your personal server address: <em>yourname</em>.subsonic.org (see <a href="networkSettings.view">Settings &gt; Network</a>).</li> \
+ <li>Share your media on Facebook, Twitter, Google+.</li> \
+ <li>No adverts in the web interface.</li> \
+ <li>Other features to be released later.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Some apps are sold by third-party developers.</p>\
+ <p>As a donor you will receive a licence key which is valid for personal, non-commercial use for this \
+ and all future releases of {0}. For commercial use, please <a href="mailto:subsonic_donation@activeobjects.no">contact</a> us for licencing options.</p> \
+ <p>The suggested donation amount is <b>&euro;20</b>, but you may select any amount you prefer:</p>
+donate.textafter = <p>Click a button to go to PayPal where you can pay by credit card or by using \
+ your PayPal account (if you have one). You'll receive the licence key via email within a few minutes.</p> \
+ <p>If you have any questions, please send an email to \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = This copy of {2} was licenced to {0} on {1}. Thank you for your support!
+donate.register = After you receive your licence key, please register it below.
+donate.resend = Already purchased a licence but lost the key? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Send it again</a>.
+donate.register.license = Licence
+
+# helpPopup.jsp
+helppopup.musicmask.text = <p>Allows you to specify the type of files that should be recognised as music.</p>
+helppopup.videomask.text = <p>Allows you to specify the type of files that should be recognised as video.</p>
+helppopup.coverartmask.text = <p>Allows you to specify the type of files that should be recognised as cover art when browsing through the media folder.</p>
+helppopup.theme.text = <p>Allows you to select the theme to use. A theme defines the look and feel of {0} in terms of colours, fonts, images, etc.</p>
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_es.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_es.properties
new file mode 100644
index 00000000..368d36dd
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_es.properties
@@ -0,0 +1,427 @@
+#
+# Spanish localization.
+# Author: Jorge Bueno Magdalena (jxrgxb at gmail.com)
+#
+
+common.home = Inicio
+common.back = Volver
+common.help = Ayuda
+common.play = Reproducir
+common.add = A\u00f1adir
+common.download = Descargar
+common.close = Cerrar
+common.refresh = Actualizar
+common.next = Siguiente
+common.previous = Anterior
+common.more = M&aacute;s
+common.ok = OK
+common.save = Guardar
+common.create = Crear
+common.delete = Borrar
+common.unknown = (Desconocido)
+common.default = (Predeterminado)
+
+# login.jsp
+login.username = Usuario
+login.password = Contrase&ntilde;a
+login.login = Iniciar sesi&oacute;n
+login.remember = Recuerdame
+login.logout = Est&aacute; usted desconectado.
+login.error = Usuario o contrase&ntilde;a incorrecta.
+
+# top.jsp
+top.home = Inicio
+top.now_playing = Reproduciendo
+top.settings = Configuraci&oacute;n
+top.status = Estado
+top.more = M&aacute;s
+top.help = Ayuda
+top.search = Buscar
+top.upgrade = <b>&iexcl;Aviso!</b> Una nueva versi&oacute;n est&aacute; disponible.<br>Descargar {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">aqu&iacute;</a>.
+top.missing = No se encuentra ning&uacute;n directorio de m&uacute;sica. Por favor cambie la configuraci&oacute;n.
+top.logout = Desconectar {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artistas<br>\
+ {1}&nbsp;albumes<br>\
+ {2}&nbsp;canciones<br>\
+ {3} (&#126; {4} horas)
+left.shortcut = Accesos directo
+left.radio = Internet TV/radio
+left.allfolders = Todos los directorios
+
+# playlist.jsp
+playlist.stop = Parar
+playlist.start = Reproducir
+playlist.clear = Limpiar
+playlist.shuffle = Modo aleatorio
+playlist.repeat_on = Desactivar repetir
+playlist.repeat_off = Activar repetir
+playlist.undo = Deshacer
+playlist.load = Cargar
+playlist.save = Guardar
+playlist.remove = Borrar
+playlist.up = Arriba
+playlist.down = Abajo
+playlist.empty = La lista de reproducci&oacute;n est&aacute; vacia
+
+# status.jsp
+status.title = Estado
+status.type = Tipo
+status.stream = Stream
+status.download = Descargar
+status.upload = Subir
+status.player = Oyente
+status.user = Usuario
+status.current = Canci&oacute;n
+status.transmitted = Transmitido
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Buscar
+search.search = Buscar
+search.index = El &iacute;ndice de busqueda se est&aacute; creando en estos momentos. Por favor intentelo m&aacute;s tarde.
+search.hits.none = No se han encontrado resultados.
+
+# home.jsp
+home.random.title = Aleatorio
+home.newest.title = Lo m&aacute;s nuevo
+home.highest.title = Los m&aacute;s valorados
+home.frequent.title = Escuchados frecuentemente
+home.recent.title = Escuchados recientemente
+home.users.title = Usuarios
+home.random.text = Albumes aleatorios
+home.newest.text = Albumes a&ntilde;adido o modificados recientemente
+home.highest.text = Los albumes mejor valorados
+home.frequent.text = Los albumes frecuentemente escuchados
+home.recent.text = Los albumes recientemente escuchados
+home.users.text = Estad&iacute;sticas de usuario
+home.scan = El directorio de m&uacute;sica esta siendo escaneado en estos momentos. No estan toda las carater&iacute;sticas a&uacute;n disponibles
+home.listsize = {0} albumes por p&aacute;gina
+home.albums = Albums {0} - {1}
+home.playcount = Reproducidos {0} veces
+home.lastplayed = Reproducidos {0}
+home.created = Modificados {0}
+home.chart.total = Total (MB)
+home.chart.stream = Streamed (MB)
+home.chart.download = Descargados (MB)
+home.chart.upload = Subidos (MB)
+
+# more.jsp
+more.title = M&aacute;s
+more.random.title = Lista de reproducci&oacute;n aleatoria
+more.random.text = Crear lista de reproducci&oacute;n aleatoria con
+more.random.songs = {0} canciones
+more.random.ok = OK
+more.mobile.title = Tel&eacute;fono m&oacute;vil
+more.mobile.text = <p>Usted puede controlar {0} con cualquier m&oacute;vil que tenga WAP activado o con una PDA.<br> \
+ Simplemente tiene que visitar la siguiene URL desde su tel&eacute;fono: <b>http://yourhostname/wap</b></p> \
+ <p>Para que esto funcione necesita que su servidor se alcance desde internet.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Listas de reproducci&oacute;n almacendas como Podcasts.<br>\
+ Use la siguiente URL en su Podcast: <b>http://yourhostname/podcast</b>, \
+ o <b><a href="podcast.view?suffix=.rss">pinche aqu&iacute;</a>.</b></p>
+more.upload.title = Subir fichero
+more.upload.source = Seleccionar fichero
+more.upload.target = Subir a
+more.upload.browse = Elegir
+more.upload.ok = Subir
+more.upload.unzip = Fichero ZIP autodescomprimible
+more.upload.progress = % completado. Por favor espere...
+
+# upload.jsp
+upload.title = Subiendo archivo
+upload.success = Subida completada <b>{0}</b>
+upload.empty = No hay ficheros para subir.
+upload.failed = Ha fallado la subida debido al siguiente error:<br><b>"{0}"</b>
+upload.unzipped = Descomprimido {0}
+
+# help.jsp
+help.title = Sobre {0}
+help.upgrade = <b>&iexcl;Aviso!</b> Una nueva versi&oacute;n esta disponible. Descargue {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">aqu&iacute;</a>.
+help.version.title = Versi&oacute;n
+help.builddate.title = Fecha de creaci&oacute;n
+help.license.title = Licencia
+help.license.text = {0} es software libre distribuido bajo la licencia de c&oacute;digo abierto <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>.
+help.homepage.title = P&aacute;gina web del proyecto
+help.forum.title = Foro
+help.contact.title = Contacto
+help.contact.text = {0} est&aacute; desarrollado y mantenido por Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Si usted tiene alguna pregunta, comentario o sugerencia de mejoras, por favor visite \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} es gratis, pero puede contribuir al proyecto ofreciendo una a donaci&oacute;n.
+help.log = Log
+help.logfile = El log completo esta guardado en {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Configuraciones
+settingsheader.general = General
+settingsheader.personal = Apariencia
+settingsheader.musicFolder = Directorios de m&uacute;sica
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.player = Oyentes
+settingsheader.transcoding = Cambiar formato
+settingsheader.user = Usuarios
+settingsheader.search = Buscar
+settingsheader.password = Contrase&ntilde;a
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Directorio de la lista de reproducci&oacute;n
+generalsettings.musicmask = Ficheros de m&uacute;sica
+generalsettings.coverartmask = Ficheros de caratula
+generalsettings.index = Indice
+generalsettings.ignoredarticles = Ignorar articulos
+generalsettings.shortcuts = Accesos directos
+generalsettings.welcomemessage = Mensaje de bienvenida
+generalsettings.language = Idioma por defecto
+generalsettings.theme = Tema por defecto
+
+# advancedSettings.jsp
+advancedsettings.coverartlimit = Limite del tama\u00f1o de caratula<br><div class="detail">(0 = Sin limite)</div>
+advancedsettings.downloadlimit = Limite de bajada (Kbps)<br><div class="detail">(0 = Sin limite)</div>
+advancedsettings.uploadlimit = Limite de subida (Kbps)<br><div class="detail">(0 = Sin limite)</div>
+advancedsettings.streamport = Puerto no SSL<br><div class="detail">(0 = Desactivado)</div>
+
+# personalSettings.jsp
+personalsettings.title = Configuraci&oacute;n de apariencia para {0}
+personalsettings.language = Idioma
+personalsettings.theme = Tema
+personalsettings.display = Pantalla
+personalsettings.browse = Navegador
+personalsettings.playlist = Lista de reproducci&oacute;n
+personalsettings.tracknumber = Pista #
+personalsettings.artist = Artista
+personalsettings.album = Album
+personalsettings.genre = Genero
+personalsettings.year = A&ntilde;o
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Duraci&oacute;n
+personalsettings.format = Formato
+personalsettings.filesize = Tama&ntilde;o del fichero
+personalsettings.captioncutoff = Caracteres visualizables
+personalsettings.finalversionnotification = Notificame sobre nuevas versiones
+personalsettings.betaversionnotification = Notificame sobre nuevas versiones beta
+
+# passwordSettings.jsp
+passwordsettings.title = Cambiar contrase&ntilde;a por {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Directorio
+musicfoldersettings.name = Nombre
+musicfoldersettings.enabled = Habilitado
+musicfoldersettings.nopath = Por favor especifique un directorio.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nombre
+transcodingsettings.sourceformat = Convertir de
+transcodingsettings.targetformat = Convertir a
+transcodingsettings.step1 = Paso 1
+transcodingsettings.step2 = Paso 2
+transcodingsettings.step3 = Paso 3
+transcodingsettings.enabled = Habilitado
+transcodingsettings.noname = Por favor especifique un nombre.
+transcodingsettings.nosourceformat = Por favor especifique un formato desde el que convertir.
+transcodingsettings.notargetformat = Por favor especifique un formato al que convertir.
+transcodingsettings.nostep1 = Por favor especifique al menos un paso para cambiar de formato.
+transcodingsettings.info = <p class="detail">(%s = El fichero cuyo formato queremos cambiar, %b = Bitrate m&aacute;xima del reproductor)</p> \
+ <p>El cambio de formato de un archivo audio es el paso de una codificaci&oacute;n a otra. El cambio de formato de {1} \
+ te permite hacer streaming de audio que normalmente no podrias hacer. El cambio de formato se hace al vuelo y no \
+ necesita espacio en disco.<p/> \
+ <p>El cambio de formato se realiza por medio de programas de linea de comandos de terceros los cuales deben ser instalados en {0}. \
+ Un pack de windows para el cambio de formato \
+ esta disponible <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>aqui</b></a>. Usted puede a&ntilde;adir su propio programa \
+ si cumple los siguientes requisitos: \
+ <ul> \
+ <li>Debe tener una interface de linea de comandos.</li> \
+ <li>Debe ser capaz de enviar la salida a stdout.</li> \
+ <li>Si se usa el paso 2 o 3 debe ser capaz de leer la entrada de stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Dese cuenta que el cambio de codificaci&oacute;n se activa en el reproductor desde la p&aacute;gina de configuraci&oacute;n.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL del stream
+internetradiosettings.homepageurl = P&aacute;gina principal
+internetradiosettings.name = Nombre
+internetradiosettings.enabled = Habilitado
+internetradiosettings.nourl = Por favor especifique una URL.
+internetradiosettings.noname = Por favor especifique un nombre.
+
+# playerSettings.jsp
+playersettings.noplayers = No se ha encontrado ning&uacute;n oyente.
+playersettings.type = Tipo
+playersettings.lastseen = Ultimo visto
+playersettings.title = Seleccione oyente
+playersettings.name = Nombre del oyente
+playersettings.coverartsize = Tama&ntilde;o de la caratula
+playersettings.maxbitrate = Bitrate m&aacute;xima
+playersettings.nolame = <em>Noticia:</em> LAME no parece estar instalado.<br>Pinche el boton de ayuda para m&aacute;s informaci&oacute;n.
+playersettings.autocontrol = Controla el reproductor automaticamente
+playersettings.dynamicip = El oyente tiene IP din&aacute;mica
+playersettings.transcodings = Cambio de formato activo
+playersettings.ok = Guardar
+playersettings.forget = Borrar oyente
+playersettings.clone = Copiar oyente
+
+# userSettings.jsp
+usersettings.title = Selecionar usuario
+usersettings.newuser = Nuevo usuario
+usersettings.admin = El usuario es administrador
+usersettings.download = Se permite la descarga de ficheros al usuario
+usersettings.upload = Se permite la subida de ficheros al usuario
+usersettings.playlist= Se permite al usuario la creaci&oacute;n y borrado de las listas de reproducci&oacute;n
+usersettings.coverart = Se permite al usuario cambiar la caratula y los tags
+usersettings.comment= Se permite al usuario crear y editar comentarios y calificaciones
+usersettings.username = Nombre de usuario
+usersettings.changepassword = Cambiar contrase&ntilde;a
+usersettings.password = Contrase&ntilde;a
+usersettings.newpassword = Nueva contrase&ntilde;a
+usersettings.confirmpassword = Confirmar contrase&ntilde;a
+usersettings.delete = Borrar este usuario
+usersettings.nousername = No se encuentra el nombre de usuario.
+usersettings.useralreadyexists = El usuario ya existe.
+usersettings.nopassword = Se requiere contrase&ntilde;a.
+usersettings.wrongpassword = Las contrase&ntilde;as no coinciden.
+usersettings.ok = El usuario {0} ha cambiado la contrase&ntilde;a correctamente.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Nunca
+musicfoldersettings.interval.one = Cada d&iacute;a
+musicfoldersettings.interval.many = Cada {0} d&iacute;as
+musicfoldersettings.hour = a las {0}:00
+
+# main.jsp
+main.up = Subir
+main.playall = Reproducir todo
+main.addall = A&ntilde;adir todo
+main.tags = Editar tags
+main.playcount = Reproducido {0} veces.
+main.lastplayed = Ultima reproducida {0}.
+main.comment = Comentario
+
+# rating.jsp
+rating.rating = Calificando
+
+# coverArt.jsp
+coverart.change = Cambiar
+coverart.zoom = Ampliar
+
+# allmusic.jsp
+allmusic.text = Buscando el album <em>{0}</em> en allmusic.com - Por favor espere.
+
+# changeCoverArt.jsp
+changecoverart.title = Cambiar caratula
+changecoverart.address = Introduzca la direcci&oacute;n de la imagen de la caratula
+changecoverart.artist = Artista
+changecoverart.album = Album
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Ha fallado al cambiar la caratula:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Editar tags
+edittags.file = Fichero
+edittags.songtitle = Titulo
+edittags.artist = Artista
+edittags.album = Album
+edittags.year = A&ntilde;o
+edittags.status = Estado
+edittags.suggest = Sugerir
+edittags.reset = Reset
+edittags.set = Establecer
+edittags.working = Trabajando
+edittags.updated = Actualizado
+edittags.skipped = Saltado
+edittags.error = Error
+
+
+# helpPopup.jsp
+helppopup.title = Ayuda de {0}
+helppopup.cover.title = Tama&ntilde;o de la caratula
+helppopup.cover.text = <p>Te permite especificar el tama\u00f1o de la caratula visualizada con la opcion de ocultarla completamente.</p>
+helppopup.transcode.title = Bitrate m&aacute;xima
+helppopup.transcode.text = <p>Si lo desea puede limitar el bitrate del stream de musica. \
+ Por ejemplo, si su mp3 original esta codificado a 256 Kbps(kilobits por segundo), configurar la bitrate maxima \
+ a 128 hara que {0} codifique la musica de 256 a 128 Kbps.</p> \
+ <p>Esta opci&oacute;n necesita que LAME est&eacute; instalado. LAME<a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ es un codificador de MP3 de c&oacute;digo abierto. Puede descargarlo <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">aqui</a>. \
+ Asegurse de que se instala en SUBSONIC_HOME/transcode, o en un directorio que este presente en su variable de entorno PATH.</p>
+helppopup.playlistfolder.title = Directorio de la lista de reproducci&oacute;n
+helppopup.playlistfolder.text = <p>Te permite especificar el directorio donde se encuentran las listas de reproducci&oacute;n.</p>
+helppopup.musicmask.title = Ficheros de musica
+helppopup.musicmask.text = <p>Te permite especificar el tipo de ficheros que deber&iacute;an reconocerse como m&uacute;sica cuando naveguemos por los directorios de m&uacute;sica.</p>
+helppopup.coverartmask.title = Ficheros de caratula
+helppopup.coverartmask.text = <p>Te permite especificar el tipo de ficheros que deber&iacute;an reconocerse como caratulas cuando naveguemos por los directorios de m&uacute;sica.</p>
+helppopup.index.title = Indice
+helppopup.index.text = <p>Te permite especificar como deberia ser el indice que esta situado en la parte de arriba de la pantalla. De esta manera \
+ los ficheros y directorios que se encuentran en los directorios de musica configurados pueden ser accedidos mas facilmente.</p> \
+ <p>La manera de especificar los indices es a traves de una lista separada por espacios. Normalmente estos indices son de un caracter \
+ pero podemos agrupar los que queramos. Por ej a traves del indice <em>el</em> accederemos a los fichero y directorios \
+ que empiecen por "el".</p> \
+ <p>Usted tambien puede crear un indice que sea un grupo de caracteres de indice, tienes que estar entre parenteis. Por ejemplo el indice \
+ <em>A-E(ABCDE)</em> se mostrara como <em>A-E</em> y enlazara con todos los ficheros y directorios que empiecen por \
+ A, B, C, D o E. Esto puede ser util para agrupar los caracteres que menos se utilizan (como X, Y y Z), o \
+ para agrupar los caracteres acentuados (como A, \u00c0 y \u00c1)</p> \
+ <p>Los ficheros y directorios que no se encuentren debaje de un indice se asignaran al indice "#".</p>
+helppopup.ignoredarticles.title = Ignorar articulos
+helppopup.ignoredarticles.text = <p>Te permite especificar una lista de articulos(como "The","el","la") que ser&aacute;n ignorados a la hora de crear el indice.</p>
+helppopup.shortcuts.title = Accesos directos
+helppopup.shortcuts.text = <p>Una lista separada por espacios de los directorios de los que quieres crear accesos directos. Utilice comillas para agrupar palabras, por ejemplo:</p> \
+ <p><em>Nuevo Incoming "Musica electronica"</em></p>
+helppopup.language.title = Idioma
+helppopup.language.text = <p>Te permite especificar el idioma que quieres usar.</p>
+helppopup.visibility.title = Visibilidad
+helppopup.visibility.text = <p>Seleccione con que detalles quiere que se muestre cada canci\u00f3n y cuantos caracteres se pueden visualizar. Este es el m&aacute;ximo \
+ de caracteres que se pueden visualizar en el titulo de una canciom, de un album o en el nombre de un artista.</p>
+helppopup.theme.title = Tema
+helppopup.theme.text = <p>Te permite seleccionar el tema que quiere usar. Un tema define la apariencia(colores, fuentes, imganes...) de {0}.</p>
+helppopup.welcomemessage.title = Mensaje de bienvenida
+helppopup.welcomemessage.text = <p>El mensaje que se muestra al inicio de la pagina.</p>
+helppopup.coverartlimit.title = Limite de caratulas
+helppopup.coverartlimit.text = <p>N&uacute;mero m&aacute;ximo de caratulas que se muestran en una p&aacute;gina.</p>
+helppopup.downloadlimit.title = Limite de descarga
+helppopup.downloadlimit.text = <p>Especifica cuanto ancho de banda se utilizara para descargar los archivos.</p>
+helppopup.uploadlimit.title = Limite de subida
+helppopup.uploadlimit.text = <p>Especifica cuanto ancho de banda se utilizara para subir los archivos.</p>
+helppopup.streamport.title = Puerto no SSL
+helppopup.streamport.text = <p>Esta opci&oacute;n solo es relevante si usa {0} en un servidor con SSL (HTTPS).</p><p>Algunos reproductores \
+ (como Winamp) no soportan streaming sobre SSL por lo tanto deberemos especificar un puerto alternativo http(normalmente 80 \
+ o 4040) para estos reproductores. Los streams no se encriptaran.</p>
+helppopup.playername.title = Nombre del oyente
+helppopup.playername.text = <p>Le permite especificar un nombre para el oyente(usuario@ip).</p>
+helppopup.autocontrol.title = Controla el reproductor automaticamente
+helppopup.autocontrol.text = <p>Si selecciona esta opci&oacute; {0} ejecutara el reproductor cuando usted pinche "Reproducir"\
+ en la lista de reproducci&oacute;n. Si no, usted debe ejecutar y conectar el reproductor.</p>
+helppopup.dynamicip.title = IP din&aacute;mica
+helppopup.dynamicip.text = <p>Deshabilite esta opci&oacute;n si el oyente usa IP estatica.</p>
+
+# wap/index.jsp
+wap.index.missing = No se ha encontrado m\u00fasica
+wap.index.playlist = Lista de reproducci\u00f3n
+wap.index.search = Buscar
+wap.index.settings = Configuraci\u00f3n
+
+# wap/browse.jsp
+wap.browse.playone = Reproduce la canci\u00f3n
+wap.browse.playall = Reproduce todo
+wap.browse.addone = A\u00f1ade la canci\u00f3n
+wap.browse.addall = A\u00f1ade todo
+
+# wap/playlist.jsp
+wap.playlist.title = Lista de reproducci\u00f3n
+wap.playlist.noplayer = Ning\u00fan reproductor conectado
+wap.playlist.clear = Limpiar
+wap.playlist.load = Cargar
+
+# wap/search.jsp
+wap.search.title = Buscar
+
+# wap/searchResult.jsp
+wap.searchresult.index = El indice de busqueda se esta creando en estos momentos. Por favor intentelo m\u00e1s tarde.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Seleccione reproductor
+wap.settings.allplayers = Todo
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_et.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_et.properties
new file mode 100644
index 00000000..5baa4b80
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_et.properties
@@ -0,0 +1,771 @@
+\ufeff#
+# Estonian localization.
+# Author: Olav M\u00e4gi
+#
+
+common.home = Kodu
+common.back = Tagasi
+common.help = Abi
+common.play = Esita
+common.add = Lisa
+common.download = Lae alla
+common.close = Sulge
+common.refresh = V\u00e4rskenda
+common.next = J\u00e4rgmine
+common.previous = Eelmine
+common.more = Rohkem
+common.ok = OK
+common.cancel = Loobu
+common.save = Salvesta
+common.create = Loo
+common.delete = Kustuta
+common.unknown = (Tundmatu)
+common.default = (Tavaline)
+
+# login.jsp
+login.username = Kasutajanimi
+login.password = Parool
+login.login = Logi sisse
+login.remember = M\u00e4leta mind
+login.logout = N\u00fc\u00fcd olete v\u00e4lja logitud.
+login.error = Kasutajanimi v\u00f5i parool on vale.
+login.insecure = {0} pole turvaline. Palun logi sisse kasutajanimega ja <br>parooliga "admin", v\u00f5i kliki <a href="login.view?user=admin&amp;password=admin">siia</a>. Peale seda muutke parool koheselt \u00e4ra.
+login.recover = Unustasite parooli?
+
+# recover.jsp
+recover.title = Unustasite parooli?
+recover.text = Uue parooli l\u00e4htestamiseks kirjuta oma <b>kasutajanimi</b> v\u00f5i <b>e-posti aadress</b> below.
+recover.username = Kasutajanimi v\u00f5i e-posti aadress
+recover.send = T\u00fchista praegune parool ja saada uus mulle
+recover.success = Sinu praegune parool t\u00fchistatu ja saadeti aadressile {0}.
+recover.error.usernotfound = Vabandame, kuid kasutajanime ei leitud.
+recover.error.noemail = Vabandame, kuid mitte \u00fchtegi kasutajat pole registreeritud selle e-posti aadressiga.
+recover.error.sendfailed = E-postile saatmine eba\u00f5nnestus, palun proovige uuesti.
+
+# accessDenied.jsp
+accessDenied.title = Ligip\u00e4\u00e4s keelatud
+accessDenied.text = Vabandame, kuid antud tegevusele pole Teile luba antud.
+
+# top.jsp
+top.home = Kodu
+top.now_playing = Esitamisel
+top.starred = Lemmikud
+top.settings = Seaded
+top.status = Olek
+top.podcast = Taskupleier
+top.more = Rohkem
+top.help = Info
+top.search = Otsi
+top.upgrade = <b>Teade!</b> Uuem versioon on saadaval.<br>Lae alla {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">siit</a>.
+top.missing = Meediakauste ei leitud. Palun muuda seadeid.
+top.logout = Logi v\u00e4lja {0}
+
+# left.jsp
+left.scanning = Meediakaustade sk\u00e4nneerimine...
+left.statistics = {0}&nbsp;esitajat<br>\
+ {1}&nbsp;albumit<br>\
+ {2}&nbsp;lugu<br>\
+ {3}<br>\
+ {4}&nbsp;tundi
+left.shortcut = Otseteed
+left.radio = Interneti TV/raadio
+left.allfolders = K\u00f5ik kaustad
+
+# playlist.jsp
+playlist.stop = Peata
+playlist.start = Esita
+playlist.confirmclear = Kas t\u00f5esti tahad puhastada esitusj\u00e4rjekorda?
+playlist.clear = Puhasta
+playlist.shuffle = Sega
+playlist.repeat_on = Kordamine on sees
+playlist.repeat_off = Kordamine on v\u00e4ljas
+playlist.undo = V\u00f5ta tagasi
+playlist.settings = Seaded
+playlist.more = Rohkem tegevusi...
+playlist.more.playlist = Esitusj\u00e4rjekord
+playlist.more.sortbytrack = Loo j\u00e4rgi sorteerimine
+playlist.more.sortbyartist = Esitaja j\u00e4rgi sorteerimne
+playlist.more.sortbyalbum = Albumi j\u00e4rgi sorteerimine
+playlist.more.selection = Valitud lood
+playlist.more.selectall = Vali k\u00f5ik
+playlist.more.selectnone = \u00c4ra vali midagi
+playlist.getflash = Hangi Flash meediaesitaja
+playlist.load = Lae esitusloend
+playlist.save = Salvesta esitusloendina
+playlist.append = Lisa esitusloendi
+playlist.remove = Eemalda
+playlist.up = \u00dcles
+playlist.down = Alla
+playlist.empty = Esitusj\u00e4rjekord on t\u00fchi
+
+# videoPlayer.jsp
+videoPlayer.getflash = Palun paigalda Flash meediaesitaja
+videoPlayer.popout = Ava h\u00fcpikaknas
+
+# loadPlaylist.jsp
+playlist.load.title = Lae esitusloend
+playlist.load.appendtitle = Lisa esitusloendisse
+playlist.load.load = Lae
+playlist.load.append = Lisa
+playlist.load.delete = Kustuta
+playlist.load.confirm_delete = Tahad t\u00f5esti esitusloendit kustutada?
+playlist.load.missing_folder = Esitusloendi kausta "{0}" pole olemas. Palun muuda seadeid.
+playlist.load.empty = Esitusloendeid pole saadaval.
+
+# savePlaylist.jsp
+playlist.save.title = Salvesta esitusloend
+playlist.save.save = Salvesta
+playlist.save.name = Esitusloendi nimi
+playlist.save.format = Formaat
+playlist.save.missing_folder = Esitusloendi kausta "{0}" pole olemas. Palun muuda seadeid.
+playlist.save.noname = Palun t\u00e4psusta esitusloendi nime.
+
+# status.jsp
+status.title = Olek
+status.type = T\u00fc\u00fcp
+status.stream = Striimi
+status.download = Lae alla
+status.upload = Lae \u00fcles
+status.player = Meediaesitaja
+status.user = Kasutaja
+status.current = Praegune fail
+status.transmitted = Edastatud
+status.bitrate = Kvaliteet (Kbps)
+
+# starred.jsp
+starred.title = Minu lemmikkirjed
+
+# search.jsp
+search.title = Otsing
+search.query = Esitaja, album v\u00f5i laulu pealkiri
+search.search = Otsi
+search.index = Loodi otsingu indeks. Palun proovi hiljem uuesti.
+search.hits.none = Tulemusi ei leitud.
+search.hits.more = Rohkem
+search.hits.artists = Esitajad
+search.hits.albums = Albumid
+search.hits.songs = Laulud
+
+# gettingStarted.jsp
+gettingStarted.title = Alustamine
+gettingStarted.text = <p>Tere tulemast Subsonicusse! Me viime teid kurssi koheselt, selleks tuleb teil j\u00e4rgida antud p\u00f5hisamme.<br> \
+ Siia lehele naasmiseks kliki nuppu "Kodu" k\u00f5rvalolevas t\u00f6\u00f6riistaribal.</p> \
+ <p>Lisainfo saamiseks v\u00f5tke \u00fchendust <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Alustamine</b></a> juhendist.</p>
+gettingStarted.root = Hoiatus! Subsonicu protsess t\u00f6\u00f6tab kasutaja "root" volitusega. Kaaluge selle seade \
+ <a href="http://subsonic.org/pages/installation.jsp" target="_blank">muutmist.</a>
+gettingStarted.step1.title = Muuda administraatori parooli.
+gettingStarted.step1.text = Muutke administraatori konto tavaparooli, et server oleks turvatud. \
+ Saate ka luua uusi kasutajakontosid erinevate juurdep\u00e4\u00e4sudega.
+gettingStarted.step2.title = Seadista meediakaustad.
+gettingStarted.step2.text = Anna Subsonicule teada oma muusika ja videote hoiupaigast.
+gettingStarted.step3.title = Seadista v\u00f5rguseadeid.
+gettingStarted.step3.text = Kui soovite nautide muusikat Interneti abil v\u00f5i tahate jagada seda koos oma pere ja s\u00f5pradega, siis vajalikud seadistused leiate siit, \
+ Hangi endale oma <b><em>sinunimi</em>.subsonic.org</b> \
+ aadress.
+gettingStarted.hide = \u00c4ra enam kuva seda
+gettingStarted.hidealert = Kui soovite seda teadet hiljem uuesti kuvada, siis leiate selle valiku alt Seaded > P\u00f5hiline.
+
+# home.jsp
+home.random.title = Suvaline
+home.alphabetical.title = K\u00f5ik
+home.newest.title = Viimati lisatud
+home.starred.title = Lemmikud
+home.highest.title = Hinnatuimad
+home.frequent.title = Popimad
+home.recent.title = Viimatu esitatud
+home.users.title = Kasutajad
+home.random.text = Suvalised albumid
+home.alphabetical.text = K\u00f5ik albumid
+home.newest.text = Hiljuti lisatud albumib
+home.starred.text = Sinu poolt lemmikuks m\u00e4rgitud albumid
+home.highest.text = Hinnatuimad albumid
+home.frequent.text = Popimad albumid
+home.recent.text = Hiljuti esitatud albumid
+home.users.text = Kasutajate statistika
+home.scan = Hetkel sk\u00e4nneeritakse meediakausta. M\u00f5ned v\u00f5imalused pole saadaval.
+home.listsize = {0} albumit lehek\u00fclje kohta
+home.albums = Albumid {0} - {1}
+home.playcount = Esitatud {0} lugu
+home.lastplayed = Esitati {0}
+home.created = Loodud {0}
+home.chart.total = Kokku (MB)
+home.chart.stream = Striimitud (MB)
+home.chart.download = Allalaetud (MB)
+home.chart.upload = \u00dcles laetud (MB)
+
+# more.jsp
+more.title = Rohkem
+more.random.title = Suvaline esitusloend
+more.random.text = Loo suvalisi esitusloendeid koos
+more.random.songs = {0} lugu
+more.random.auto = Esitusloendi l\u00f5ppedes esita rohkem suvalisi lugusid.
+more.random.ok = OK
+more.random.genre = \u017eanri j\u00e4rgi
+more.random.anygenre = suvalise \u017eanri
+more.random.year = ja aasta
+more.random.anyyear = Suvaline
+more.random.folder = kaustas
+more.random.anyfolder = Suvaline
+more.apps.title = Subsonicu rakendused
+more.apps.text = <p>Minge uurige uusi ja \u00e4gedaid <a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonicu rakendusi</a>. \
+ Need teevad sinu meedia kollektsiooni nautimise l\u00f5busamaks ja pakub alternatiivseid viise selleks - asukohast hoolimata. \
+ Rakendused on saadavad j\u00e4rgmistele nutitelefonidele: Android, iPhone, Windows Phone, PlayBook, Roku ja paljud teised.</p>
+more.mobile.title = Mobiiltelefon
+more.mobile.text = <p>Sa haldad {0} igalt WAP-toetatud mobiiltelefonilt v\u00f5i PDA kaudu.<br> \
+ Selleks k\u00fclastage j\u00e4rgnevat portaali oma mobiiltelefonis: <b>http://sinuv\u00f5rgunimi/wap</b></p> \
+ <p>N\u00f5udmiseks on ligip\u00e4\u00e4s Internetile.</p>
+more.podcast.title = Taskupleier
+more.podcast.text = <p>Salvestatud esitusloendid on saadaval ka taskupleieritena.<br>\
+ Kasuta j\u00e4rgnevat portaali oma Taskupleieris: <b>http://Sinuv\u00f5rgunimi/podcast</b>, \
+ v\u00f5i <b><a href="podcast.view?suffix=.rss">kliki siia</a>.</b></p>
+more.upload.title = Lae \u00fcles fail
+more.upload.source = Vali fail
+more.upload.target = Lae \u00fcles asukohta
+more.upload.browse = Vali
+more.upload.ok = Lae \u00fcles
+more.upload.unzip = Automaatselt paki lahti zip-fail.
+more.upload.progress = % on \u00fcles laetud. Palun oota...
+
+# upload.jsp
+upload.title = Faili \u00fcleslaadimine
+upload.success = Edukalt on \u00fcles laetud <b>{0}</b>
+upload.empty = \u00dcleslaadimiseks pole saadaval faile.
+upload.failed = \u00dcleslaadimin luhtus j\u00e4rgneva veateatega:<br><b>"{0}"</b>
+upload.unzipped = {0} on lahti pakitud
+
+# help.jsp
+help.title = Info {0}\u00b4i kohta
+help.upgrade = <b>Note!</b> Saadaval on uuem versioon. Lae alla {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">siit</a>.
+help.version.title = Versioon
+help.builddate.title = Valmistamiskuup\u00e4ev
+help.server.title = Server
+help.license.title = Kasutajatingimused
+help.license.text = {0} on vabavaraline tarkvara levitaja <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> avatud l\u00e4htekoodiga lepingu alusel. \
+ {0} kasutab <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">litsenseeritud kolmanda osapoole lisasid</a>. Pea meeles, et {0} <em>pole</em> \
+ v\u00f5imalus, mille abil rikutakse autori\u00f5igusi. Alati pane t\u00e4hele ning j\u00e4rgi oma riigile vastavaid p\u00f5hiseaduseid.
+help.homepage.title = Kodulehek\u00fclg
+help.forum.title = Foorum
+help.shop.title = \u0160oppa
+help.contact.title = V\u00f5ta \u00fchendust
+help.contact.text = {0}u arendajaks ja \u00fclalpidajaks on Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Kui sul on k\u00fcsimusi, kommentaare v\u00f5i soovitusi portaali t\u00e4iustamiseks siis palun k\u00fclasta \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonicu foorumit</a>.
+help.donate = {0}u tavaline versioon on tasuta k\u00e4ttesaadav, kuid te v\u00f5ite lisaks hankida suurel hulgal <a href="donate.view">lisav\u00f5imalusi</a>, selleks tehke portaalile <b><a href="donate.view?">annetus</a></b>.
+help.log = Logi
+help.logfile = Kokkuv\u00f5tlik logi on salvestatud asukohta {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Seaded
+settingsheader.general = P\u00f5hiline
+settingsheader.advanced = T\u00e4iustused
+settingsheader.personal = Isiklik
+settingsheader.musicFolder = Meediakaustad
+settingsheader.internetRadio = Interneti TV/raadio
+settingsheader.podcast = Taskupleier
+settingsheader.player = Meediaesitajad
+settingsheader.share = Jagatud meedia
+settingsheader.network = V\u00f5rk
+settingsheader.transcoding = Transkodeerimine
+settingsheader.user = Kasutajad
+settingsheader.search = Otsing/vahem\u00e4lu
+settingsheader.coverArt = Albumi kaaned
+settingsheader.password = Parool
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Esitusloendi kaust
+generalsettings.musicmask = Muusikafailid
+generalsettings.videomask = Videofailid
+generalsettings.coverartmask = Albumi kaante failid
+generalsettings.index = Indeks
+generalsettings.ignoredarticles = Eiratavad artiklid
+generalsettings.shortcuts = Otseteed
+generalsettings.showgettingstarted = Kuva k\u00e4ivitumisel teade "Sissejuhatus"
+generalsettings.welcometitle = Tervituse pealkiri
+generalsettings.welcomesubtitle = Tervituse subtiiter
+generalsettings.welcomemessage = Tervituse teade
+generalsettings.loginmessage = Sisselogimisteade
+generalsettings.language = P\u00f5hiline keel
+generalsettings.theme = P\u00f5hiline teema
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Asetuse muutmise k\u00e4sk
+advancedsettings.coverartlimit = Albumi kaane piirang<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.downloadlimit = Allalaadimispiirang (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.uploadlimit = \u00dcleslaadimise piirang (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.streamport = Mitte-SSL striimi v\u00f5rk<br><div class="detail">(0 = Disabled)</div>
+advancedsettings.ldapenabled = Luba LDAP kinnitamine
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP otsingufilter
+advancedsettings.ldapmanagerdn = LDAP haldur DN<br><div class="detail">(Valikuline)</div>
+advancedsettings.ldapmanagerpassword = Parool
+advancedsettings.ldapautoshadowing = Automaatselt loo kasutajad nimega {0}
+
+# personalSettings.jsp
+personalsettings.title = Isiklikud seaded {0}u jaoks
+personalsettings.language = Keel
+personalsettings.theme = Teema
+personalsettings.display = Kuvamine
+personalsettings.browse = Sirvi
+personalsettings.playlist = Esitusloend
+personalsettings.tracknumber = Lugu #
+personalsettings.artist = Esitaja
+personalsettings.album = Album
+personalsettings.genre = \u017danr
+personalsettings.year = Aasta
+personalsettings.bitrate = Pikkus
+personalsettings.format = Formaat
+personalsettings.filesize = Faili suurus
+personalsettings.captioncutoff = Lause v\u00e4ljal\u00fclitus
+personalsettings.partymode = Peomeeleolu
+personalsettings.shownowplaying = Kuva, mida teised kuulavad
+personalsettings.nowplayingallowed = Luba teistel n\u00e4ha, mida ma kuulan
+personalsettings.showchat = Kuva vestlusakna teateid
+personalsettings.finalversionnotification = Uute versioonide v\u00e4ljastamisest teavita mind
+personalsettings.betaversionnotification = Uute Beta-versioonide v\u00e4ljastamisest teavita mind
+personalsettings.lastfmenabled = Registreeri minu kuulatavate lugude loend portaali <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm\u00b4i kasutajanimi
+personalsettings.lastfmpassword = Last.fm\u00b4i parool
+personalsettings.avatar.title = Isiklik pilt
+personalsettings.avatar.none = Pilt puudub
+personalsettings.avatar.custom = Kohandatud pilt
+personalsettings.avatar.changecustom = Muuda kohandatud pilti
+personalsettings.avatar.upload = Lae \u00fcles
+personalsettings.avatar.courtesy = Ikoonide omanikuks on <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Muuda isiklikku pilti
+avataruploadresult.success = Isiklik pilt "{0}" on edukalt \u00fcles laetud.
+avataruploadresult.failure = Isikliku pildi \u00fcleslaadimine luhtus. Detailide n\u00e4gemiseks uuri <a href="help.view?">logi</a>.
+
+# passwordSettings.jsp
+passwordsettings.title = Muuda {0} parooli
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Kaust
+musicfoldersettings.name = Nimi
+musicfoldersettings.enabled = Lubatud
+musicfoldersettings.add = Lisa meediakaust
+musicfoldersettings.nopath = Palun t\u00e4psusta kausta.
+musicfoldersettings.notfound = Kausta ei leitud
+musicfoldersettings.scan = Sk\u00e4nneeri meediakauste
+musicfoldersettings.interval.never = Mitte iial
+musicfoldersettings.interval.one = Iga p\u00e4ev
+musicfoldersettings.interval.many = Iga {0}-p\u00e4eva tagant
+musicfoldersettings.hour = kell {0}:00
+musicfoldersettings.nowscanning = Hetkel sk\u00e4nneeritakse meediakauste. Sk\u00e4nneerimine v\u00f5tab aega m\u00f5ned minutid, see s\u00f5ltub \
+ sinu meediakogu suurusest.
+musicfoldersettings.scannow = Sk\u00e4nneeri meediakaustasi n\u00fc\u00fcd
+musicfoldersettings.fastcache = Kiire sisenemismeetod
+musicfoldersettings.fastcache.description = Selle valiku valimisel v\u00e4hendatakse k\u00f5vaketta koormust, n\u00e4iteks, kui sinu meediafailid asuvad interneti serveris. \
+ M\u00e4rkus: Failidele j\u00f5ustuvad muudatused p\u00e4rast meediakaustade sk\u00e4nneeringut.
+
+musicfoldersettings.organizebyfolderstructure = Organiseeri kausta \u00fclesehituse j\u00e4rgi
+musicfoldersettings.organizebyfolderstructure.description = Kasuta seda valikut, et sirvida oma meediakogu sihtkoha \u00fclesehituse j\u00e4rgi. Selle asemel, et kasutada ID3 teekide esitaja/albumi andmeid.
+
+# networkSettings.jsp
+networksettings.text = Kasuta seda seadet, et hallata ligip\u00e4\u00e4su Subsonicu serverisse Interneti kaudu.<br> \
+ Kui ilmneb raskusi siis tutvuge <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Sissejuhatuse</b></a> peat\u00fckiga.
+networksettings.portforwardingenabled = Automaatselt seadista oma ruuter, et Subsonicu \u00fchendused oleks alati lubatud (kasutatakse UPnP v\u00f5i NAT-PMP pordi saatmist).
+networksettings.portforwardinghelp = Ruuterit on ka v\u00f5imalik seadistada k\u00e4sitsi, kui automaatselt ei saa. \
+ J\u00e4rgi juhendeid portaalil <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Sa pead saatma porti {0}, et Subsonicu server t\u00f6\u00f6taks korrektselt.
+networksettings.urlredirectionenabled = Hangi h\u00e4sti meeldej\u00e4\u00e4v aadress, et ligip\u00e4\u00e4s oma serverile Interneti kaudu oleks lihtsustatud.
+networksettings.status = Olek:
+networksettings.trialexpired = Prooviversioon aegus {0}. Palun <b><a href="donate.view?">anneta</a></b>, et lubada see v\u00f5imalus ajutiselt.
+networksettings.trialnotexpired = See v\u00f5imalus on saadaval kuni {0}. P\u00e4rast aegumist on v\u00f5imalik seda ajutiselt edasi kasutada, selleks peate tegema<b><a href="donate.view?">annetuse</a></b>.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nimi
+transcodingsettings.sourceformat = Konverdi formaadist
+transcodingsettings.targetformat = formaati
+transcodingsettings.step1 = 1. samm
+transcodingsettings.step2 = 2. samm
+transcodingsettings.step3 = 3. samm
+transcodingsettings.add = Lisa konverditav
+transcodingsettings.defaultactive = Luba see konvertimine k\u00f5ikidel olemasolevatel ja uutele meediaesitajatel.
+transcodingsettings.recommended = Soovitatavad seadistused
+transcodingsettings.noname = T\u00e4psusta nimi.
+transcodingsettings.nosourceformat = T\u00e4psusta, mis formaadist konverditakse.
+transcodingsettings.notargetformat = T\u00e4psusta, mis formaati konverditakse.
+transcodingsettings.nostep1 = Palun t\u00e4psusta v\u00e4hemalt \u00fcks konvertimise samm.
+transcodingsettings.info = <p class="detail">(%s = Konverditav fail, %b = esitaja suurim kvaliteet, %t = pealkiri, %a = esitaja, %l = Album)</p> \
+ <p>Transkodeerimine on protsess, kus meediafaili konverditakse \u00fchest formaadist teise. {1}''i transkodeerimise \
+ s\u00fcsteem v\u00f5imaldab meedial, mida muidu ei suudetaks siin esitada, esitada peale formaadi muutmist . Transkodeerimine toimub linnulennul ja ei \
+ n\u00f5ua \u00fcldse k\u00f5vaketta kasutamist.<p/> \
+ <p>Tegelik transkodeerimise protsess toimub kolmanda osapoolte k\u00e4surea programmide abil, mis peavad olema paigaldatud asukohta {0}. \
+ V\u00f5id lisada ka omaenda kohandatud transkooderi, kui see vastab j\u00e4rgnevatele n\u00f5uetele: \
+ <ul> \
+ <li>Sel peab olema k\u00e4surea kasutajaliides.</li> \
+ <li>See peab olema suuteline saatma v\u00e4ljundi stdout\u00b4i.</li> \
+ <li>Kui kasutada teises sammus siis sellel v\u00f5iks olla sisseehitatud stdin\u00b4i sisendi lugemise funkstioon.</li> \
+ </ul> \
+ </p> \
+ <p> Pane t\u00e4hele, et transkodeeringud aktiveeritakse eel-esitaja basis asukohast <b>Seaded &gt; Esitajad</b>.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Striimi URL
+internetradiosettings.homepageurl = Kodulehek\u00fclg
+internetradiosettings.name = Nimi
+internetradiosettings.enabled = Lubatud
+internetradiosettings.add = Lisa Interneti TV/raadio
+internetradiosettings.nourl = Palun t\u00e4psusta URLi.
+internetradiosettings.noname = Palun t\u00e4psusta nimi.
+
+# podcastSettings.jsp
+podcastsettings.update = Kontrolli, kas on saadaval uued osad
+podcastsettings.keep = J\u00e4ta alles
+podcastsettings.keep.all = K\u00f5ik osad
+podcastsettings.keep.one = Viimane osa
+podcastsettings.keep.many = {0} viimast osa
+podcastsettings.download = Kui uued osad on saadaval
+podcastsettings.download.all = Lae alla k\u00f5ik
+podcastsettings.download.one = Lae alla k\u00f5ige uuem osa
+podcastsettings.download.many = Lae alla {0} viimast osa
+podcastsettings.download.none = \u00c4ra tee midagi
+podcastsettings.interval.manually = K\u00e4sitsi
+podcastsettings.interval.hourly = Iga tund
+podcastsettings.interval.daily = Iga p\u00e4ev
+podcastsettings.interval.weekly = Iga n\u00e4dal
+podcastsettings.folder = Salvesta taskupleierid asukohta
+
+# playerSettings.jsp
+playersettings.noplayers = Meediaesitajaid ei leitud.
+playersettings.type = T\u00fc\u00fcp
+playersettings.lastseen = Viimati n\u00e4htud
+playersettings.title = Vali esitaja
+playersettings.technology.web.title = Veebiesitaja
+playersettings.technology.external.title = V\u00e4line esitaja
+playersettings.technology.external_with_playlist.title = V\u00e4line esitaja koos esitusloendiga
+playersettings.technology.jukebox.title = Kukeboks
+playersettings.technology.web.text = Kasutage sisseehitatud Flash esitajat, et esitada muusikat otse veebilehitsejast.
+playersettings.technology.external.text = Esita muusikat oma lemmikesitaja kaudu nagu n\u00e4iteks WinAmp v\u00f5i Windows Media Player.
+playersettings.technology.external_with_playlist.text = Sama, mis k\u00f5rvalolev aga esitusloendit haldab meediaesitaja, \
+ Subsonicu serveri asemel. See olek v\u00f5imaldab vahele j\u00e4tta lugusid.
+playersettings.technology.jukebox.text = Esita muusikat Subsonicu serveri audioseadme kaudu. (Kinnitatud kasutajaile ainult).
+playersettings.name = Esitaja nimi
+playersettings.coverartsize = Albumi esikaane suurus
+playersettings.maxbitrate = Suurim kvaliteet
+playersettings.coverart.off = V\u00e4ljas
+playersettings.coverart.small = V\u00e4ike
+playersettings.coverart.medium = Keskmine
+playersettings.coverart.large = Suur
+playersettings.nolame = <em>T\u00e4helepanek:</em> Tundub, et transkooder pole paigaldatud.<br>Lisainfoks klikkige nupul Abi.
+playersettings.autocontrol = Juhi meediaesitajat automaatselt
+playersettings.dynamicip = Meediaesitajal on d\u00fcnaamiline IP-aadress
+playersettings.transcodings = Aktiivsed transkodeeringud
+playersettings.ok = Salvesta
+playersettings.forget = Kustuta meediaesitaja
+playersettings.clone = Klooni meediaesitajat
+
+# shareSettings.jsp
+sharesettings.name = Nimi
+sharesettings.owner = Jagajaks on
+sharesettings.description = Kirjeldus
+sharesettings.visits = K\u00fclastused
+sharesettings.lastvisited = Viimati k\u00fclastatud
+sharesettings.expires = Aegub
+sharesettings.files = Jagatud failid
+sharesettings.expirein = Aegub
+sharesettings.expirein.week = 1n
+sharesettings.expirein.month = 1k
+sharesettings.expirein.year = 1a
+sharesettings.expirein.never = mitte iial
+
+# userSettings.jsp
+usersettings.title = Vali kasutaja
+usersettings.newuser = Uus kasutaja
+usersettings.admin = Kasutaja on administraator
+usersettings.settings = Kasutajal on lubatud muuta seadeid ja parooli
+usersettings.stream = Kasutajal on lubatud esitada faile
+usersettings.jukebox = Kasutajal on lubatud esitada faile kukeboksi olekus
+usersettings.download = Kasutajal on lubatud faile alla laadida
+usersettings.upload = Kasutajal on lubatud faile \u00fcles laadida
+usersettings.share = Kasutajal on lubatud faile jagada k\u00f5igia
+usersettings.playlist= kasutajal on lubatud luua ja kustutada esitusloendeid
+usersettings.coverart = Kasutajal on lubatud muuta albumi esikaant ja infot
+usersettings.comment= Kasutajal on lubatud luua ja muuta kommentaare ning hinnanguid
+usersettings.podcast= Kasutajal on lubatud Taskupleieritel end omanikuks nimetada
+usersettings.username = Kasutajanimi
+usersettings.email = E-post
+usersettings.changepassword = Muuda parooli
+usersettings.password = Parool
+usersettings.newpassword = Uus parool
+usersettings.confirmpassword = Kinnita parool
+usersettings.delete = Kustuta see kasutaja
+usersettings.ldap = Kinnita kasutaja LDAP-s
+usersettings.nousername = Kasutajanimi on kadunud.
+usersettings.noemail= E-posti aadress on vigane.
+usersettings.useralreadyexists = Kasutaja on juba olemas.
+usersettings.nopassword = N\u00f5utav on parool.
+usersettings.wrongpassword = Paroolid ei kattunud.
+usersettings.ldapdisabled = LDAP kinnitus pole lubatud. Vaata T\u00e4psemaid seadeid.
+usersettings.passwordnotsupportedforldap = Ei suuda m\u00e4\u00e4rata ega muuta LDAP-kinnitatud kasutajate paroole.
+usersettings.ok = Kasutaja {0} parool on edukalt muudetud.
+
+# main.jsp
+main.up = \u00dcles
+main.playall = Esita k\u00f5ik
+main.playrandom = Esita suvaliselt
+main.addall = Lisa k\u00f5ik
+main.tags = Muuda teeke
+main.playcount = Esitatud {0} korda.
+main.lastplayed = Viimati esitati {0}.
+main.comment = Kommentaar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>New paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Named link</td></tr>\
+ </table>
+main.sharealbum = Jaga
+main.more = Rohkem tegevusi...
+main.more.selection = Valitud lood
+main.more.share = Jaga
+main.donate = <a href="{0}" style="text-decoration:underline">Anneta</a> {1}-le!<br>(ja eemalda see reklaam)
+main.nowplaying = Hetkel esitamisel
+main.lyrics = Laulus\u00f5nad
+main.minutesago = minutit tagasi
+main.chat = Vestlusakna teated
+main.scanning = Failide sk\u00e4nneerimine:
+main.message = Kirjuta teade
+main.clearchat = Puhasta teated
+
+# rating.jsp
+rating.rating = Hindamine
+rating.clearrating = Puhasta hindamised
+
+# coverArt.jsp
+coverart.change = Muuda
+coverart.zoom = Suurenda
+
+# allmusic.jsp
+allmusic.text = Albumi <em>{0}</em> otsimine portaalist allmusic.com - Palun oota.
+
+# changeCoverArt.jsp
+changecoverart.title = Muuda albumi esikaant
+changecoverart.address = V\u00f5i sisesta pildi aadress
+changecoverart.artist = Esitaja
+changecoverart.album = Album
+changecoverart.search = Google pildiotsing
+changecoverart.wait = Palun oota...
+changecoverart.success = Pilt on edukalt alla laetud.
+changecoverart.error = Pildi allalaadimine eba\u00f5nnestus.
+changecoverart.noimagesfound = Pilte ei leitud.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Albumi esikaane muutmine eba\u00f5nnestus:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Muuda andmeid
+edittags.file = Fail
+edittags.track = Lugu
+edittags.songtitle = Pealkiri
+edittags.artist = Esitaja
+edittags.album = Album
+edittags.year = Aasta
+edittags.genre = \u017danr
+edittags.status = Olek
+edittags.suggest = Soovitus
+edittags.reset = Taasv\u00e4\u00e4rtusta
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = M\u00e4\u00e4ra
+edittags.working = T\u00f6\u00f6tlemine
+edittags.updated = Uuendatud
+edittags.skipped = Vahele j\u00e4etud
+edittags.error = Viga
+
+# share.jsp
+share.title = Jaga
+share.warning = <h2>T\u00c4HTIS TEADE!</h2><p>M\u00e4ngi ausat m\u00e4ngu &ndash; \u00c4rge jagage autori\u00f5igusega kaitstud faile, mis jagamine on keelatud teie riigi p\u00f5hiseadusega ning v\u00f5ib kaasa tuua vanglakaristuse.</p>
+share.facebook = Jaga Facebookis
+share.twitter = Jaga Twitteris
+share.googleplus = Jaga Google+-is
+share.link = V\u00f5i saatmiseks kasutage linki: <a href="{0}" target="_blank">{0}</a>
+share.disabled = Esmalt, et saaksite muusikat jagada kellegagi, peate te registreerima oma <em>subsonic.org</em> aadressi.<br> \
+ Palun mine <a href="networkSettings.view"><b>Seaded &gt; V\u00f5rk</b></a> (administraatori \u00f5igus on vajalik).
+share.manage = Halda minu jagatud meediat
+
+# donate.jsp
+donate.title = Anneta
+donate.invalidlicense = Litsentsiv\u00f5ti on vigane.
+donate.amount = Anneta {0}
+
+donate.textbefore = <p>T\u00e4name v\u00e4ga, et v\u00f5tsite vaevaks annetada projekti {0}! \
+ Annetajad saavad ligip\u00e4\u00e4su lisav\u00f5imalustele nagu n\u00e4iteks:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Rakendused</a> operatsioonis\u00fcsteemidele Android, iPhone ja Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Rakendused</a> operatsioonis\u00fcsteemidele PlayBook, Roku, Mac, Chrome ja teistele*.</li> \
+ <li>Video striimimine.</li> \
+ <li>Teie isiklik serveri aadress: <em>sinunimi</em>.subsonic.org (Mine <a href="networkSettings.view">Seaded &gt; V\u00f5rk</a>).</li> \
+ <li>Jaga oma meediat suhtlusportaalides Facebook, Twitter, Google+.</li> \
+ <li>Kasutajaliides ei sisalda reklaame.</li> \
+ <li>Rohkem v\u00f5imalusi tuleb hiljem juurde.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Kolmanda osapoole arendajad m\u00fc\u00fcvad teatud rakendusi.</p>\
+ <p>Peale annetamist saate te isikliku litsentsiv\u00f5tme, mis on m\u00f5eldud ainult enda jaoks, mitte reklaamimise p\u00f5him\u00f5ttel \
+ ja k\u00f5ikide tulevaste {0}u uuenduste. Kui on soovi reklaamimiseks, palun <a href="mailto:subsonic_donation@activeobjects.no">v\u00f5tke \u00fchendust</a> meiega litsentsiv\u00f5tme asjus.</p> \
+ <p>Soovitatav summa annetamiseks on <b>&euro;20</b>, kuid Teil on v\u00f5imalik valida \u00fckstapuha, missugune summa:</p>
+donate.textafter = <p>Kliki nupul, et saaksite edasi minna PayPali, kus saate maksta krediitkaardi v\u00f5i \
+ oma PayPal konto kaudu (juhul, kui omate seda). Peale maksmist m\u00f5nede minutite jooksul saate litsentsiv\u00f5tme e-posti teel .</p> \
+ <p>Kui teil on k\u00fcsimusi maksmise suhtes, siis saatke k\u00fcsimus j\u00e4rgnevale e-posti aadressile \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = {2}u kopeering on litsentsitud {0}-le {1}-s. T\u00e4name toetuse eest!
+donate.register = P\u00e4rast litsentsiv\u00f5tme saabumist sisestaga see siia.
+donate.resend = Litsents on juba ostetud, kuid kaotsi l\u00e4inud? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Saada uuesti</a>.
+donate.register.email = E-post
+donate.register.license = Litsents
+
+# podcastReceiver.jsp
+podcastreceiver.title = Taskupleierite hankija
+podcastreceiver.expandall = Kuva osad
+podcastreceiver.collapseall = Peida osad
+podcastreceiver.status.new = Uus
+podcastreceiver.status.downloading = Allalaadimine
+podcastreceiver.status.completed = Valmis
+podcastreceiver.status.error = Viga
+podcastreceiver.status.deleted = Kustutatud
+podcastreceiver.status.skipped = Vahele j\u00e4etud
+podcastreceiver.downloadselected= Lae alla valitud
+podcastreceiver.deleteselected= Kustuta valitud
+podcastreceiver.confirmdelete= T\u00f5esti soovite kustutada valitud taskupleierid?
+podcastreceiver.check = Kontrolli uute osade saadavust
+podcastreceiver.refresh = V\u00e4rskenda lehek\u00fclge
+podcastreceiver.settings = Taskupleieri seaded
+podcastreceiver.subscribe = Telli taskupleier
+
+# lyrics.jsp
+lyrics.title = Laulus\u00f5nad
+lyrics.artist = Esitaja
+lyrics.song = Lugu
+lyrics.search = Otsi
+lyrics.wait = Laulus\u00f5nade otsimine, palun oota...
+lyrics.courtesy = (Laulus\u00f5nade omanik on <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Laulus\u00f5nu ei leitud.
+
+# helpPopup.jsp
+helppopup.title = {0}u Abi
+helppopup.cover.title = Albumi kaane suurus
+helppopup.cover.text = <p>V\u00f5imaldab m\u00e4\u00e4rata kuvatava albumi esikaane suurust , koos valikuga.</p>
+helppopup.transcode.title = Suurim sagedus
+helppopup.transcode.text = <p>Kui teil on constrained kvaliteediga faile, siis v\u00f5ite kvaliteeti suurendada. \
+ N\u00e4iteks, kui sinu originaalsed mp3 failid on enkodeeritud kvaliteediga 256 Kbps (kilobitti sekundi kohta), suurima kvaliteedi m\u00e4\u00e4ramine \
+ 128-ks muudab {0} automaatselt muusikapala 256-st 128 Kbps kvaliteedini.</p>
+helppopup.playlistfolder.title = Esitusloendi kaust
+helppopup.playlistfolder.text = <p>V\u00f5imaldab m\u00e4\u00e4rata Teie esitusloendite asukohta.</p>
+helppopup.musicmask.title = Muusikafailid
+helppopup.musicmask.text = <p>V\u00d5imaldab valida faile, mis kuuluvad muusika hulka.</p>
+helppopup.videomask.title = Video failid
+helppopup.videomask.text = <p>V\u00f5imaldab valida faile, mis kuuluvad videote hulka.</p>
+helppopup.coverartmask.title = Albumite esikaante failid
+helppopup.coverartmask.text = <p>V\u00f5imaldab valida faile, mis kuuluvad albumi esikaane hulka, kui sirvite meediakausta.</p>
+helppopup.downsamplecommand.title = Kvaliteedi v\u00e4hendamise k\u00e4sk
+helppopup.downsamplecommand.text = <p>V\u00f5imaldab t\u00e4psustada k\u00e4sku, kui alandatakse v\u00e4iksemale kvaliteedile lahti pakkimisel.</p>\
+ <p>(%s = Fail, millel v\u00e4hendatakse kvaliteeti, %b = Meediapleieri suurim kvaliteet, %t = Pealkiri, %a = Esitaja, %l = Album)</p>
+helppopup.index.title = Indeks
+helppopup.index.text = <p>V\u00f5imaldab t\u00e4psustada indeksi (asub ekraani vasakul pool) ilmumist. Indeksi abil on v\u00f5imalik kergelt \
+ ligi p\u00e4\u00e4seda peamisesse meediakausta.</p> \
+ <p>T\u00e4psustusteks v\u00f5ib olla indeksi sisestused, mis on eraldatud t\u00fchikuga. Tavaliselt iga sisestus koosneb \u00fchest t\u00e4hest, \
+ kui v\u00f5ite ka t\u00e4psustada mitu t\u00e4hte koos. N\u00e4iteks, sisestus <em>The</em> suunab \u00fcmber k\u00f5ikide failideni ja \
+ kaustadeni, mis algavad s\u00f5naga "The".</p> \
+ <p>V\u00f5ite ka lisada sulgudesse indeksi t\u00e4hem\u00e4rke, et luua sisestus. N\u00e4iteks, sisestust \
+ <em>A-E(ABCDE)</em> kuvatakse <em>A-E</em> ja link, mis v\u00f5ib alata \
+ A, B, C, D v\u00f5i E-ga viib k\u00f5ikidesse failidesse ja kaustadesse. See tuleb kasuks harvemini esinevate t\u00e4hem\u00e4rkide (nagu X, Y ja Z) v\u00f5i \
+ v\u00f5i t\u00e4pit\u00e4htede grupeerimisel (nagu A, \u00c0 ja \u00c1)</p> \
+ <p>Failid ja kaustad, mis pole m\u00e4\u00e4ratud t\u00e4psustatud indeksi sisestusega, t\u00e4histatakse sisestusega "#".</p>
+helppopup.ignoredarticles.title = Artiklid, mida eiratakse
+helppopup.ignoredarticles.text = <p>Lubab indeksi loomisel t\u00e4psustava artiklite listi luua, mida eiratakse (n\u00e4iteks "The").</p>
+helppopup.shortcuts.title = Otseteed
+helppopup.shortcuts.text = <p>Tipptasemel kaustade loend, kuhu loodakse otseteid ja mis on t\u00fchikutega eraldatud. S\u00f5nade grupeerimiseks kasutage jutum\u00e4rke, n\u00e4iteks:</p> \
+ <p><em>Uued sissetulevad "Helilood"</em></p>
+helppopup.language.title = Keel
+helppopup.language.text = <p>Lubab valida sobiva keele kasutajaliidesele.</p>
+helppopup.visibility.title = L\u00e4bipaistvus
+helppopup.visibility.text = <p>Vali, mida kuvatakse iga loo kohta, lisaks ka pealkirja v\u00e4ljal\u00fclitamise. See on \
+ laulu pealkirja, albumi ja esitaja t\u00e4hem\u00e4rkide suurim kuvamishulk.</p>
+helppopup.partymode.title = Peomeeleolu
+helppopup.partymode.text = <p>Peomeeleolu sissel\u00fclitusel on kasutajaliides lihtsustatud ja lihtsamini kasutatav tavaliste kasutajate jaoks. \
+ Eriti hoiab \u00e4ra kogemata esitusloendite botching.</p>
+helppopup.theme.title = Teema
+helppopup.theme.text = <p>Lubab kasutada valitud teemat. Teema m\u00e4\u00e4ratleb {0}u v\u00e4limust v\u00e4rvide, fontide, piltide jne hulgas.</p>
+helppopup.welcomemessage.title = Tervitusteade
+helppopup.welcomemessage.text = <p>Teade, mida kuvatakse kodulehel.</p>
+helppopup.loginmessage.title = Sisselogimisteade
+helppopup.loginmessage.text = <p>Teade, mida kuvatakse p\u00e4rast sisselogimist.</p>
+helppopup.coverartlimit.title = Albumi esikaane piirang
+helppopup.coverartlimit.text = <p>Albumite esikaante suurim hulk \u00fche lehek\u00fclje kohta.</p>
+helppopup.downloadlimit.title = Alla laadimise piirang
+helppopup.downloadlimit.text = <p>M\u00e4\u00e4rab, kui suurel hulgal andmeside kasutatakse \u00fcleslaadimisel.</p>
+helppopup.uploadlimit.title = \u00dcles laadimise piirang
+helppopup.uploadlimit.text = <p>M\u00e4\u00e4rab, kui suurel hulgal andmeside kasutatakse \u00fcleslaadimisel.</p>
+helppopup.streamport.title = Striimi port, mis pole Non-SSL
+helppopup.streamport.text = <p>Seda valikut on tark kasutada juhul, kui {0} asub serveris, mis toetab SSL (HTTPS) \u00fchendust.</p><p>M\u00f5ned meediaesitajad \
+ (n\u00e4iteks Winamp) ei toeta esitamist SSL v\u00f5rgust. T\u00e4psusta tavalise http \u00fchenduse pordi numbrit (tavaliselt 80 \
+ v\u00f5i 4040), kui te ei soovi striimida SSL v\u00f5rgu kaudu. J\u00e4ta meelde, et striime ei kr\u00fcpteerita.</p>
+helppopup.ldap.title = LDAP kinnitus
+helppopup.ldap.text = <p>Kasutajaid on v\u00f5imalik kinnitada v\u00e4lise LDAP serveri kaudu (sisaldab Windows Active Directory-t). \
+ Kui LDAP-lubatud kasutajad logivad sisse {0}usse siis kasutajanimi ja parooli kontrollib v\u00e4line server, mitte {0}.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>LDAP serveri URL. Protokoll v\u00f5iks olla, kas <em>ldap://</em> v\u00f5i <em>ldaps://</em> \
+ (LDAP \u00fclekanne SSL-i kaudu). Vaata <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">siit</a>, \
+ et n\u00e4ha detailsemat kirjeldust.</p>
+helppopup.ldapsearchfilter.title = LDAP otsingufilter
+helppopup.ldapsearchfilter.text = <p>Filtri v\u00e4ljendust kasutatakse kasutajaotsingus. See on LDAP otsingufilter \
+ (selgituse leiate siit <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Kasutajanimi on asendatud mustriga "'{0'}", n\u00e4iteks: \
+ <ul>\
+ <li>(uid='{0'}) - see otsib kasutajanime tulemust uid omaduses.</li> \
+ <li>(sAMAccountName='{0'}) - t\u00fc\u00fcpiliselt kasutatakse Microsofti Active Directory kinnitusel.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP haldur DN
+helppopup.ldapmanagerdn.text = <p>Kui LDAP server ei toeta anon\u00fc\u00fcmset kohustust, mida pead t\u00e4psustama DN \
+ (<em>Distinguished Name (V\u00e4ljapaistev nimi)</em>) ja LDAPi kasutaja parooli kohustuse ajal.</p>
+helppopup.ldapautoshadowing.title = Loo automaatselt LDAP kasutajaid {0}us
+helppopup.ldapautoshadowing.text = <p>Kui see valik on valitud, siis LDAP kasutajad ei pea k\u00e4sitsi {0} enne sisselogimist.</p> \
+ <p>M\u00c4RGE! See t\u00e4hendab seda, et iga kasutaja saab kehtiva LDAP kasutajanimega ja parooliga logida sisse {0}usse, \
+ mis ei pruugi olla sinu soovide kohane.</p>
+helppopup.playername.title = Meediaesitaja nimi
+helppopup.playername.text = <p>Lubab anda meediaesitajale meeldej\u00e4\u00e4va nime nagu n\u00e4iteks "T\u00f6\u00f6" v\u00f5i "Elutuba".</p>
+helppopup.autocontrol.title = Halda meediaesitajat automaatselt
+helppopup.autocontrol.text = <p>Kui see valik on valitud, {0} k\u00e4ivitab meediaesitaja automaatselt, kui esitusloendis klikitakse "Esita". \
+ Muidu peate ise k\u00e4ivitama ja \u00fchendama meediaesitaja.</p>
+helppopup.dynamicip.title = D\u00fcnaamiline IP aadress
+helppopup.dynamicip.text = <p>Kui meediaesitaja kasutab staatilist IP-aadressi siis l\u00fclitage antud valik v\u00e4lja.</p>
+
+# wap/index.jsp
+wap.index.missing = Muusikat ei leitud
+wap.index.playlist = Esitusloend
+wap.index.search = Otsi
+wap.index.settings = Seaded
+
+# wap/browse.jsp
+wap.browse.playone = Esita lugu
+wap.browse.playall = Play all
+wap.browse.addone = Lisa lugu
+wap.browse.addall = Lisa k\u00f5ik
+wap.browse.downloadone = Lae alla lugu
+wap.browse.downloadall = Lae alla k\u00f5ik
+
+# wap/playlist.jsp
+wap.playlist.title = Esitusloend
+wap.playlist.noplayer = Meediaesitajad puuduvad
+wap.playlist.clear = Puhasta
+wap.playlist.load = Lae
+wap.playlist.random = Sega
+wap.playlist.play = Esita mobiiltelefonis
+
+# wap/search.jsp
+wap.search.title = Otsing
+
+# wap/searchResult.jsp
+wap.searchresult.index = Hetkel luuakse otsingu indeksit. Palun proovi hiljem uuesti.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Vali meediaesitaja
+wap.settings.allplayers = K\u00f5ik
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fi.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fi.properties
new file mode 100644
index 00000000..b83cf75b
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fi.properties
@@ -0,0 +1,675 @@
+#
+# Finnish localization.
+# Author: Reijo Jäärni
+#
+
+common.home = Etusivu
+common.back = Takaisin
+common.help = Tuki
+common.play = Toista
+common.add = Lisää soittolistalle
+common.download = Lataa
+common.close = Sulje
+common.refresh = Päivitä
+common.next = Seuraava
+common.previous = Edellinen
+common.more = Lisää
+common.ok = OK
+common.cancel = Peruuta
+common.save = Tallenna
+common.create = Luo
+common.delete = Poista
+common.unknown = (Tuntematon)
+common.default = (Oletus)
+
+# login.jsp
+login.username = Käyttäjätunnus
+login.password = Salasana
+login.login = Kirjaudu sisään
+login.remember = Muista minut
+login.logout = Olet nyt kirjautunut ulos.
+login.error = Väärä käyttäjätunnus tai salasana.
+login.insecure = {0} profiili ei ole turvallinen. Kirjaudu sisään käyttäjätunnuksella ja<br>salasanalla "admin", tai klikkaa <a href="login.view?user=admin&amp;password=admin">tässä</a>. Vaihda salasana välittömästi.
+
+# accessDenied.jsp
+accessDenied.title = Pääsy kielletty
+accessDenied.text = Valitettavasti sinulla ei ole lupaa suorittaa tätä toimintoa.
+
+# top.jsp
+top.home = Etusivu
+top.now_playing = Toisto
+top.settings = Asetukset
+top.status = Tilanne
+top.podcast = Podcastit
+top.more = Lisää
+top.help = Tuki
+top.search = Etsi
+top.upgrade = <b>Huomio!</b> Uusi ohjelmaversio on saatavilla.<br>Lataa {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">tästä</a>.
+top.missing = Musiikkikansioita ei löydy. Tarkista asetukset.
+top.logout = Kirjaa ulos {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artistia<br>\
+ {1}&nbsp;albumia<br>\
+ {2}&nbsp;kappaletta<br>\
+ {3} (&#126; {4} tuntia)
+left.shortcut = Oikopolut
+left.radio = Internet Tv/Radio
+left.allfolders = Kaikki kansiot
+
+# playlist.jsp
+playlist.stop = Seis
+playlist.start = Toista
+playlist.confirmclear = Tyhjennä soittolista?
+playlist.clear = Tyhjennä
+playlist.shuffle = Sekoita
+playlist.repeat_on = Jatkuva toisto
+playlist.repeat_off = Ei jatkuva toisto
+playlist.undo = Kumoa
+playlist.settings = Asetukset
+playlist.more = Lisää toimintoja...
+playlist.more.playlist = Soittolista
+playlist.more.sortbytrack = Lajittele raidan mukaan
+playlist.more.sortbyartist = Lajittele artistin mukaan
+playlist.more.sortbyalbum = Lajittele albumin mukaan
+playlist.more.selection = Valitut kappaleet
+playlist.more.selectall = Valitse kaikki
+playlist.more.selectnone = Peruuta valinta
+playlist.getflash = Lataa Flash player
+playlist.load = Toista soittolista
+playlist.save = Tallenna
+playlist.append = Lisää soittolistalle
+playlist.remove = Poista soittolistalta
+playlist.up = Ylös
+playlist.down = Alas
+playlist.empty = Soittolista on tyhjä
+
+# status.jsp
+status.title = Tilanne
+status.type = Tyyppi
+status.stream = Striimi
+status.download = Ladattu palvelimelta
+status.upload = Siirretty palvelimeen
+status.player = Soitin
+status.user = Käyttäjä
+status.current = Tiedosto
+status.transmitted = Lähetetty
+status.bitrate = Siirtonopeus (Kbps)
+
+# search.jsp
+search.title = Etsi
+search.search = Etsi
+search.index = Tietokantaa luodaan parhaillaan. Yritä hetken päästä uudelleen.
+search.hits.none = Ei osumia.
+
+# gettingStarted.jsp
+gettingStarted.title = Aloitusasetukset
+gettingStarted.text = <p>Tervetuloa käyttämään Subsonicia! Seuraavaksi käydään läpi perusasetukset.<br> \
+ Klikkaa "Etusivu" näppäintä ylhäällä valikossa päästäksesi takaisin tälle sivulle.</p>
+gettingStarted.step1.title = Muuta pääkäyttäjän salasana.
+gettingStarted.step1.text = Suojaa serveri muuttamalla pääkäyttäjätilin oletussalasana. \
+ Voit myös luoda uusia tilejä eri oikeuksin.
+gettingStarted.step2.title = Määritä musiikkikansioiden sijainti.
+gettingStarted.step2.text = Määritä Subsonic-ohjelmalle musiikkikansioiden sijainti.
+gettingStarted.step3.title = Määritä verkkoasetukset.
+gettingStarted.step3.text = Hyödyllisiä asetuksia jos haluat kuunnella musiikkia internetin yli. \
+ Määritä helposti muistettava <b><em>yourname</em>.subsonic.org</b> \
+ osoite.
+gettingStarted.hide = Älä näytä tätä sivua enää.
+gettingStarted.hidealert = Tämän sivun saat uudelleen näkyviin valitsemalla Asetukset > Yleiset.
+
+# home.jsp
+home.random.title = Satunnainen
+home.newest.title = Uudet
+home.highest.title = Suosituimmat
+home.frequent.title = Soitetuimmat
+home.recent.title = Viimeksi soitetut
+home.users.title = Käyttäjät
+home.random.text = Satunnaiset albumit
+home.newest.text = Viimeksi lisätyt tai muokatut albumit
+home.highest.text = Suosituimmat albumit
+home.frequent.text = Soitetuimmat albumit
+home.recent.text = Viimeksi soitetut albumit
+home.users.text = Käyttäjät yhteenveto
+home.scan = Hakutietokantaa päivitetään parhaillaan. Kaikki toiminnot eivät ole vielä käytössä.
+home.listsize = {0} albumia sivulla
+home.albums = Albumit {0} - {1}
+home.playcount = Soitettu {0} kappaletta
+home.lastplayed = Soitettu {0}
+home.created = Muokattu {0}
+home.chart.total = Yhteensä (MB)
+home.chart.stream = Striimattu (MB)
+home.chart.download = Ladattu palvelimelta (MB)
+home.chart.upload = Siirretty palvelimelle (MB)
+
+# more.jsp
+more.title = Lisätoiminnot
+more.random.title = Satunnainen soittolista
+more.random.text = Luo satunnainen soittolista
+more.random.songs = {0} kappaleesta
+more.random.auto = Kun soittolista soitettu, toista lisää satunnaisia kappaleita.
+more.random.ok = OK
+more.random.genre = tyylilajista
+more.random.anygenre = Mikä tahansa
+more.random.year = vuosilta
+more.random.anyyear = Mikä tahansa
+more.random.folder = kansiossa
+more.random.anyfolder = Mikä tahansa
+more.mobile.title = Matkapuhelin
+more.mobile.text = <p>Voit käyttää {0} ohjelmaa millä tahansa WAP-ominaisuuksin varustetulla matkapuhelimella tai kämmentietokoneella.<br> \
+ Käytä seuraavaa WAP-osoitetta: <b>http://yourhostname/wap</b></p> \
+ <p>Tämä edellyttää, että serveriin voidaan muodostaa yhteys internetistä käsin.</p>
+more.podcast.title = Podcasti
+more.podcast.text = <p>Tallennettuja soittolistoja voidaan tilata kuten podcasteja.<br>\
+ Käytä seuraavaa osoitetta Podcast soittimessasi: <b>http://yourhostname/podcast</b>, \
+ tai <b><a href="podcast.view?suffix=.rss">Klikkaa tässä</a>.</b></p>
+more.upload.title = Siirrä tiedosto palvelimeen
+more.upload.source = Valitse tiedosto
+more.upload.target = Siirrä kansioon
+more.upload.browse = Valitse
+more.upload.ok = Siirrä
+more.upload.unzip = Automaattisesti pura zip-tiedostot.
+more.upload.progress = % valmiina. Odota...
+
+# upload.jsp
+upload.title = Tiedoston siirto
+upload.success = Onnistuneesti siirretty <b>{0}</b>
+upload.empty = Tiedostoja ei ole valittu siirrettäväksi palvelimeen.
+upload.failed = Siirto epäonnistui seuraavan virheen vuoksi:<br><b>"{0}"</b>
+upload.unzipped = Purettu {0}
+
+# help.jsp
+help.title = Ohjelma {0}
+help.upgrade = <b>Huomio!</b> Uusi ohjelmaversio on saatavilla. Lataa {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">tästä</a>.
+help.version.title = Versio
+help.builddate.title = Build date
+help.server.title = Serveri
+help.license.title = Lisenssi
+help.license.text = {0} is free software distributed under the <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source license. \
+ {0} uses <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>.
+help.homepage.title = Kotisivu
+help.forum.title = Foorumi
+help.shop.title = Oheistuotteet
+help.contact.title = Yhteystiedot
+help.contact.text = {0} ohjelman on kehittänyt ja ylläpitää Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Jos sinulla on kysyttävää, kommentoitavaa tai parannusehdotuksia ohjelmaan, vieraile \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Foorumissa</a>.
+help.donate = {0} on ilmainen, mutta voit tukea projektia antamalla sille <b><a href="donate.view?">lahjoituksen</a></b>.
+help.log = Loki
+help.logfile = Täydellinen loki on tallennettu {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Ohjelman asetukset
+settingsheader.general = Yleiset
+settingsheader.advanced = Lisäasetukset
+settingsheader.personal = Henkilökohtaiset
+settingsheader.musicFolder = Kansiot
+settingsheader.internetRadio = Internet Tv/Radio
+settingsheader.podcast = Podcastit
+settingsheader.player = Soittimet
+settingsheader.network = Verkko
+settingsheader.transcoding = Muuntaminen
+settingsheader.user = Käyttäjät
+settingsheader.search = Hakutietokanta
+settingsheader.coverArt = Kansikuvitus
+settingsheader.password = Salasana
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Soittolistojen kansio
+generalsettings.musicmask = Musiikin tiedostotyypit
+generalsettings.videomask = Videon tiedostotyypit
+generalsettings.coverartmask = Kuvan tiedostotyypit
+generalsettings.index = Hakemisto
+generalsettings.ignoredarticles = Kielletyt sanat
+generalsettings.shortcuts = Oikopolut
+generalsettings.showgettingstarted = Näytä "Aloitusasetukset" etusivulla
+generalsettings.welcometitle = Tervetuloa otsikko
+generalsettings.welcomesubtitle = Tervetuloa alaotsikko
+generalsettings.welcomemessage = Tervetuloa viesti
+generalsettings.loginmessage = Kirjautumisen viesti
+generalsettings.language = Oletuskieli
+generalsettings.theme = Oletusteema
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Muuta näytetaajuutta
+advancedsettings.coverartlimit = Kansikuvien määrä sivulla<br><div class="detail">(0 = Rajoittamaton)</div>
+advancedsettings.downloadlimit = Latauksen rajoitus (Kbps)<br><div class="detail">(0 = Rajoittamaton)</div>
+advancedsettings.uploadlimit = Tiedoston siirron rajoitus (Kbps)<br><div class="detail">(0 = Rajoittamaton)</div>
+advancedsettings.streamport = Ei SSL salattu portti<br><div class="detail">(0 = Poissa käytöstä)</div>
+advancedsettings.ldapenabled = Ota käyttöön LDAP tunnistautuminen
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP hakusuodatin
+advancedsettings.ldapmanagerdn = LDAP manager DN<br><div class="detail">(Vaihtoehto)</div>
+advancedsettings.ldapmanagerpassword = Salasana
+advancedsettings.ldapautoshadowing = Automaattisesti luo käyttäjätilit {0} ohjelmaan
+
+# personalSettings.jsp
+personalsettings.title = Henkilökohtaiset asetukset profiilille {0}
+personalsettings.language = Kieli
+personalsettings.theme = Teema
+personalsettings.display = Näytä
+personalsettings.browse = Selattaessa
+personalsettings.playlist = Soittolistalla
+personalsettings.tracknumber = Raita #
+personalsettings.artist = Artisti
+personalsettings.album = Albumi
+personalsettings.genre = Tyylilaji
+personalsettings.year = Vuosi
+personalsettings.bitrate = Bittivirta
+personalsettings.duration = Kesto
+personalsettings.format = Formaatti
+personalsettings.filesize = Tiedoston koko
+personalsettings.captioncutoff = Kansikuvan esikatselukuva
+personalsettings.partymode = Party mode toiminto
+personalsettings.shownowplaying = Näytä mitä muut kuuntelevat
+personalsettings.nowplayingallowed = Anna muiden nähdä mitä minä kuuntelen
+personalsettings.showchat = Näytä chat-viestit
+personalsettings.finalversionnotification = Ilmoita uudesta ohjelmaversiosta
+personalsettings.betaversionnotification = Ilmoita uudesta ohjelman beetta versiosta
+personalsettings.lastfmenabled = Rekisteröi mitä kuuntelen <a href="http://last.fm/" target="_blank">Last.fm-palveluun</a>
+personalsettings.lastfmusername = Last.fm käyttäjätunnus
+personalsettings.lastfmpassword = Last.fm salasana
+personalsettings.avatar.title = Henkilökohtainen kuva
+personalsettings.avatar.none = Ei kuvaa
+personalsettings.avatar.custom = Valinnainen kuva
+personalsettings.avatar.changecustom = Valitse kuvaksi
+personalsettings.avatar.upload = Siirrä serverille
+personalsettings.avatar.courtesy = Icons courtesy of <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Vaihda henkilökohtainen kuva
+avataruploadresult.success = Onnistuneesti siirretty palvelimelle henkilökohtainen kuva "{0}".
+avataruploadresult.failure = Kuvan siirto palvelimelle epäonnistui. Katso <a href="help.view?">lokista</a> lisätietoja.
+
+# passwordSettings.jsp
+passwordsettings.title = Vaihda salasana profiilille {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Kansion sijainti
+musicfoldersettings.name = Kansion nimi
+musicfoldersettings.enabled = Käytössä
+musicfoldersettings.add = Lisää musiikkikansio
+musicfoldersettings.nopath = Määritä kansion sijainti.
+
+# networkSettings.jsp
+networksettings.text = Käytä alla olevia asetuksia määrittääksesi pääsyn Subsonic serverille internetistä käsin.
+networksettings.portforwardingenabled = Määritä reititin automaattisesti hyväksymään yhteydet internetistä käsin Subsonic ohjelmaan (UPnP portin uudelleen ohjaus).
+networksettings.portforwardinghelp = Jos reititintä ei voi määrittää automaattisesti, voit tehdä sen manuaalisesti. \
+ Lue ohjeet <a href="http://portforward.com/" target="_blank">portforward.com sivustolta</a>. \
+ Sinun tulee uudelleen ohjata portti {0} sille tietokoneelle, missä Subsonic serveri on.
+networksettings.urlredirectionenabled = Pääsy serverille internetistä käsin käyttämällä helposti muistettavaa verkko-osoitetta.
+networksettings.status = Tilanne:
+networksettings.trialexpired = Koekäyttö on päättynyt {0}. Ole hyvä ja <b><a href="donate.view?">tee lahjoitus</a></b> voidaksesi käyttää tätä ominaisuutta pysyvästi.
+networksettings.trialnotexpired = Tämä ominaisuus on käytettävissä {0}. Sen jälkeen sinun tulee <b><a href="donate.view?">tehdä lahjoitus</a></b> käyttääksesi jatkossa tätä ominaisuutta.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Sääntö
+transcodingsettings.sourceformat = Käännä
+transcodingsettings.targetformat = Lopputulos
+transcodingsettings.step1 = Vaihe 1
+transcodingsettings.step2 = Vaihe 2
+transcodingsettings.step3 = Vaihe 3
+transcodingsettings.defaultactive = Oletus
+transcodingsettings.enabled = Käytössä
+transcodingsettings.add = Lisää uusi sääntö
+transcodingsettings.noname = Anna säännölle nimi.
+transcodingsettings.nosourceformat = Määritä mistä tiedostomuodosta muunnetaan.
+transcodingsettings.notargetformat = Määritä lopputuloksen tiedostomuoto.
+transcodingsettings.nostep1 = Määritä ainakin yksi vaihe muunnokseen.
+transcodingsettings.info = <p class="detail">(%s = Tiedosto, mikä muunnetaan, %b = Maksimi bittivirta soittimessa)</p> \
+ <p>Muuntaminen on prosessi, missä tiedostotyyppi muunnetaan toiseksi tiedostotyypiksi. {1}'ohjelma \
+ antaa striimata myös mediaa, mikä normaalisti ei olisi striimattavissa. Muunnos tehdään lennossa, eikä se \
+ vaadi levytilaa.<p/> \
+ <p>Varsinaisen muunnoksen suorittaa kolmannen osapuolen komentoriviohjelma, mikä tulee asentaa kansioon c:|subsonic|transcode. \
+ Muunnosohjelma Windowsiin \
+ on saatavilla <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>täältä</b></a>. \
+ Voit käyttää myös jotain muuta ohjelmaa jos se täyttää seuraavat ehdot: \
+ <ul> \
+ <li>Siinä pitää olla komentorivikäyttöliittymä.</li> \
+ <li>Sen pitää pystyä lähettämään tiedot ulos (stdout).</li> \
+ <li>Jos ohjelmaa käytetään vaiheessa 2 tai 3, sen pitää pystyä lukemaan tiedot (stdin).</li> \
+ </ul> \
+ </p> \
+ <p> Huomaa, että muunnokset ovat käytettävissä soitinkohtaisesti. Laittamalla täppä kohtaan "Oletus", muunnokset \
+ ovat automaattisesti käytettävissä myös uusille soittimille.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Striimin URL-osoite
+internetradiosettings.homepageurl = Kotisivu
+internetradiosettings.name = Nimi
+internetradiosettings.enabled = Käytössä
+internetradiosettings.add = Lisää Internet Tv/Radio
+internetradiosettings.nourl = Määritä URL osoite.
+internetradiosettings.noname = Määritä nimi.
+
+# podcastSettings.jsp
+podcastsettings.update = Tarkista uudet jaksot
+podcastsettings.keep = Pidä koneella
+podcastsettings.keep.all = Kaikki jaksot
+podcastsettings.keep.one = Viimeisin jakso
+podcastsettings.keep.many = Viimeiset {0} jaksoa
+podcastsettings.download = Kun uudet jaksot ovat saatavilla
+podcastsettings.download.all = Lataa kaikki jaksot
+podcastsettings.download.one = Lataa uusin jakso
+podcastsettings.download.many = Lataa viimeiset {0} jaksoa
+podcastsettings.download.none = Älä tee mitään
+podcastsettings.interval.manually = Manuaalisesti
+podcastsettings.interval.hourly = Joka tunti
+podcastsettings.interval.daily = Joka päivä
+podcastsettings.interval.weekly = Joka viikkko
+podcastsettings.folder = Tallenna Podcastit hakemistoon
+
+# playerSettings.jsp
+playersettings.noplayers = Soitinta ei löytynyt.
+playersettings.type = Tyyppi
+playersettings.lastseen = Viimeksi käytetty
+playersettings.title = Valitse soitin
+
+playersettings.technology.web.title = Soitin selaimessa
+playersettings.technology.external.title = Ulkoinen soitin
+playersettings.technology.external_with_playlist.title = Ulkoinen soitin ja soittolista
+playersettings.technology.jukebox.title = Jukeboksi
+playersettings.technology.web.text = Toistaa musiikkia suoraan internetselaimessa Flash playerin avulla.
+playersettings.technology.external.text = Toistaa musiikin suoraan suosikkiohjelmassasi esim. WinAmp tai Windows Media Player.
+playersettings.technology.external_with_playlist.text = Kuten edelinen, mutta soittolistoja käsitellään suosikkiohjelmassasi, eikä \
+ Subsonic ohjelmassa. Tämä valinta mahdollistaa kappaleiden yli hyppäämisen.
+playersettings.technology.jukebox.text = Soita musiikkia suoraan Subsonic serverillä olevasta audiolähteestä. (Vain ne käyttäjät, joilla siihen on lupa).
+playersettings.name = Soittimen nimi
+playersettings.coverartsize = Kansikuvan koko
+playersettings.maxbitrate = Kappaleen maksimi bittivirta
+playersettings.coverart.off = Ei käytössä
+playersettings.coverart.small = Pieni
+playersettings.coverart.medium = Keskikoko
+playersettings.coverart.large = Suuri
+playersettings.nolame = <em>Huomio:</em> LAME ei ole asennettu.<br>Klikkaa Tuki näppäintä nähdäksesi mistä saat apua.
+playersettings.autocontrol = Kontrolloi ulkoista soitinta automaattisesti
+playersettings.dynamicip = Soittimella on dynaaminen IP osoite
+playersettings.transcodings = Aktiiviset uudelleen muunnokset
+playersettings.ok = Tallenna
+playersettings.forget = Poista soitin
+playersettings.clone = Kloonaa soitin
+
+# userSettings.jsp
+usersettings.title = Valitse käyttäjä
+usersettings.newuser = Uusi käyttäjä
+usersettings.admin = Käyttäjä on pääkäyttäjä
+usersettings.settings = Käyttäjän on lupa vaihtaa asetuksia ja salasana
+usersettings.stream = Käyttäjän on lupa toistaa tiedostoja
+usersettings.jukebox = Käyttäjän on lupa soittaa tiedostoja Jukebox tilassa
+usersettings.download = Käyttäjän on lupa ladata tiedostoja omalle koneelle
+usersettings.upload = Käyttäjän on lupa siirtää tiedostoja palvelimelle
+usersettings.playlist= Käyttäjän on lupa luoda ja poistaa soittolistoja
+usersettings.coverart = Käyttäjän on lupa vaihtaa albumin kansikuva ja muokata avainsanoja
+usersettings.comment= Käyttäjän on lupa kommentoida, muokata kommentteja sekä arvioida albumeita
+usersettings.podcast= Käyttäjän on lupa hallita podcasteja
+usersettings.username = Käyttäjänimi
+usersettings.changepassword = Vaihda salasana
+usersettings.password = Salasana
+usersettings.newpassword = Uusi salasana
+usersettings.confirmpassword = Vahvista salasana
+usersettings.delete = Poista tämä käyttäjä
+usersettings.ldap = LDAP tunnistautuminen
+usersettings.nousername = Puuttuva käyttäjätunnus.
+usersettings.useralreadyexists = Käyttäjätunnus on jo olemassa.
+usersettings.nopassword = Salasana vaaditaan.
+usersettings.wrongpassword = Salasana ei täsmää.
+usersettings.ldapdisabled = LDAP oikeuksien tarkistaminen ei ole käytössä. Katso lisäasetukset.
+usersettings.passwordnotsupportedforldap = Ei voi asettaa tai muuttaa salasanaa LDAP käyttäjälle.
+usersettings.ok = Salasana onnistuneesti vaihdettu käyttäjälle {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Ei koskaan
+musicfoldersettings.interval.one = Joka päivä
+musicfoldersettings.interval.many = Joka {0} päivä
+musicfoldersettings.hour = klo {0}:00
+
+# coverArtSettings.jsp
+coverartsettings.auto = Automaattisesti lataa puuttuvat kansikuvat kun hakutietokanta on päivitetty.
+coverartsettings.manual = Lataa puuttuvat kansikuvat nyt.
+coverartsettings.missing = {0} albumilla {1} albumista ei ole kansikuvaa.
+coverartsettings.running = Lataa kansikuvia. Tämä voi kestää useita minuutteja, riippuen mediakirjaston \
+ koosta.
+coverartsettings.albumList = Listaa kansikuvattomat albumit.
+
+# main.jsp
+main.up = Ylös
+main.playall = Toista kaikki
+main.playrandom = Toista satunnaisesti
+main.addall = Lisää kaikki soittolistaan
+main.tags = Avainsanat
+main.playcount = Toistettu {0} kertaa.
+main.lastplayed = Viimeksi toistettu {0}.
+main.comment = Kommentoi
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Lihavoitu teksti </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Rivinvaihto</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Kursivoitu teksti </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>Uusi kappale</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>Listaus </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Linkki</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Numeroitu listaus</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Nimetty linkki</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Lahjoita</a> ohjelmalle {1}!<br>(ja poista tämä ilmoitus)
+main.nowplaying = Nyt toistetaan:
+main.lyrics = Sanat
+main.minutesago = minuuttia sitten
+main.chat = Chat viestit
+main.message = Kirjoita viesti
+
+# rating.jsp
+rating.rating = Tähtiä
+rating.clearrating = Tyhjennä luokitus
+
+# coverArt.jsp
+coverart.change = Vaihda
+coverart.zoom = Zoomaa
+
+# allmusic.jsp
+allmusic.text = Hakee albumia <em>{0}</em> allmusic.com palvelusta - Odota hetki.
+
+# changeCoverArt.jsp
+changecoverart.title = Vaihda kansikuva
+changecoverart.address = tai anna kuvan osoite
+changecoverart.artist = Artisti
+changecoverart.album = Albumi
+changecoverart.searchdiscogs = Hae Discogs palvelusta
+changecoverart.wait = Odota hetki...
+changecoverart.success = Kuva on onnistuneesti siirretty palvelimelle ja vaihdettu.
+changecoverart.error = Kuvan lataaminen epäonnistui.
+changecoverart.noimagesfound = Kuvaa ei löytynyt.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Kansikuvan vaihtaminen ei onnistunut:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Muokkaa avainsanoja
+edittags.file = Tiedosto
+edittags.track = Raita
+edittags.songtitle = Nimi
+edittags.artist = Artisti
+edittags.album = Albumi
+edittags.year = Vuosi
+edittags.genre = Tyyli
+edittags.status = Tilanne
+edittags.suggest = Ehdotus
+edittags.reset = Resetoi
+edittags.suggest.short = E
+edittags.reset.short = R
+edittags.set = Aseta
+edittags.working = Työskentelee
+edittags.updated = Päivitetty
+edittags.skipped = Ohitettu
+edittags.error = Virhe
+
+# donate.jsp
+donate.title = Lahjoita
+donate.invalidlicense = Väärä lisenssinumero.
+donate.amount = Lahjoita {0}
+donate.textbefore = <p>Thank you for considering a donation to support the {0} project! \
+ As a donor you will receive a license key which disables ads, allows unlimited streaming to Android phones, \
+ and unlocks other premium features to be released later. The license is valid for this \
+ and all future releases of {0}.</p> \
+ <p>The suggested donation amount is <b>&euro;20</b>, but you can give as much or as little as you feel like. \
+ Note that the license key will be sent to the email address you specify, so make sure you provide a \
+ proper address when registering the donation at PayPal.</p>
+donate.textafter = <p>Click one of the buttons to go to PayPal where you can pay by credit card or by using \
+ your PayPal account (if you have one). Once the donation is processed, you will receive the license key by email.</p> \
+ <p>If you have any questions, please send an email to \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = This copy of {2} was licensed to {0} on {1}. Thank you for your support!
+donate.register = After you receive your license key, please register it below.
+donate.register.email = Sähköpostiosoite
+donate.register.license = Lisenssiavain
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcastit
+podcastreceiver.expandall = Näytä jaksot
+podcastreceiver.collapseall = Piilota jaksot
+podcastreceiver.status.new = Uusi
+podcastreceiver.status.downloading = Lataa
+podcastreceiver.status.completed = Valmis
+podcastreceiver.status.error = Virhe
+podcastreceiver.status.deleted = Poistettu
+podcastreceiver.status.skipped = Ohitettu
+podcastreceiver.downloadselected= Lataa valitut
+podcastreceiver.deleteselected= Poista valitut
+podcastreceiver.confirmdelete= Haluatko todella poistaa valitut podcastit?
+podcastreceiver.check = Tarkista uudet jaksot
+podcastreceiver.refresh = Päivitä sivu
+podcastreceiver.settings = Podcastien asetukset
+podcastreceiver.subscribe = Tilaa podcasti
+
+# lyrics.jsp
+lyrics.title = Sanat
+lyrics.artist = Artisti
+lyrics.song = Kappale
+lyrics.search = Etsi
+lyrics.wait = Hakee sanoitusta, odota hetki...
+lyrics.courtesy = (Sanat <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Sanoitusta ei löytynyt.
+
+# helpPopup.jsp
+helppopup.title = {0} Help
+helppopup.cover.title = Kansikuvan koko
+helppopup.cover.text = <p>Antaa sinun määrittää näytettävän kansikuvan koon. Voit myös estää kansikuvan näytön kokonaan.</p>
+helppopup.transcode.title = Kappaleen maksimi bittivirta
+helppopup.transcode.text = <p>Jos sinulla on hidas internetyhteys, voit asettaa ylärajan striimatulle musiikille. \
+ Esimerkiksi jos alkuperäinen mp3 tiedosto on koodattu 256 Kbps niin asettamalla maksimi bittivirraksi \
+ 128 Kbps {0} automaattisesti muuntaa musiikin annettuun uuteen arvoon.</p> \
+ <p>Tämä valinta edellyttää että LAME on asennettu. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ on avoimen lähdekoodin MP3 kooderi. Voit <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">ladata sen täältä</a>. \
+ Varmista että asennat ohjelman kansioon SUBSONIC_HOME/transcode, tai kansioon missä määritellään tietokoneen ympäristön muuttujat.</p>
+helppopup.playlistfolder.title = Soittolistojen kansio
+helppopup.playlistfolder.text = <p>Voit määrittää sen kansion sijainnin, missä soittolistat ovat.</p>
+helppopup.musicmask.title = Musiikin tiedostotyypit
+helppopup.musicmask.text = <p>Tähän määritetään ne tiedostotyypit, mitkä tunnistetaan musiikkitiedostoiksi.</p>
+helppopup.videomask.title = Videon tiedostotyypit
+helppopup.videomask.text = <p>Tähän määritetään ne tiedostotypit, mitkä tunnistetaan videotiedostoiksi.</p>
+helppopup.coverartmask.title = Kuvien tiedostotyypit
+helppopup.coverartmask.text = <p>Tähän määritetään ne tiedostotyypit, mitkä tunnistetaan kuvatiedostoiksi.</p>
+helppopup.downsamplecommand.title = Muuta näytetaajuutta
+helppopup.downsamplecommand.text = <p>Voit määrittää komennon näytetaajuuden muuttamiseksi matalammalle taajuudelle.</p>\
+ <p>(%s = Tiedosto minkä taajuutta muutetaan, %b = Maksimi bittivirta soittimessa)</p>
+helppopup.index.title = Hakemisto
+helppopup.index.text = <p>Tähän määritetään aakkosellisen hakemiston ulkoasu (vasen ylä- ja alareuna). Hakemistosta pääsee helposti käsiksi juurihakemistossa \
+ oleviin kansioihin ja tiedostoihin.</p> \
+ <p>Hakemistossa olevat käskyt erotellaan toisistaan välilyönnein. Normaalisti yksi käsky on yksi kirjain hakemistossa tai \
+ koostuu useista kirjainyhdistelmistä. Esimerkiksi käsky <em>The</em> listaa kaikki ne tiedostot ja kansiot, mitkä \
+ alkavat "The".</p> \
+ <p>Esimerkki 2. Käsky \
+ <em>A-E(ABCDE)</em> näkyy hakemistossa <em>A-E</em> ja linkittää kaikkiin tiedostoihin ja kansioihin, mitkä alkavat joko \
+ A, B, C, D tai E. Tämä voi olla hyödyllinen toiminto esimerkiksi vähemmän käytettyjen kirjainten kohdalla (kuten X, Y tai Z), tai \
+ joukolle aksenttimerkkejä (kuten A, \u00c0 tai \u00c1)</p> \
+ <p>Tiedostot ja kansiot joille ei ole määritelty komentoa hakemistossa löytyvät hakemiston kohdasta "#".</p>
+helppopup.ignoredarticles.title = Kielletyt sanat
+helppopup.ignoredarticles.text = <p>Määritetään ne sanat (esim."The") mitkä ohitetaan hakutietokannan luonnissa/päivityksessä.</p>
+helppopup.shortcuts.title = Oikopolut
+helppopup.shortcuts.text = <p>Välilyönnein eroteltu lista ylätason kansioista, joihin luodaan oikopolku. Käytä lainausmerkkejä sanojen ympärillä jos kansion nimessä on sanoja kaksi tai enemmän. </p> \
+ <p><em>Esimerkki: New Incoming "Sound tracks".</em></p>
+helppopup.language.title = Oletuskieli
+helppopup.language.text = <p>Voit vaihtaa ohjelman oletuskielen.</p>
+helppopup.visibility.title = Näytettävät tiedot
+helppopup.visibility.text = <p>Valitse ne tiedot, mitkä näytetään jokaisen kappaleen kohdalla selattaessa tai soittolistalla. Voit myös määrittää kansikuvan esikatselukuvan koon.</p>
+helppopup.partymode.title = Party mode toiminto
+helppopup.partymode.text = <p>Kun party mode toiminto on päällä, käyttöliittymä on yksinkertaisempi ja helpompi käyttää. \
+ Esimerkiksi soittolistan sekoittumisen voi välttää party-mode tilassa.</p>
+helppopup.theme.title = Oletusteema
+helppopup.theme.text = <p>Voit valita ohjelman ulkoasun.</p>
+helppopup.welcomemessage.title = Tervetuloa viesti
+helppopup.welcomemessage.text = <p>Viesti, mikä näytetään etusivulla.</p>
+helppopup.loginmessage.title = Kirjautumisen viesti
+helppopup.loginmessage.text = <p>Viesti, mikä näytetään sisään kirjautumisen sivulla.</p>
+helppopup.coverartlimit.title = Kansikuvien määrä sivulla
+helppopup.coverartlimit.text = <p>Määritä kuinka monta kansikuvaa näytetään yhdellä sivulla.</p>
+helppopup.downloadlimit.title = Latauksen rajoitus
+helppopup.downloadlimit.text = <p>Määritä paljonko lataus serveriltä saa varata kaistaa internetyhteydeltä.</p>
+helppopup.uploadlimit.title = Tiedoston siirron rajoitus
+helppopup.uploadlimit.text = <p>Määritä paljonko tiedoston siirto serverille saa varata kaistaa internetyhteydeltä.</p>
+helppopup.streamport.title = Ei SSL salattu portti
+helppopup.streamport.text = <p>Tämä vaihtoehto on tarpeellinen silloin jos käytät {0} ohjelmaa serverillä, mikä käyttää salattua liikennettä SSL (HTTPS).</p><p>Jotkin soittimet \
+ (kuten Winamp) eivät tue salattua lähetystä. Määritä portin numero tavalliselle http liikenteelle (tavallisesti 80 \
+ tai 4040) jos et tahdo lähettää striimiä salattuna.</p>
+helppopup.ldap.title = LDAP tunnistautuminen
+helppopup.ldap.text = <p>Käyttäjät voidaan tunnistaa ulkoisella LDAP serverillä (Mukaanlukien Windows Active Directory). \
+ Kun LDAP-toiminto on päällä ja käyttäjä kirjautuu {0} ohjelmaan niin käyttäjätunnus ja salasana tarkistetaan ulkoiselta serveriltä, ei {0} ohjelmasta.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>URL-osite LDAP serverille. Protokollan pitää joko <em>ldap://</em> tai <em>ldaps://</em> \
+ (LDAP yhteys salattuna SSL). Katso <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">täältä</a> \
+ lisätietoja.</p>
+helppopup.ldapsearchfilter.title = LDAP haku suodatin
+helppopup.ldapsearchfilter.text = <p>Tämä on LDAP haku suodatin, mitä käytetään käyttäjän haussa \
+ (kerrottu ohjeessa <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Kaavassa "'{0'}" korvataan käyttäjätunnuksella, esimerkiksi: \
+ <ul>\
+ <li>(uid='{0'}) - tämä etsisi käyttäjätunnusta, mikä vastaa uid määritelmää.</li> \
+ <li>(sAMAccountName='{0'}) - normaalisti käytetty Microsoft Active Directory tunnistautumisessa.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>Jos LDAP serveri ei tue anonyymia yhteyttä, sinun tulee määritellä erityiset käyttäjätunnukset DN \
+ (<em>Distinguished Name</em>) ja salasana LDAP käyttäjille yhteydenottoa varten.</p>
+helppopup.ldapautoshadowing.title = Automaattisesti luo käyttäjätunnukset ohjelmaan {0}
+helppopup.ldapautoshadowing.text = <p>Kun tämä on valittuna, LDAP käyttäjien käyttäjätunnuksia ei tarvitse etukäteen manuaalisesti luoda {0} ohjelmaan ennen sisäänkirjautumista.</p> \
+ <p>HUOMIO! Tämä tarkoittaa sitä, että kuka tahansa jolla on voimassa oleva LDAP käyttäjätunnus ja salasana voi sisäänkirjautua {0} ohjelmaan. \
+ Tätä et ehkä kuitenkaan halua.</p>
+helppopup.playername.title = Soittimen nimi
+helppopup.playername.text = <p>Voit antaa soittimelle helposti muistettavan nimen, kuten "Työ" tai "Olohuone".</p>
+helppopup.autocontrol.title = Kontrolloi ulkoista soitinta automaattisesti
+helppopup.autocontrol.text = <p>Kun tämä valinta on valittu, {0} automaattisesti käynnistää ulkoisen soittimen kun klikkaat "Toista". \
+ Muuten sinun pitää käynnistää soitin itse.</p>
+helppopup.dynamicip.title = Dynaaminen IP osoite
+helppopup.dynamicip.text = <p>Poista tämä valinta jos soitin käyttää staattista IP osoitetta.</p>
+
+# wap/index.jsp
+wap.index.missing = Musiikkia ei löytynyt
+wap.index.playlist = Soittolista
+wap.index.search = Etsi
+wap.index.settings = Asetukset
+
+# wap/browse.jsp
+wap.browse.playone = Toista kappale
+wap.browse.playall = Toista kaikki
+wap.browse.addone = Lisää kappale soittolistaan
+wap.browse.addall = Lisää kaikki soittolistaan
+wap.browse.downloadone = Lataa kappale
+wap.browse.downloadall = Lataa kaikki
+
+# wap/playlist.jsp
+wap.playlist.title = Soittolista
+wap.playlist.noplayer = Soitin ei ole yhteydessä
+wap.playlist.clear = Tyhjennä
+wap.playlist.load = Lataa soittolista
+wap.playlist.random = Sekoita
+wap.playlist.play = Soita puhelimessa
+
+# wap/search.jsp
+wap.search.title = Etsi
+
+# wap/searchResult.jsp
+wap.searchresult.index = Tietokantaa päivitetään parhaillaan. Yritä hetken kuluttua uudelleen.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Valitse soitin
+wap.settings.allplayers = Kaikki
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fr.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fr.properties
new file mode 100644
index 00000000..a7c3330e
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_fr.properties
@@ -0,0 +1,682 @@
+#
+# French localization.
+# Author: John Dillinger (no_mad_soul at hotmail.com) / Raphael Boulcourt (r.boulcourt at free dot fr)
+#
+
+common.home = Accueil
+common.back = Pr\u00e9c\u00e9dent
+common.help = Aide
+common.play = Jouer
+common.add = Ajouter
+common.download = T\u00e9l\u00e9charger
+common.close = Fermer
+common.refresh = Rafraichir
+common.next = Suivant
+common.previous = Pr\u00e9c\u00e9dent
+common.more = Plus
+common.ok = OK
+common.cancel = Annuler
+common.save = Sauvegarder
+common.create = Cr\u00e9er
+common.delete = Effacer
+common.unknown = (Inconnu)
+common.default = (D\u00e9faut)
+
+# login.jsp
+login.username = Utilisateur
+login.password = Mot de passe
+login.login = Entrer
+login.remember = Se souvenir de moi
+login.logout = Vous \u00eates maintenant d\u00e9connect\u00e9.
+login.error = Erreur de nom d'utilisateur ou de mot de passe.
+login.insecure = {0} n''est pas s\u00e9curis\u00e9. Connectez-vous avec "admin" comme nom d''utilisateur<br> et mot de passe, puis modifiez le imm\u00e9diatement.
+
+# accessDenied.jsp
+accessDenied.title = Acc\u00e8s refus\u00e9
+accessDenied.text = D\u00e9sol\u00e9, vous n''avez pas l''autorisation d''effectuer cette op\u00e9ration.
+
+# top.jsp
+top.home = Accueil
+top.now_playing = Joue
+top.settings = Param\u00e8tres
+top.status = Statut
+top.podcast = Podcast
+top.more = Plus
+top.help = Aide
+top.search = Rechercher
+top.upgrade = <b>Note!</b> Une nouvelle version est disponible.<br>T\u00e9l\u00e9charger {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">ici</a>.
+top.missing = Aucun fichier trouv\u00e9. Veuillez modifier vos param\u00e8tres.
+top.logout = Se d\u00e9connecter
+
+# left.jsp
+left.statistics = {0}&nbsp;artistes<br>\
+ {1}&nbsp;albums<br>\
+ {2}&nbsp;chansons<br>\
+ {3} (&#126; {4} heures)
+left.shortcut = Raccourcis
+left.radio = Internet TV/Radio
+left.allfolders = Tous les dossiers
+
+# playlist.jsp
+playlist.stop = Stop
+playlist.start = Ecouter
+playlist.confirmclear = Voulez-vous r\u00e9ellement effacer la liste de lecture ?
+playlist.clear = Effacer
+playlist.shuffle = Al\u00e9atoire
+playlist.repeat_on = R\u00e9p\u00e9ter autoris\u00e9
+playlist.repeat_off = R\u00e9p\u00e9ter non autoris\u00e9
+playlist.undo = Annuler
+playlist.settings = Param\u00e8tres
+playlist.more = Plus d'options...
+playlist.more.playlist = Liste de lecture
+playlist.more.sortbytrack = Trier par pistes
+playlist.more.sortbyartist = Trier par artistes
+playlist.more.sortbyalbum = Trier par albums
+playlist.more.selection = Chansons s\u00e9lectionn\u00e9es
+playlist.more.selectall = Tout s\u00e9lectionner
+playlist.more.selectnone = Ne rien S\u00e9lectionner
+playlist.getflash = Obtenir Flash player
+playlist.load = Charger
+playlist.save = Sauvegarder
+playlist.append = Ajouter \u00e0 une liste de lecture
+playlist.remove = Effacer
+playlist.up = Haut
+playlist.down = Bas
+playlist.empty = Liste de lecture vide
+
+# status.jsp
+status.title = Statut
+status.type = Type
+status.stream = Flux
+status.download = T\u00e9l\u00e9charger
+status.upload = Envoi
+status.player = Lecteur
+status.user = Utilisateur
+status.current = Fichier
+status.transmitted = Transmis
+status.bitrate = Vitesse (Kbps)
+
+# search.jsp
+search.title = Rechercher
+search.query = Artiste, album ou titre
+search.search = Rechercher
+search.index = L'index de recherche va \u00EAtre cr\u00e9\u00e9. Veuillez r\u00e9essayer dans quelques instants.
+search.hits.none = Aucun r\u00e9sultat.
+search.hits.more = Plus
+search.hits.artists = Artistes
+search.hits.albums = Albums
+search.hits.songs = Titres
+
+# gettingStarted.jsp
+gettingStarted.title = D\u00e9marrage rapide
+gettingStarted.text = <p>Bienvenue sur Subsonic ! Suivez les quelques \u00e9tapes ci-dessous pour commencer \u00e0 l''utiliser.<br> \
+ Cliquez le bouton "Accueil" dans la barre du haut pour revenir sur cette page.</p> \
+ <p>Pour plus d''informations, consultez le <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>guide de d\u00e9marrage rapide</b></a>.</p>
+gettingStarted.step1.title = Changer le mot de passe administrateur.
+gettingStarted.step1.text = S\u00e9curisez votre installation en modifiant le mot de passe du compte administrateur. \
+ Vous pouvez aussi cr\u00e9er de nouveaux comptes utilisateurs avec diff\u00e9rents privil\u00e8ges.
+gettingStarted.step2.title = G\u00e9rer les dossiers de musique.
+gettingStarted.step2.text = Subsonic doit savoir dans quel dossier se trouve votre musique.
+gettingStarted.step3.title = Configurer les r\u00e9glages r\u00e9seau.
+gettingStarted.step3.text = Quelques r\u00e9glages pour acc\u00e9der \u00e0 votre musique depuis Internet, \
+ ou la partager avec votre famille ou vos amis. Obtenez votre adresse personnelle <b><em>votre nom</em>.subsonic.org</b>.
+gettingStarted.hide = Ne plus montrer cet \u00e9cran
+gettingStarted.hidealert = Pour afficher cet \u00e9cran \u00e0 nouveau, allez dans Param\u00e8tres > G\u00e9n\u00e9ral.
+
+
+# home.jsp
+home.random.title = Al\u00e9atoire
+home.newest.title = Nouveaux !
+home.highest.title = Mieux not\u00e9s
+home.frequent.title = Plus jou\u00e9s
+home.recent.title = R\u00e9cemment jou\u00e9s
+home.users.title = Utilisateurs
+home.random.text = Albums al\u00e9atoires
+home.newest.text = Albums r\u00e9cemment ajout\u00e9s ou modifi\u00e9s
+home.highest.text = Albums mieux not\u00e9s
+home.frequent.text = Albums fr\u00e9quemment jou\u00e9s
+home.recent.text = Albums r\u00e9cemment jou\u00e9s
+home.users.text = Statistiques utilisateurs
+home.scan = Le dossier musique est actuellement scann\u00e9. Tous les fichiers ne sont pas encore disponibles.
+home.listsize = {0} albums par page
+home.albums = Albums {0} - {1}
+home.playcount = Ecout\u00e9 {0} fois
+home.lastplayed = Ecout\u00e9 le {0}
+home.created = Modifi\u00e9 le {0}
+home.chart.total = Total (MB)
+home.chart.stream = Lus (MB)
+home.chart.download = T\u00e9l\u00e9charg\u00e9s (MB)
+home.chart.upload = Envoy\u00e9s (MB)
+
+# more.jsp
+more.title = Plus
+more.random.title = Liste de lecture al\u00e9atoire
+more.random.text = Cr\u00e9er une liste de lecture al\u00e9atoire avec
+more.random.songs = {0} chansons
+more.random.auto = Jouer plus de chansons al\u00e9atoires lorsque la fin de la liste est atteinte.
+more.random.ok = OK
+more.random.genre = de genre
+more.random.anygenre = Tous les genres
+more.random.year = des ann\u00e9es
+more.random.anyyear = Toutes les ann\u00e9es
+more.random.folder = et du dossier
+more.random.anyfolder = Tous les dossiers
+more.apps.title = Applications
+more.apps.text = Des <a href="http://subsonic.org/pages/apps.jsp" target="_blank">applications Subsonic</a> sont disponibles pour <b>iPhone</b>, \
+ <b>Android</b> et <b>AIR</b>.
+more.mobile.title = T\u00e9l\u00e9phone mobile
+more.mobile.text = <p>Vous pouvez controler {0} \u00e0 partir de votre t\u00e9l\u00e9phone mobile.<br> \
+ Connectez-vous simplement \u00e0 l''adresse suivante \u00e0 partir de votre t\u00e9l\u00e9phone : <b>http://yourhostname/wap</b></p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Pour utiliser une liste de lecture comme un podcast, utilisez simplement l'adresse suivante dans votre logiciel de lecture de Podcasts : <b>http://yourhostname/podcast</b> \
+ ou <b><a href="podcast.view?suffix=.rss">cliquez ici</a>.</b></p>
+more.upload.title = Envoyer un fichier
+more.upload.source = S\u00e9lectionner un fichier
+more.upload.target = Envoyer vers
+more.upload.browse = Parcourir...
+more.upload.ok = Envoyer
+more.upload.unzip = D\u00e9compresser automatiquement les fichiers "zip".
+more.upload.progress = % effectu\u00e9. Veuillez patienter...
+
+# upload.jsp
+upload.title = Fichier envoy\u00e9
+upload.success = Envoy\u00e9 avec succ\u00e8s <b>{0}</b>
+upload.empty = Aucun fichier envoy\u00e9.
+upload.failed = Echec de l''envoi avec l''erreur suivante :<br><b>"{0}"</b>
+upload.unzipped = D\u00e9compress\u00e9 {0}
+
+# help.jsp
+help.title = A propos de {0}
+help.upgrade = <b>Note!</b> Une nouvelle version est disponible. T\u00e9l\u00e9charger {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">ici</a>.
+help.version.title = Version
+help.builddate.title = Date
+help.server.title = Serveur
+help.license.title = Licence
+help.license.text = {0} est un logiciel gratuit distribu\u00e9 sous licence <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>. \
+ {0} utilise <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">une biblioth\u00e8que tiers autoris\u00e9e</a>. Veuillez noter qu''il n''est <em>pas</em> \
+ destin\u00e9 \u00e0 partager ill\u00e9galement des oeuvres non-libres de droit. Renseignez-vous sur les lois en vigueur dans votre pays.
+help.homepage.title = Page d'accueil
+help.forum.title = Forum
+help.shop.title = Marchandises
+help.contact.title = Contact
+help.contact.text = {0} est developp\u00e9 et maintenu par Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Si vous avez des questions, commentaires ou suggestions pour l''am\u00e9liorer, visitez le \
+ <a href="http://forum.subsonic.org" target="_blank">Forum Subsonic</a>.
+help.donate = {0} est gratuit, mais vous pouvez contribuer au projet en faisant un <b><a href="donate.view?">don</a></b>.
+help.log = Log
+help.logfile = L''historique complet est sauvegard\u00e9e dans {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Param\u00e8tres
+settingsheader.general = G\u00e9n\u00e9ral
+settingsheader.advanced = Avanc\u00e9
+settingsheader.personal = Personnel
+settingsheader.musicFolder = Dossier de musique
+settingsheader.internetRadio = TV/Radio Internet
+settingsheader.podcast = Podcast
+settingsheader.player = Lecteurs
+settingsheader.network = R\u00e9seau
+settingsheader.transcoding = Encodage
+settingsheader.user = Utilisateurs
+settingsheader.search = Recherche
+settingsheader.coverArt = Jaquettes
+settingsheader.password = Identification
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Dossier des listes de lecture
+generalsettings.musicmask = Extensions des fichiers audio
+generalsettings.videomask = Extensions des fichiers vid\u00e9o
+generalsettings.coverartmask = Extensions des fichiers des jaquettes
+generalsettings.index = Index
+generalsettings.ignoredarticles = Articles \u00e0 ignorer
+generalsettings.shortcuts = Raccourcis
+generalsettings.showgettingstarted = Montrer la page de "D\u00e9marrage Rapide" \u00e0 l'ouverture
+generalsettings.welcometitle = Titre du message de bienvenue
+generalsettings.welcomesubtitle = Sous-titre
+generalsettings.welcomemessage = Message de bienvenue
+generalsettings.loginmessage = Message de connexion
+generalsettings.language = Langue par d\u00e9faut
+generalsettings.theme = Th\u00e8me par d\u00e9faut
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Commande de r\u00e9\u00e9chantillonnage
+advancedsettings.coverartlimit = Nombre maximum de jaquettes<br><div class="detail">(0 = Illimit\u00e9)</div>
+advancedsettings.downloadlimit = Limite de la vitesse de t\u00e9l\u00e9chargement (Kbps)<br><div class="detail">(0 = Illimit\u00e9e)</div>
+advancedsettings.uploadlimit = Limite de la vitesse d'envoi (Kbps)<br><div class="detail">(0 = Illimit\u00e9e)</div>
+advancedsettings.streamport = Port pour les flux non-SSL<br><div class="detail">(0 = D\u00e9sactiv\u00e9)</div>
+advancedsettings.ldapenabled = Autoriser l'authentification LDAP
+advancedsettings.ldapurl = Url du serveur LDAP
+advancedsettings.ldapsearchfilter = Filtre de recherche LDAP
+advancedsettings.ldapmanagerdn = Gestionnaire LDAP DN<br><div class="detail">(Optionnel)</div>
+advancedsettings.ldapmanagerpassword = Mot de passe
+advancedsettings.ldapautoshadowing = Cr\u00e9er automatiquement les utilisateurs dans {0}
+
+# personalSettings.jsp
+personalsettings.title = Param\u00e8tres personnels pour {0}
+personalsettings.language = Langue
+personalsettings.theme = Th\u00e8me
+personalsettings.display = Afficher
+personalsettings.browse = Parcourir
+personalsettings.playlist = Liste de lecture
+personalsettings.tracknumber = Piste #
+personalsettings.artist = Artiste
+personalsettings.album = Album
+personalsettings.genre = Genre
+personalsettings.year = Ann\u00e9e
+personalsettings.bitrate = Vitesse
+personalsettings.duration = Dur\u00e9e
+personalsettings.format = Format
+personalsettings.filesize = Taille du fichier
+personalsettings.captioncutoff = Cacher la l\u00e9gende
+personalsettings.partymode = Party mode
+personalsettings.shownowplaying = Voir ce que les autres utilisateurs \u00e9coutent
+personalsettings.nowplayingallowed = Montrer aux autres ce que j'\u00e9coute
+personalsettings.showchat = Voir les messages du chat
+personalsettings.finalversionnotification = Me notifier les nouvelles versions
+personalsettings.betaversionnotification = Me notifier les nouvelles versions beta
+personalsettings.lastfmenabled = Enregistrer ce que j'\u00e9coute sur <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Identifiant Last.fm
+personalsettings.lastfmpassword = Mot de passe Last.fm
+personalsettings.avatar.title = Image personnelle
+personalsettings.avatar.none = Aucune image
+personalsettings.avatar.custom = Image personnalis\u00e9e
+personalsettings.avatar.changecustom = Changer d'image personnalis\u00e9e
+personalsettings.avatar.upload = Envoyer
+personalsettings.avatar.courtesy = Images fournies par <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a> et \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Changer d'image personnelle
+avataruploadresult.success = Image personnelle envoy\u00e9e avec succ\u00e8s "{0}".
+avataruploadresult.failure = Echec de l'envoi de l'image personnelle. Voir <a href="help.view?">l'historique</a> pour plus d\u00e9tails.
+
+# passwordSettings.jsp
+passwordsettings.title = Changer le mot de passe de {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Dossier
+musicfoldersettings.name = Nom
+musicfoldersettings.enabled = Utiliser
+musicfoldersettings.add = Ajouter un dossier
+musicfoldersettings.nopath = Veuillez indiquer un dossier.
+
+# networkSettings.jsp
+networksettings.text = Compl\u00e9tez les options ci-dessous afin de d\u00e9finir comment acc\u00e9der \u00e0 votre serveur Subsonic depuis Internet.<br> \
+ Si vous rencontrez des probl\u00e8mes, consultez le guide de <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>D\u00e9marrage rapide</b></a>.
+networksettings.portforwardingenabled = Configurer automatiquement le routeur pour autoriser les connexions entrantes vers Subsonic (transit par port UPnP).
+networksettings.portforwardinghelp = Si votre routeur ne peut pas \u00eatre configur\u00e9 automatiquement, vous pouvez le faire manuellement en suivant \
+ les instructions de <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Vous devrez alors ouvrir le port TCP {0} de l''ordinateur sur lequel est install\u00e9 Subsonic.
+networksettings.urlredirectionenabled = Acc\u00e9dez \u00e0 votre serveur depuis Internet en utilisant une adresse facile \u00e0 retenir.
+networksettings.status = Statut :
+networksettings.trialexpired = La p\u00e9riode d''essai expire le {0}. Veuillez <b><a href="donate.view?">faire un don</a></b> pour activer cette fonction d\u00e9finitivement.
+networksettings.trialnotexpired = Cette fonction est disponible jusque {0}. Apr\u00e8s cette date, vous devrez <b><a href="donate.view?">faire un don</a></b> pour activer cette fonction d\u00e9finitivement.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nom
+transcodingsettings.sourceformat = Source
+transcodingsettings.targetformat = Destination
+transcodingsettings.step1 = Etape 1
+transcodingsettings.step2 = Etape 2
+transcodingsettings.step3 = Etape 3
+transcodingsettings.defaultactive = Par d\u00e9faut
+transcodingsettings.enabled = Autoriser
+transcodingsettings.add = Ajouter un transcodage
+transcodingsettings.recommended = Configuration recommand\u00e9e
+transcodingsettings.noname = Veuillez indiquer un nom.
+transcodingsettings.nosourceformat = Veuillez indiquer le format de la source.
+transcodingsettings.notargetformat = Veuillez indiquer le format de la cible.
+transcodingsettings.nostep1 = Veuillez renseigner au moins une \u00e9tape.
+transcodingsettings.info = <p class="detail">(%s = Le fichier \u00e0 encoder, %b = Bitrate maximum du lecteur)</p> \
+ <p>L''encodage est un processus de conversion d''un format de fichier vers un autre. Le moteur d''encodage de {1} \
+ permet de lire un fichier qui normalement n''aurait pas pu l''\u00eatre. L''encodage est effectu\u00e9 \u00e0 la vol\u00e9e et ne \
+ requiert aucun usage du disque dur.<p/> \
+ <p>L''encodage peut se faire en ligne de commande \u00e0 l''aide d'un programme tiers qui peut \u00eatre install\u00e9 dans Subsonic. \
+ Un pack d''encodage pour Windows \
+ est disponible <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>ici</b></a>. Vous pouvez ajouter votre propre encodeur \
+ s''il remplit les conditions suivantes : \
+ <ul> \
+ <li> avoir une interface en ligne de commande.</li> \
+ <li>pouvoir envoyer output to stdout.</li> \
+ <li>s''il est utilis\u00e9 dans l''\u00e9tape 2 ou 3, il doit pouvoir lire input from stdin.</li> \
+ </ul> \
+ </p> \
+ <p> La page de param\u00e8tres des lecteurs permet d''activer l''encodage par d\u00e9faut, en cochant "D\u00e9faut". Dans ce cas, l''encodage \
+ sera automatiquement activ\u00e9 pour tout nouveau lecteur.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Adresse URL de diffusion
+internetradiosettings.homepageurl = Page d'accueil
+internetradiosettings.name = Nom
+internetradiosettings.enabled = Autoriser
+internetradiosettings.add = Ajouter une TV/Radio Internet
+internetradiosettings.nourl = Veuillez indiquer une URL.
+internetradiosettings.noname = Veuillez indiquer un nom.
+
+# podcastSettings.jsp
+podcastsettings.update = Mettre \u00e0 jour les \u00e9pisodes
+podcastsettings.keep = Conserver
+podcastsettings.keep.all = Tous les \u00e9pisodes
+podcastsettings.keep.one = Les \u00e9pisode les plus r\u00e9cents
+podcastsettings.keep.many = Derniers {0} \u00e9pisodes
+podcastsettings.download = Quand de nouveaux \u00e9pisodes sont disponibles
+podcastsettings.download.all = Tout t\u00e9l\u00e9charger
+podcastsettings.download.one = T\u00e9l\u00e9charger le plus r\u00e9cent
+podcastsettings.download.many = T\u00e9l\u00e9charger les derniers {0} \u00e9pisodes
+podcastsettings.download.none = Ne rien faire
+podcastsettings.interval.manually = Manuellement
+podcastsettings.interval.hourly = Chaque heure
+podcastsettings.interval.daily = Chaque jour
+podcastsettings.interval.weekly = Chaque semaine
+podcastsettings.folder = Enregistrer les fichiers dans le dossier
+
+# playerSettings.jsp
+playersettings.noplayers = Aucun lecteur trouv\u00e9.
+playersettings.type = Type
+playersettings.lastseen = Utilis\u00e9 pour la derni\u00E8re fois
+playersettings.title = S\u00e9lectionner un lecteur
+playersettings.technology.web.title = Lecteur Web
+playersettings.technology.external.title = Lecteur externe
+playersettings.technology.external_with_playlist.title = Lecteur externe avec playlist
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Joue la musique directement dans le navigateur avec le lecteur flash int\u00e9gr\u00e9.
+playersettings.technology.external.text = Joue la musique dans votre lecteur favori (Winamp, Windows Media Player ou autre).
+playersettings.technology.external_with_playlist.text = Comme ci-dessus, la playlist est g\u00e9r\u00e9e par le lecteur et non par Subsonic. \
+Avec ce mode, sauter une chanson est possible pendant l'\u00e9coute.
+playersettings.technology.jukebox.text = Joue la musique directement sur le lecteur du serveur Subsonic. (Utilisateurs autoris\u00e9s uniquement).
+playersettings.name = Nom du lecteur
+playersettings.coverartsize = Taille des jaquettes
+playersettings.maxbitrate = Vitesse maximum
+playersettings.coverart.off = D\u00e9sactiv\u00e9
+playersettings.coverart.small = Petites
+playersettings.coverart.medium = Moyennes
+playersettings.coverart.large = Grandes
+playersettings.nolame = <em>Attention:</em> Il semble que LAME ne soit pas install\u00e9.<br>Cliquez sur le bouton d'aide pour en savoir plus.
+playersettings.autocontrol = Lancer le lecteur automatiquement
+playersettings.dynamicip = Le lecteur a une adresse IP dynamique
+playersettings.transcodings = Activer l'encodage
+playersettings.ok = Sauvegarder
+playersettings.forget = Effacer le lecteur
+playersettings.clone = Cloner le lecteur
+
+# userSettings.jsp
+usersettings.title = S\u00e9lectionner un utilisateur
+usersettings.newuser = Nouvel utilisateur
+usersettings.admin = Nouvel administrateur
+usersettings.settings = Permettre \u00e0 l'utilisateur de changer ses options et son mot de passe
+usersettings.stream = Permettre \u00e0 l'utilisateur de jouer des fichiers
+usersettings.jukebox = Permettre \u00e0 l'utilisateur de jouer les fichiers en mode jukebox
+usersettings.download = Permettre \u00e0 l'utilisateur de t\u00e9l\u00e9charger des fichiers
+usersettings.upload = Permettre \u00e0 l'utilisateur d'envoyer des fichiers
+usersettings.playlist= Permettre \u00e0 l'utilisateur de cr\u00e9er et supprimer des listes de lecture
+usersettings.coverart = Permettre \u00e0 l'utilisateur de modifier les jaquettes et \u00e9tiquettes
+usersettings.comment= Permettre \u00e0 l'utilisateur de cr\u00e9er et \u00e9diter des commentaires et estimations
+usersettings.podcast= Permettre \u00e0 l'utilisateur de g\u00e9rer les Podcasts
+usersettings.username = Identifiant
+usersettings.changepassword = Changer le mot de passe
+usersettings.password = Mot de passe
+usersettings.newpassword = Nouveau mot de passe
+usersettings.confirmpassword = Confirmer le mot de passe
+usersettings.delete = Supprimer cet utilisateur
+usersettings.ldap = Authentifier l'utilisateur avec LDAP
+usersettings.nousername = Utilisateur manquant.
+usersettings.useralreadyexists = Utilisateur existant.
+usersettings.nopassword = Mot de passe requis.
+usersettings.wrongpassword = Veuillez confirmer votre mot de passe.
+usersettings.ldapdisabled = Authentification par LDAP non s\u00e9lectionn\u00e9e. Regardez vos param\u00e8tres avanc\u00e9s.
+usersettings.passwordnotsupportedforldap = Impossible de changer le mot de passe pour les utilisateurs authentifi\u00e9s avec LDAP.
+usersettings.ok = Mot de passe modifi\u00e9 avec succ\u00e8s {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Jamais
+musicfoldersettings.interval.one = Chaque jour
+musicfoldersettings.interval.many = Tous les {0} jours
+musicfoldersettings.hour = \u00e0 {0}:00
+
+# main.jsp
+main.up = Haut
+main.playall = Tout jouer
+main.playrandom = Jouer au hasard
+main.addall = Tout ajouter
+main.tags = Editer les tags
+main.playcount = Ecout\u00e9 {0} fois
+main.lastplayed = et pour la derni\u00e8re fois le {0}.
+main.comment = Commentaire
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Gras </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Retour \u00e0 la ligne</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italique </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>Nouveau paragraphe</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>Liste </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Lien</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Liste \u00e9num\u00e9r\u00e9e</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Lien textuel</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Faire un don</a> pour {1} !<br>(et effacer cet ajout)
+main.nowplaying = Ecout\u00e9 en ce moment :
+main.lyrics = Paroles
+main.minutesago = minutes
+main.chat = Chat
+main.message = Ecrire un message
+main.clearchat = Effacer les messages
+
+# rating.jsp
+rating.rating = Evaluer
+rating.clearrating = Remettre \u00e0 z\u00e9ro
+
+# coverArt.jsp
+coverart.change = Changer
+coverart.zoom = Zoomer
+
+# allmusic.jsp
+allmusic.text = Recherche de l''album sur allmusic.com - Veuillez patienter.
+
+# changeCoverArt.jsp
+changecoverart.title = Changer la jaquette
+changecoverart.address = Ou entrez l'adresse d'une image
+changecoverart.artist = Artiste
+changecoverart.album = Album
+changecoverart.searchdiscogs = Chercher sur Discogs
+changecoverart.wait = Veuillez patienter...
+changecoverart.success = Image t\u00e9l\u00e9charg\u00e9e avec succ\u00e8s.
+changecoverart.error = Erreur de t\u00e9l\u00e9chargement de l'image.
+changecoverart.noimagesfound = Aucune image trouv\u00e9e.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Erreur de changement de jaquette :<br><b>"{0}"</b>
+
+# coverArtSettings.jsp
+coverartsettings.auto = T\u00e9l\u00e9charger automatiquement les jaquettes manquantes lorsque l'index de recherche est mis \u00e0 jour.
+coverartsettings.manual = T\u00e9l\u00e9charger les jaquettes manquantes maintenant.
+coverartsettings.missing = {0} de {1} albums sans jaquette.
+coverartsettings.running = T\u00e9l\u00e9chargement des jaquettes. Ceci peut durer plusieurs minutes, selon la taille de \
+ votre librairie.
+coverartsettings.albumList = Lister les albums dont la jaquette est manquante.
+
+# editTags.jsp
+edittags.title = Editer les tags
+edittags.file = Fichier
+edittags.track = Piste
+edittags.songtitle = Titre
+edittags.artist = Artiste
+edittags.album = Album
+edittags.year = Ann\u00e9e
+edittags.genre = Genre
+edittags.status = Statut
+edittags.suggest = Suggestion
+edittags.reset = Remise \u00e0 z\u00e9ro
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = R\u00e9gler
+edittags.working = En cours
+edittags.updated = A jour
+edittags.skipped = Pass\u00e9
+edittags.error = Erreur
+
+# donate.jsp
+donate.title = Contribuer
+donate.invalidlicense = Cl\u00e9 de licence invalide.
+donate.amount = Montant {0}
+donate.textbefore = <p>Merci de songer \u00e0 contribuer au projet {0} ! \
+ Les donateurs peuvent acc\u00e9der \u00e0 des fonctions suppl\u00e9mentaires, comme :</p> \
+ <ul> \
+ <li>L''utilisation illimit\u00e9 des <a href="http://subsonic.org/pages/apps.jsp" target="blank">applications Subsonic</a> pour iPhone, Android et AIR.</li> \
+ <li>Votre adresse web personnelle : <em>votrenom</em>.subsonic.org (voir <a href="networkSettings.view">Param\u00e8tres &gt; R\u00e9seau</a>).</li> \
+ <li>Aucune publicit\u00e9.</li> \
+ <li>D''autres avantages \u00e0 venir.</li> \
+ </ul> \
+ <p> \
+ En tant que donateur, vous recevrez une cl\u00e9 de licence valide pour toutes les versions de Subsonic \u00e0 venir.</p> \
+ <p>Nous sugg\u00e9rons un don de <b>20&euro;</b>, mais vous \u00eates libres de choisir le montant de votre choix :</p>
+donate.textafter = <p>Cliquez sur l'un des boutons pour vous rendre sur PayPal o\u00f9 vous pourrez effectuer votre don par carte bancaire ou en utilisant \
+ votre compte. Vous recevrez une cl\u00e9 de licence par email dans quelques minutes.</p> \
+ <p>Si vous avez des questions, n'h\u00e9sitez pas \u00e0 envoyer un email \u00e0 \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Cette copie de {2} a \u00e9t\u00e9 enregistr\u00e9e \u00e0 {0} on {1}. Merci pour votre contribution et votre soutien !
+donate.register = Apr\u00e8s avoir re\u00e7u votre cl\u00e9 de licence, veuillez l'enregistrer ci-dessous.
+donate.register.email = Email
+donate.register.license = Licence
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast
+podcastreceiver.expandall = Montrer toutes les \u00e9missions
+podcastreceiver.collapseall = Cacher toutes les \u00e9missions
+podcastreceiver.status.new = Nouveau
+podcastreceiver.status.downloading = En cours de t\u00e9l\u00e9chargement...
+podcastreceiver.status.completed = Complet
+podcastreceiver.status.error = Erreur
+podcastreceiver.status.deleted = Effacer
+podcastreceiver.status.skipped = Passer
+podcastreceiver.downloadselected= Mettre \u00e0 jour les podcasts selectionn\u00e9s
+podcastreceiver.deleteselected= Effacer les \u00e9missions selectionn\u00e9es
+podcastreceiver.confirmdelete= Voulez-vous r\u00e9ellement effacer les \u00e9missions s\u00e9lectionn\u00e9es ?
+podcastreceiver.check = Mettre \u00e0 jour tous les Podcasts
+podcastreceiver.refresh = Rafraichir la page
+podcastreceiver.settings = Param\u00e8tres
+podcastreceiver.subscribe = S'abonner \u00e0 un Podcast
+
+# lyrics.jsp
+lyrics.title = Paroles
+lyrics.artist = Artiste
+lyrics.song = Titre
+lyrics.search = Chercher
+lyrics.wait = Recherche des paroles, veuillez patienter...
+lyrics.courtesy = (Paroles de <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Aucune parole trouv\u00e9e.
+
+# helpPopup.jsp
+helppopup.title = Aide {0}
+helppopup.cover.title = Taille des jaquettes
+helppopup.cover.text = <p>Permet de sp\u00e9cifier la taille des jaquettes \u00e0 afficher, ou de ne rien afficher du tout.</p>
+helppopup.transcode.title = Vitesse maximum
+helppopup.transcode.text = <p>Si vous \u00eates limit\u00e9 par la bande passante, vous pouvez fixer une limite inf\u00e9rieure au d\u00e9bit de lecture de la musique. \
+ Par exemple, si vos fichiers mp3 sont encod\u00e9s en 256 Kbps (kilobits par seconde), et que vous r\u00e9glez le param\u00e8tre de d\u00e9bit maximum \
+ \u00e0 128 Kbps, {0} r\u00e9\u00e9chantillonnera automatiquement la musique de 256 \u00e0 128 Kbps.</p> \
+ <p>Cette option requiert que LAME soit install\u00e9. <a target="_blank" href="http://lame.sourceforge.net/">LAME</a> \
+ est un encodeur de mp3 open source. Vous pouvez <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">le t\u00e9l\u00e9charger ici</a>. \
+ Dans ce cas, assurez-vous de l''installer dans le dossier SUBSONIC_HOME/transcode.</p>
+helppopup.playlistfolder.title = Dossier des listes de lecture
+helppopup.playlistfolder.text = <p>Permet d''indiquer l''emplacement de vos listes de lecture.</p>
+helppopup.musicmask.title = Extensions des fichiers musicaux
+helppopup.musicmask.text = <p>Permet d''indiquer les types de fichier reconnus quand vous naviguerez dans vos dossiers audio.</p>
+helppopup.coverartmask.title = Extensions des jaquettes
+helppopup.coverartmask.text = <p>Permet d''indiquer les types de fichier utilis\u00e9s pour les jaquettes des albums.</p>
+helppopup.downsamplecommand.title = Commande de r\u00e9\u00e9chantillonnage
+helppopup.downsamplecommand.text = <p>Permet d''indiquer la commande \u00e0 employer pour r\u00e9\u00e9chantillonner les d\u00e9bits .</p>\
+ <p>(%s = Le fichier \u00e0 r\u00e9\u00e9chantillonner, %b = Le d\u00e9bit maximum du lecteur)</p>
+helppopup.index.title = Index
+helppopup.index.text = <p>Permet de r\u00e9gler l''affichage de l''index (situ\u00e9 \u00e0 gauche de l''\u00e9cran). Les fichiers et dossiers \
+ situ\u00e9s directement \u00e0 la racine du dossier de musique seront facilement accessibles en utilisant cet index.</p> \
+<p>Les \u00e9l\u00e9ments de la liste des entr\u00e9es de l''index doivent \u00EAtre s\u00e9par\u00e9s par un espace. Normalement, chaque entr\u00e9e est un caract\u00e8re simple, \
+ mais vous pouvez choisir d''en afficher plusieurs. Par exemple, l'entr\u00e9e <em>"Les"</em> rassemblera les dossiers et \
+ fichiers dont les noms commencent par <em>"Les"</em>.</p> \
+ <p>Vous pouvez \u00e9galement cr\u00e9er une entr\u00e9e entre paranth\u00e8ses qui regroupera plusieurs caract\u00e8res. Par exemple, l'entr\u00e9e \
+ <em>"A-E(ABCDE)"</em> affichera <em>A-E</em> et regroupera tous les fichiers et dossiers dont les noms commencent par \
+ A, B, C, D ou E. Cette option peut \u00EAtre utile pour regrouper les caract\u00e8res les moins utilis\u00e9s (comme les lettres X, Y et Z), ou \
+ les caract\u00e8res accentu\u00e9s (comme A, \u00c0 et \u00c1).</p> \
+ <p>Les fichiers et dossiers non index\u00e9s seront regroup\u00e9s \u00e0 l''entr\u00e9e "#".</p>
+helppopup.ignoredarticles.title = Articles \u00e0 ignorer
+helppopup.ignoredarticles.text = <p>Permet de lister les articles \u00e0 ignorer lors de la cr\u00e9ation de l''index (par exemple, "le", "The"...).</p>
+helppopup.shortcuts.title = Raccourcis
+helppopup.shortcuts.text = <p>Permet d'afficher une liste de raccourcis au sommet de la liste des dossiers, par exemple:</p> \
+ <p><em>Nouveau Podcast</em></p>
+helppopup.language.title = Langage
+helppopup.language.text = <p>Permet de choisir la langue utilis\u00e9 par d\u00e9faut.</p>
+helppopup.visibility.title = Afficher
+helppopup.visibility.text = <p>Choisissez les \u00e9l\u00e9ments \u00e0 afficher dans les menus et dans la liste de lecture.</p>
+helppopup.partymode.title = Party mode
+helppopup.partymode.text = <p>Activer ce mode simplifie l''interface d''utilisation et \u00e9vite les modifications accidentelles, par exemple pour les utilisateurs inexp\u00e9riment\u00e9s.</p>
+helppopup.theme.title = Th\u00e8me
+helppopup.theme.text = <p>Permet de choisir le th\u00e8me utilis\u00e9 (couleurs, apparence, polices, images...) par d\u00e9faut.</p>
+helppopup.welcomemessage.title = Titre du message de bienvenue
+helppopup.welcomemessage.text = <p>Titre du message affich\u00e9 sur la page d''accueil.</p>
+helppopup.loginmessage.title = Message de connexion
+helppopup.loginmessage.text = <p>Le message affich\u00e9 sur la page de connexion.</p>
+helppopup.coverartlimit.title = Nombre de jaquettes \u00e0 afficher
+helppopup.coverartlimit.text = <p>Permet de d\u00e9finir le nombre maximum de jaquettes \u00e0 afficher sur une page.</p>
+helppopup.downloadlimit.title = Limite de t\u00e9l\u00e9chargement
+helppopup.downloadlimit.text = <p>Permet de limiter l''utilisation de la bande passante pour les t\u00e9l\u00e9chargements de fichiers.</p>
+helppopup.uploadlimit.title = Limite d''envoi
+helppopup.uploadlimit.text = <p>Permet de limiter l''utilisation de la bande passante pour les envois de fichiers.</p>
+helppopup.streamport.title = Port pour les flux non-SSL
+helppopup.streamport.text = <p>Cette option n''est utile que si vous utilisez Subsonic sur un serveur utilisant la technologie SSL (HTTPS).</p><p>Certain lecteurs \
+ (comme Winamp) ne supportent pas le streaming par SSL. Si vous ne voulez pas que le flux soit transmis par SSL, indiquez un num\u00e9ro de port pour une utilisation http (g\u00e9n\u00e9ralement 80 ou 4040). Notez que ce flux ne sera pas crypt\u00e9.</p>
+helppopup.ldap.title = Authentification LDAP
+helppopup.ldap.text = <p>Les utilisateurs peuvent \u00eatre authentifi\u00e9s par un serveur LDAP externe (dont Windows Active Directory). \
+ Quand ces utilisateurs se connectent sur Subsonic, leur nom d''utilisateur et mot de passe sont v\u00e9rifi\u00e9s par le serveur externe et non par Subsonic.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>Permet d''indiquer l''URL du serveur LDAP. Le protocole sera <em>ldap ://</em> ou <em>ldaps://</em> \
+ (pour LDAP par SSL). Voir <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">ici</a> \
+ pour une description d\u00e9taill\u00e9e.</p>
+helppopup.ldapsearchfilter.title = Filtre de recherche LDAP
+helppopup.ldapsearchfilter.text = <p>Le filtre de recherche utilis\u00e9 dans la recherche utilisateur. Celui-ci est un filtre de recherche LDAP (comme d\u00e9fini <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">ici</a>).</p> \
+ <p>Le mod\u00e8le "{0}" est remplac\u00e9 par le nom d'utilisateur, par exemple :<br /> \
+ - (uid={0}) - cherchera un nom d'utilisateur correspondant \u00e0 l''uid.<br /> \
+ - (sAMAccountName={0}) - habituellement utilis\u00e9 par Microsoft Active Directory pour l''authentification.</p>
+helppopup.ldapmanagerdn.title = Gestionnaire LDAP DN
+helppopup.ldapmanagerdn.text = <p>Si le server LDAP ne g\u00e8re pas les connexions anonymes, vous devez indiquer le DN \
+ (<em>Distinguished Name</em>) et le mot de passe de l''utilisateur LDAP qui l''utilise lorsqu''il se connecte.</p>
+helppopup.ldapautoshadowing.title = Cr\u00e9er automatiquement les utilisateurs LDAP dans {0}
+helppopup.ldapautoshadowing.text = <p>En activant cette option, les utilisateurs LDAP ne devront pas \u00EAtre cr\u00e9\u00e9s manuellement dans {0} avant de pouvoir s''identifier.</p> \
+ <p>ATTENTION ! N''importe quel utilisateur avec un nom identifiant LDAP et un mot de passe pourra donc se connecter \u00e0 {0}, \
+ ce n''est peut-\u00EAtre pas ce que vous voulez.</p>
+helppopup.playername.title = Nom du lecteur
+helppopup.playername.text = <p>Permet de d\u00e9finir un nom de lecteur facile \u00e0 retenir, comme "Travail" ou "Maison".</p>
+helppopup.autocontrol.title = Lancer le lecteur automatiquement
+helppopup.autocontrol.text = <p>Si cette option est activ\u00e9e, {0} lancera le lecteur automatiquement quand vous cliquerez sur "Lecture" \
+ dans une liste de lecture. Autrement, vous devrez connecter et lancer le lecteur vous m\u00eame.</p>
+helppopup.dynamicip.title = Adresse IP dynamique
+helppopup.dynamicip.text = <p>D\u00e9sactivez cette option si votre lecteur utilise une adresse IP statique.</p>
+
+# wap/index.jsp
+wap.index.missing = Aucun fichier trouv\u00e9
+wap.index.playlist = Liste de lecture
+wap.index.search = Recherche
+wap.index.settings = R\u00e9glages
+
+# wap/browse.jsp
+wap.browse.playone = Jouer le fichier
+wap.browse.playall = Tout jouer
+wap.browse.addone = Ajouter le fichier
+wap.browse.addall = Tout ajouter
+wap.browse.downloadone = T\u00e9l\u00e9charger le fichier
+wap.browse.downloadall = Tout t\u00e9l\u00e9charger
+
+# wap/playlist.jsp
+wap.playlist.title = Liste de lecture
+wap.playlist.noplayer = Aucun lecteur connect\u00e9
+wap.playlist.clear = Effacer
+wap.playlist.load = Charger
+wap.playlist.random = Au hasard
+wap.playlist.play = Jouer sur le t\u00e9l\u00e9phone
+
+# wap/search.jsp
+wap.search.title = Rechercher
+
+# wap/searchResult.jsp
+wap.searchresult.index = L'index de recherche est actuellement en cours de cr\u00e9ation. Merci de r\u00e9essayer dans quelques instants.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Selectionnez un lecteur
+wap.settings.allplayers = Tous
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_is.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_is.properties
new file mode 100644
index 00000000..4062e006
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_is.properties
@@ -0,0 +1,635 @@
+#
+# Icelandic localization.
+# Author: DJ Danni
+#
+
+common.home = Heim
+common.back = Til Baka
+common.help = Hjálp
+common.play = Spila
+common.add = Bæta Við
+common.download = Niðurhal
+common.close = Loka
+common.refresh = Endurhlaða
+common.next = Næst
+common.previous = Til Baka
+common.more = Meira
+common.ok = OK
+common.cancel = Hætta Við
+common.save = Vista
+common.create = Búa Til
+common.delete = Eyða
+common.unknown = (Óþekt)
+common.default = (Venjuleg)
+
+# login.jsp
+login.username = Notandanafn
+login.password = Lykilorð
+login.login = Innskrá
+login.remember = Muna Mig
+login.logout = Þú ert nú Útskráður.
+login.error = Rangt Notandanafn/Lykilorð.
+login.insecure = {0} er ekki Örugt. Vinsamlegast Innskráður þig með Notandanafn og<br>Lykilorð "admin", Eða smelltu <a href="login.view?user=admin&amp;password=admin">Hér</a>. Til að breyta Lykilorðinu Strax.
+
+# accessDenied.jsp
+accessDenied.title = Aðgangi Hafnað
+accessDenied.text = Því miður ert þú ekki heimild til að framkvæma umbeðna aðgerð.
+
+# top.jsp
+top.home = Heim
+top.now_playing = Í Spilun
+top.settings = Stillingar
+top.status = Staða
+top.podcast = Podcast
+top.more = Meira
+top.help = Hjálp
+top.search = Leita
+top.upgrade = <b>A.T.H!</b> Nýrri Útgáfa er tilbúin.<br>Sækja {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">Hér</a>.
+top.missing = Engar Tónlistarskrár Fundust. Vinsamlegast Breyttu Stillingunum.
+top.logout = Útskrá {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artists<br>\
+ {1}&nbsp;albums<br>\
+ {2}&nbsp;songs<br>\
+ {3} (&#126; {4} hours)
+left.shortcut = Flýtileiðir
+left.radio = Internet TV/Útvarp
+left.allfolders = Allar Möppur
+
+# playlist.jsp
+playlist.stop = Stoppa
+playlist.start = Spila
+playlist.confirmclear = Villtu Hreinsa Lagalistan?
+playlist.clear = Hreinsa
+playlist.shuffle = Shuffle
+playlist.repeat_on = Endurtekning er á
+playlist.repeat_off = Endurtekning er Af
+playlist.undo = Hætta
+playlist.settings = Stillingar
+playlist.more = Fleiri Aðgerðir...
+playlist.more.playlist = Lagalisti
+playlist.more.sortbytrack = Raða eftir Lögum
+playlist.more.sortbyartist = Raða eftir Höfundum
+playlist.more.sortbyalbum = Sort by album
+playlist.more.selection = Völd Lög
+playlist.more.selectall = Velja Allt
+playlist.more.selectnone = Velja ekkert
+playlist.getflash = Sækja Flash Spilara
+playlist.load = Hlaða
+playlist.save = Vista
+playlist.append = Bæta við á Lagalistann
+playlist.remove = Eyða
+playlist.up = Upp
+playlist.down = Niður
+playlist.empty = Lagalistinn er Tómur
+
+# status.jsp
+status.title = Staða
+status.type = Gerð
+status.stream = Leki
+status.download = Niðurhal
+status.upload = Hlaða Upp
+status.player = Spilari
+status.user = Notendur
+status.current = Núverandi Skrá
+status.transmitted = Sendandi
+status.bitrate = Gæði (Kbps)
+
+# search.jsp
+search.title = Leita
+search.search = Leita
+search.index = Leitin vísitölu er verið að skapa. Vinsamlegast reyndu aftur síðar.
+search.hits.none = Engin Niðurstaða Fannst.
+
+# home.jsp
+home.random.title = Mismunandi
+home.newest.title = Nýrra
+home.highest.title = Há Einkun
+home.frequent.title = Algengustu
+home.recent.title = Nýjustu
+home.users.title = Notendur
+home.random.text = Mismunandi Plötur
+home.newest.text = Flestir Nýlega bætt við eða breytt albúm
+home.highest.text = Mist Einkanir Plötur
+home.frequent.text = Oftast Spilaðar Plötur
+home.recent.text = Nýjustu Plötur
+home.users.text = Notanda Staða
+home.scan = Tónlistin mappa er nú verið skönnuð. Allar aðgerðir eru ekki enn í boði.
+home.listsize = {0} Plötur á hverri síðu
+home.albums = Plötur {0} - {1}
+home.playcount = Spilað {0} songs
+home.lastplayed = Spilað {0}
+home.created = Breytt {0}
+home.chart.total = Heild (MB)
+home.chart.stream = Sennt (MB)
+home.chart.download = Sótt (MB)
+home.chart.upload = Sótt (MB)
+
+# more.jsp
+more.title = Meira
+more.random.title = Mismunandi Lagalisti
+more.random.text = Búa til Mismunandi Lagalista sem
+more.random.songs = {0} Lög
+more.random.auto = Spila fleiri Mismunandi lög við lok lagalista er náð.
+more.random.ok = OK
+more.random.genre = Frá Tegund
+more.random.anygenre = Hvað sem er
+more.random.year = og ár
+more.random.anyyear = Hvað sem er
+more.random.folder = Í Möppu
+more.random.anyfolder = Hvað sem er
+more.mobile.title = Farsími
+more.mobile.text = <p>Þú getur Stjórnað {0} Frá Wapp í Farsímanum þínum eða með PDA.<br> \
+ Einfaldlega skoðið Eftirfarandi Slóð í Símanum þínum: <b>http://Þitthostnafn/wap</b></p> \
+ <p>Þetta krefst þess að framreiðslumaður geta verið náð á Netinu.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Vista Lagalista er virkt sem Podcasts.<br>\
+ Notið eftirfarandi Slóð í Podcast Mótakara: <b>http://þitthostnafn/podcast</b>, \
+ eða <b><a href="podcast.view?suffix=.rss">Smella Hér</a>.</b></p>
+more.upload.title = Hlaða upp Skrá
+more.upload.source = Veldu Skrá
+more.upload.target = Hlaða upp í
+more.upload.browse = Velja
+more.upload.ok = Hlaða Upp
+more.upload.unzip = Sjálfvirk Afþjöppun zip-Skráar.
+more.upload.progress = % Lokið. Vinsamlegast Bíðið...
+
+# upload.jsp
+upload.title = Hleð Upp Skrá
+upload.success = Upphlöðun Lokið <b>{0}</b>
+upload.empty = Engin Skrá til að hlaða Upp.
+upload.failed = Hlaða Upp mistókst með eftirfarandi villum:<br><b>"{0}"</b>
+upload.unzipped = Afþjappað {0}
+
+# help.jsp
+help.title = Um {0}
+help.upgrade = <b>A.T.H!</b> Nýrri Útgáfa er tilbúin.<br>Sækja {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">Hér</a>.
+help.version.title = Útgáfa
+help.builddate.title = Smíði Dags
+help.server.title = Þjónn
+help.license.title = Leifi
+help.license.text = {0} er ókeypis hugbúnaði er dreift samkvæmt <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source license. \
+ {0} Notaðu <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>.
+help.homepage.title = Heimasíða
+help.forum.title = Spjallborð
+help.shop.title = Vörur
+help.contact.title = Hafa Samband
+help.contact.text = {0} er þróað og viðhaldið af Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Ef þú hefur einhverjar spurningar, athugasemdir eða tillögur til úrbóta, skaltu fara á \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Spjallborð</a>.
+help.donate = {0} er frjáls, en þú getur lagt sitt af mörkum til verkefnisins með því að gefa <b><a href="donate.view?">framlag</a></b>.
+help.log = Yfirlit
+help.logfile = Tilbúið Yfirlit er Vistað í {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Stillingar
+settingsheader.general = Hefðbundnar
+settingsheader.advanced = Frekari
+settingsheader.personal = Einka
+settingsheader.musicFolder = Tónlistar Mappa
+settingsheader.internetRadio = Internet TV/Útvarp
+settingsheader.podcast = Podcast
+settingsheader.player = Spilarar
+settingsheader.transcoding = Transcoding
+settingsheader.user = Notendur
+settingsheader.search = Leita
+settingsheader.password = Lykilorð
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Mappa á Lagalista
+generalsettings.musicmask = Tónlistar Gríma
+generalsettings.coverartmask = Fela Höfundar Grímu
+generalsettings.index = Forsíða
+generalsettings.ignoredarticles = Greinar til að Hunsa
+generalsettings.shortcuts = Flýtileiðir
+generalsettings.welcometitle = Velkomin Titill
+generalsettings.welcomesubtitle = Velkomin Aukatitill
+generalsettings.welcomemessage = Velkomin Skilaboð
+generalsettings.loginmessage = Innskráningar Skilaboð
+generalsettings.language = Venjulegt Tungumál
+generalsettings.theme = Venjulegt Útlit
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Niðursýning Skipun
+advancedsettings.coverartlimit = Fela Höfundar Takmörk<br><div class="detail">(0 = Ótakmarkað)</div>
+advancedsettings.downloadlimit = Niðurhal Takmörkun (Kbps)<br><div class="detail">(0 = Ótakmarkað)</div>
+advancedsettings.uploadlimit = Hlaða Upp Takmörkun (Kbps)<br><div class="detail">(0 = Ótakmarkað)</div>
+advancedsettings.streamport = Engin SSL Útsending port<br><div class="detail">(0 = Óvirkt)</div>
+advancedsettings.ldapenabled = Virkja LDAP Auðkenni
+advancedsettings.ldapurl = LDAP Slóð
+advancedsettings.ldapsearchfilter = LDAP Leitar Skilirði
+advancedsettings.ldapmanagerdn = LDAP Stjórn DN<br><div class="detail">(Val)</div>
+advancedsettings.ldapmanagerpassword = Lykilorð
+advancedsettings.ldapautoshadowing = Gera Sjálfkrafa Notanda í {0}
+
+# personalSettings.jsp
+personalsettings.title = Einka Stillingar Fyrir {0}
+personalsettings.language = Tungumál
+personalsettings.theme = Útlit
+personalsettings.display = Sína
+personalsettings.browse = Skoðar
+personalsettings.playlist = Lagalisti
+personalsettings.tracknumber = Lög #
+personalsettings.artist = Höfundur
+personalsettings.album = Plata
+personalsettings.genre = Tegund
+personalsettings.year = Ár
+personalsettings.bitrate = Gæði
+personalsettings.duration = Lengd
+personalsettings.format = Gerð
+personalsettings.filesize = Stærð Skráar
+personalsettings.captioncutoff = Titill Kliptaf
+personalsettings.partymode = Partý Mót
+personalsettings.shownowplaying = Sína hvað aðrir eru að Spila
+personalsettings.nowplayingallowed = Leifa Öðrum að sjá hvað ég er að Spila
+personalsettings.showchat = Sína Spjall Skilaboð
+personalsettings.finalversionnotification = Láta mig vita af Nýjum Uppfærslum
+personalsettings.betaversionnotification = Láta mig vita af Prufui Útgáfum
+personalsettings.lastfmenabled = Skrá Ég Er Að Spila Á <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm Notandanafn
+personalsettings.lastfmpassword = Last.fm Lykilorð
+personalsettings.avatar.title = Einka Mynd
+personalsettings.avatar.none = Engin Mynd
+personalsettings.avatar.custom = Valin Mynd
+personalsettings.avatar.changecustom = Breyta Einka Mynd
+personalsettings.avatar.upload = Hlaða Upp
+personalsettings.avatar.courtesy = Tákn kurteisi <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Breyta Einka Mynd
+avataruploadresult.success = Upphleðsla á Einkamynd Tókst "{0}".
+avataruploadresult.failure = Virkaði ekki að Hlaða Upp Einka Mynd. Skoða <a href="help.view?">Yfirlit</a> fyrir Upplýsingar.
+
+# passwordSettings.jsp
+passwordsettings.title = Breyta Lykilorði Fyrir {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Mappa
+musicfoldersettings.name = Nafn
+musicfoldersettings.enabled = Virkt
+musicfoldersettings.add = Bæta Tónlistar Möppu
+musicfoldersettings.nopath = Vinsamlegast veldu Möppu.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nafn
+transcodingsettings.sourceformat = Breyta Frá
+transcodingsettings.targetformat = Breyta Til
+transcodingsettings.step1 = Skref 1
+transcodingsettings.step2 = Skref 2
+transcodingsettings.step3 = Skref 3
+transcodingsettings.defaultactive = Venjulegt
+transcodingsettings.enabled = Virkt
+transcodingsettings.add = Bæta VIð transcoding
+transcodingsettings.noname = Vinsamlegast Sláðu inn Nafn.
+transcodingsettings.nosourceformat = Vinsamlegast veldu til að breyta Frá.
+transcodingsettings.notargetformat = Vinsamlegast veldu til að breyta Til.
+transcodingsettings.nostep1 = Einsamlegast Skilgreindu einn transcoding Skref.
+transcodingsettings.info = <p class="Upplýsingar">(%s = Eftirfarandi Skrá Til Eða transcoded, %b = Mest Gæði á Spilara)</p> \
+ <p>Transcoding er að vinna að breyta frá miðöldum snið til annar. {1}''s transcoding \
+ él leyfa fyrir á fjölmiðla sem myndi venjulega ekki streamable. The transcoding er flutt á-the-fljúga og doesn''t \
+ þurfa allir diskur notkun.<p/> \
+ <p>raunverulegur transcoding er gert við þriðja aðila stjórn lína forrit verður að vera uppsett í {0}. \
+ transcoding Pakki Fyrir Windows \
+ Fæst <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>Hér</b></a>. Þú getur bætt eigin sérsniðnum transcoder þinn gefið það \
+ fullnægja eftirfarandi kröfum: \
+ <ul> \
+ <li>Það verður að hafa stjórn lína tengi.</li> \
+ <li>Það verður að geta sent framleiðsla til stdout.</li> \
+ <li>Ef notað í skrefi 2 eða 3, skal hún vera fær um að lesa inntak frá stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Athugaðu að transcodings eru virkar á hverja leikmaður undirstaða af the leikmaður stillingum síðunni. Ef "Venjulegt" er merkt sem transcoding \
+ sjálfkrafa virkur fyrir nýja leikmenn.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Útsendingar Slóð
+internetradiosettings.homepageurl = Heimasíða
+internetradiosettings.name = Nafn
+internetradiosettings.enabled = Virkt
+internetradiosettings.add = Bæta Við Internet TV/Útvarp
+internetradiosettings.nourl = Vinsamlegast Sláðu inn Slóð.
+internetradiosettings.noname = Vinsamlegast Sláðu inn Nafn.
+
+# podcastSettings.jsp
+podcastsettings.update = Athuga af Nýjum Þáttum
+podcastsettings.keep = Halda
+podcastsettings.keep.all = Allir Þættir
+podcastsettings.keep.one = Nýjasti Þáttur
+podcastsettings.keep.many = Síðasti {0} Þáttur
+podcastsettings.download = Þegar nýr Þáttur er Til
+podcastsettings.download.all = Sækja Allt
+podcastsettings.download.one = Sækja þann Nýjasta
+podcastsettings.download.many = Sækja Síðasta {0} Þátt
+podcastsettings.download.none = Gera Ekkert
+podcastsettings.interval.manually = Handvirkt
+podcastsettings.interval.hourly = Hverjum Klukkutóma
+podcastsettings.interval.daily = Alla Daga
+podcastsettings.interval.weekly = Hverri Viku
+podcastsettings.folder = Vista Podcasts Í
+
+# playerSettings.jsp
+playersettings.noplayers = Enginn Spilari Fannst.
+playersettings.type = Gerð
+playersettings.lastseen = Sást Síðast
+playersettings.title = Velja Spilara
+
+playersettings.technology.web.title = Vef Spilari
+playersettings.technology.external.title = Utanaðkomandi Spilari
+playersettings.technology.external_with_playlist.title = Ytri Spilari með lagalista
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Spila tónlist beint í the vefur flettitæki með samþætta Flash player.
+playersettings.technology.external.text = Spila tónlist í uppáhalds leikmaður, svo sem WinAmp eða Windows Media Player.
+playersettings.technology.external_with_playlist.text = Sama og að ofan, en lagalista er stjórnað af the leikmaður, frekar \
+ en Subsonic framreiðslumaður. Í þessari stillingu, skipstjóri á lögin er hægt.
+playersettings.technology.jukebox.text = Spila tónlist beint á the hljómflutnings-tæki á Subsonic miðlara. (Heimilaður notendur).
+playersettings.name = Nafn Spilara
+playersettings.coverartsize = Fela Art Stærð
+playersettings.maxbitrate = Mest Gæði
+playersettings.coverart.off = Af
+playersettings.coverart.small = Lítið
+playersettings.coverart.medium = Miðlungs
+playersettings.coverart.large = Stórt
+playersettings.nolame = <em>A.T.H:</em> LAME hjartarskinn ekki virðast til vera setja í embætti. <br> Smelltu Hjálp takkann til að fá frekari upplýsingar.
+playersettings.autocontrol = Stjótna Spilara Sjálfvirkt
+playersettings.dynamicip = Spilari hefur dynamic IP Tölu
+playersettings.transcodings = Virkja transcodings
+playersettings.ok = Vista
+playersettings.forget = Eyða Spilara
+playersettings.clone = lóna Spilara
+
+# userSettings.jsp
+usersettings.title = Velja Nýjann Notanda
+usersettings.newuser = Nýr Notandi
+usersettings.admin = Notandinn er Umsjónarmaður
+usersettings.settings = Notanda er leifilegt að breyta Notandanafni og Lykilorði
+usersettings.stream = Notanda er leifilegt að spila Skrár
+usersettings.jukebox = Notanda er LEifilegt að spila Skrár í jukebox Móti
+usersettings.download = Notanda er leifiegt að sækja Skrár
+usersettings.upload = Notanda er Leifilegt að Hlaða upp Skrám
+usersettings.playlist= Notanda er leift að Búa til að Eyða Skrám
+usersettings.coverart = Notanda er leift að Fela
+usersettings.comment= Notanda er leift að Skrifa og Búa Til Einkanir og Athugasemdir
+usersettings.podcast= Notandi hefur Leifi sem Umsjónarmanna Podcasts
+usersettings.username = Notandanafn
+usersettings.changepassword = Breyta Lykilorði
+usersettings.password = Lykilorð
+usersettings.newpassword = Nýtt Lykilorð
+usersettings.confirmpassword = Staðfesta Nýtt Lykilorð
+usersettings.delete = Eyða Þessum Notanda
+usersettings.ldap = Sannvottun Notandi LDAP
+usersettings.nousername = Vanntar Notandanafn.
+usersettings.useralreadyexists = Notandi er þegar Til.
+usersettings.nopassword = Þarfnast Lykilorð.
+usersettings.wrongpassword = Lykilorðin Pössuðu ekki.
+usersettings.ldapdisabled = LDAP auðkenning er ekki virk. Sjá Ítarlegar stillingar.
+usersettings.passwordnotsupportedforldap = Get ekki sett eða Breytt Lykilorði Fyrir LDAP-auðkennisnote Ndur.
+usersettings.ok = Lykilorði Breytt fyrir Notanda {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Aldrey
+musicfoldersettings.interval.one = Daglega
+musicfoldersettings.interval.many = Hvern {0} Daga
+musicfoldersettings.hour = þann {0}:00
+
+# main.jsp
+main.up = Upp
+main.playall = Spila Allt
+main.playrandom = Spila Handahóf
+main.addall = Bæta Öllu Við
+main.tags = Breyta Tag
+main.playcount = Spilað {0} Sinnum.
+main.lastplayed = Síðast Spilað {0}.
+main.comment = Athugasemndir
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>New paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Named link</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Styrkja </ a> á (1)! <br> (Og fjarlægja þessa auglýsingu)
+main.nowplaying = Í Spilun
+main.lyrics = Lyrics
+main.minutesago = Mínútu Síðan
+main.chat = Spjall Skilaboð
+main.message = Skrifa Skilaboð
+
+# rating.jsp
+rating.rating = Einkun
+rating.clearrating = Hreinsa Einkanir
+
+# coverArt.jsp
+coverart.change = Breyta
+coverart.zoom = Stækka
+
+# allmusic.jsp
+allmusic.text = Leita af Plötu <em>{0}</em> á allmusic.com - Vinsamlegast Bíðið.
+
+# changeCoverArt.jsp
+changecoverart.title = Breyta kápa list
+changecoverart.address = Eða sláðu inn mynd slóð
+changecoverart.artist = Höfundur
+changecoverart.album = Plata
+changecoverart.searchdiscogs = Leita á Discogs
+changecoverart.wait = Vinsamlegast Bíðið...
+changecoverart.success = Mynd hefur verið sótt.
+changecoverart.error = Náði ekki að sækja mynd.
+changecoverart.noimagesfound = Engar Myndir Fundust.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Tókst ekki að breyta kápa list:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Breyta tags
+edittags.file = Skrá
+edittags.track = Lag
+edittags.songtitle = Titill
+edittags.artist = Höfundur
+edittags.album = Plata
+edittags.year = Ár
+edittags.genre = Regund
+edittags.status = Staða
+edittags.suggest = Hugmynd
+edittags.reset = Endurstilla
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Setja
+edittags.working = Vinn
+edittags.updated = Uppfæri
+edittags.skipped = Slepti
+edittags.error = Villa
+
+# donate.jsp
+donate.title = Styrkja
+donate.invalidlicense = Rangur Leifis Lykill.
+donate.amount = Styrkja {0}
+donate.textbefore = <p>Þakka þér fyrir að miðað við framlag til að styðja við (0) verkefni! \
+ Sem gjafa þú vilja fá leyfisveitandi lykill hver Slökkva auglýsingar, leyfa ótakmarkaða á að Android sími, \
+ og kennt hvernig aðrar aðgerðir iðgjald til að gefa út síðar. The leyfisveitandi er í gildi fyrir þennan \
+ og allir í framtíðinni gefa út af (0). </ p> \
+ <p> leiðbeinandi framlag upphæð er <b> € 20 </ b>, en þú getur gefið eins miklu eða eins litlu og þér líður eins. \
+ Athugaðu að leyfisveitandi lykill verður sendur á uppgefið netfang sem þú tilgreinir, svo tryggja þú gefa a \
+ réttur netfang þegar skrá er framlag á PayPal. </ p>
+donate.textafter = <p> Smelltu á einn af the hnappur til að fara til PayPal þar sem þú getur greitt með greiðslukorti eða með því að nota \
+ PayPal reikninginn þinn (ef þú ert einn). Þegar framlag er unnin, munt þú fá the leyfisveitandi lykill með tölvupósti. </ P> \
+ <p> Ef þú hefur einhverjar spurningar, vinsamlegast sendu tölvupóst á \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Þessi afrit af (2) var leyfi til (0) á (1). Þakka þér fyrir þinn styðja!
+donate.register = Eftir að þú færð leyfisveitandi lykill þinn, vinsamlegast skrá sig hér fyrir neðan.
+donate.register.email = Email
+donate.register.license = Leifi
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast Mótakari
+podcastreceiver.expandall = Sína Þætti
+podcastreceiver.collapseall = Fela Þætti
+podcastreceiver.status.new = Nýtt
+podcastreceiver.status.downloading = Sækji
+podcastreceiver.status.completed = Lokið
+podcastreceiver.status.error = Villa
+podcastreceiver.status.deleted = Eytt
+podcastreceiver.status.skipped = Slept
+podcastreceiver.downloadselected= Sækja Valið
+podcastreceiver.deleteselected= Eyða Völdu
+podcastreceiver.confirmdelete= Villtu eyða Völdu Podcasts?
+podcastreceiver.check = Athuga af nýjum Þáttum
+podcastreceiver.refresh = Endurhlaða Síðu
+podcastreceiver.settings = Podcast Stillingar
+podcastreceiver.subscribe = Gerast Áskrifandi Af Podcast
+
+# lyrics.jsp
+lyrics.title = Lyrics
+lyrics.artist = Höfundur
+lyrics.song = Tónlist
+lyrics.search = Leita
+lyrics.wait = Leita af lyrics, Vinsamlegast Bíðið...
+lyrics.courtesy = (Lyrics Eftir <a href="http://www.metrolyrics.com/" target="_blank">MetroLyrics</a>)
+lyrics.nolyricsfound = Ekkert lyrics Fannst.
+
+# helpPopup.jsp
+helppopup.title = {0} Hjálp
+helppopup.cover.title = Umslag Höfunda Stærð
+helppopup.cover.text = <p>Let's þú tilgreinir stærð á skjánum kápa list, með möguleika til að slökkva alveg. </ P>
+helppopup.transcode.title = Max Bitrate
+helppopup.transcode.text = <p> Ef þú ert nauðugur bandwidth, getur þú sett efri mörk fyrir bitrate á tónlist á. \
+ Fyrir dæmi, ef þinn frumeintak MP3 skrár eru umrita í dulmál með 256 Kbps (kilobits á sekúndu), setja max bitrate \
+ til 128 vilja gera (0) sjálfkrafa resample tónlistin 256-128 Kbps. </ p> \
+ <p> Þessi möguleiki krefst þess LAME er sett upp. LAME <a target="_blank" href="http://lame.sourceforge.net/"> (http://lame.sourceforge.net) </ a> \
+ er opinn uppspretta MP3 kóðun. Þú getur <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/"> sækja það hér </ a>. \
+ Gakktu úr skugga um að setja hana í SUBSONIC_HOME / transcode, eða í möppu sem er til staðar í PATH umhverfi breyta þínum.</p>
+helppopup.playlistfolder.title = Lagalista Mappa
+helppopup.playlistfolder.text = <p>Láttu sem þú tilgreinir í möppuna þar sem Lagalistarnir eru staðsettir.</p>
+helppopup.musicmask.title = Tónlistar Gríma
+helppopup.musicmask.text = <p>Láttu sem þú tilgreinir tegund af skrá skal vera viðurkennd sem tónlist þegar vafrað um tónlist mappa.</p>
+helppopup.coverartmask.title = Tónlistar Gríma
+helppopup.coverartmask.text = <p>Láttu sem þú tilgreinir tegund af skrá skal vera viðurkennd sem ná mála þegar vafrað um tónlist mappa.</p>
+helppopup.downsamplecommand.title = Niðurdæmis Stjórn
+helppopup.downsamplecommand.text = <p>Láttu sem þú tilgreinir í stjórn til að framkvæma þegar downsampling að lækka bitrates.</p>\
+ <p>(%s = Skráin að niðurdæmi Stjórn,% b = Mest Gæði Á Spilara)</p>
+helppopup.index.title = Forsíða
+helppopup.index.text = <p>Skulum þú tilgreinir hvernig Index (staðsett efst á skjánum) ætti að líta út. Skrár og möppur \
+ beint í tónlist rót mappa er auðvelt að nálgast með því að nota þessa vísitölu. </ p> \
+ <p> skilgreining er rúm-aðskilinn listi yfir færslur vísitölu. Venjulega, hverja færslu er bara einn stafur, \
+ en þú getur einnig tilgreint mörg tákn. Til dæmis, að innganga <em> á </ em> mun tengja allar skrár og \
+ mappa byrjar með "The". </ p> \
+ <p> Þú getur líka búið til færslu með hóp af eðli neysluverðs í parenthesis. Til dæmis, að innganga \
+ <em> AE (ABCDE) </ em> birtir sem <em> AE </ em> og tengja allar skrár og möppur sem byrja á annaðhvort \
+ A, B, C, D og E. Þetta getur verið gagnlegt fyrir hópa minna-oft notuð tákn (eins og X, Y og Z), eða \
+ að hópar accented stafi (td A, \u00c0 og \u00c1) </ p> \
+ <p> skrár og möppur sem eru ekki undir vísitölu færslu verður lögð undir vísitölu færslu "#".</ p>
+helppopup.ignoredarticles.title = Articles að hunsa
+helppopup.ignoredarticles.text = <p> skulum sem þú tilgreinir er listi af greinum (eins og "The") sem mun vera hunsuð þegar vísitölunni. </ p>
+helppopup.shortcuts.title = Flýtileiðir
+helppopup.shortcuts.text = <p> rúm-aðskilinn listi yfir helstu möppur stigi að búa til flýtileiðir á. Nota vitna í hópinn orð, til dæmis: </ p> \
+ <p> <em> Nýr Komandi "Sound lögin" </ em> </ p>
+helppopup.language.title = Tungumál
+helppopup.language.text = <p> skulum þú velur tungumál til að nota. </ p>
+helppopup.visibility.title = Skyggni
+helppopup.visibility.text = <p> Veldu hvaða atriði ætti að sýna í hverju lagi, sem og yfirskrift cutoff. Þetta er hámark \
+ fjölda stafa til að sýna að titill söngur, albúm og listamaður. </ p>
+helppopup.partymode.title = Party ham
+helppopup.partymode.text = <p> Þegar aðili stilling er virk, notendaviðmótið er einfölduð og auðveldara að starfa fyrir utan reynda notendur. \
+ Einkum tilviljun Messías upp á lagalista er að forðast. </ P>
+helppopup.theme.title = Theme
+helppopup.theme.text = <p> Let' er að velja þema til að nota. Þema skilgreinir útlit og feel af (0) varðar liti, letur, ímynd o.þ.h. </ p>
+helppopup.welcomemessage.title = Velkomin skilaboð
+helppopup.welcomemessage.text = <p> Skilaboðin sem birtist á heimasíðuna. </ p>
+helppopup.loginmessage.title = Login skilaboð
+helppopup.loginmessage.text = <p> skilaboð sem birtast á the tenging blaðsíða. </ p>
+helppopup.coverartlimit.title = Cover Art takmörk
+helppopup.coverartlimit.text = <p> hámarksfjöldi kápa list myndir til að birta á einni síðu. </ p>
+helppopup.downloadlimit.title = Download takmörk
+helppopup.downloadlimit.text = <p> efri mörk fyrir hversu mikið bandbreidd verða notuð til að hlaða niður skrám. </ p>
+helppopup.uploadlimit.title = Hlaða takmörk
+helppopup.uploadlimit.text = <p> efri mörk fyrir hversu mikið bandbreidd verða notuð til að hlaða upp skrám. </ p>
+helppopup.streamport.title = Non-SSL á höfn
+helppopup.streamport.text = <p> Þessi valkostur er aðeins máli ef þú notar (0) á netþjóni með SSL (HTTPS). </ p> Sumir spilarar \
+ (eins og Winamp) don''t styðja streymi yfir SSL. Tilgreina the höfn tala fyrir reglulegum http (venjulega 80 \
+ eða 4040) ef þú vilt don''t að vatnsföll til að senda yfir SSL. Athugaðu að vatnsföll eru ekki dulkóðuð. </ P>
+helppopup.ldap.title = LDAP auðkenning
+helppopup.ldap.text = <p> Notendur geta að auðkenna með utanaðkomandi LDAP miðlara (þ.mt Windows Active Directory). \
+ Þegar LDAP-gera kleift notandi tengja við (0), er notandanafn og lykilorð köflóttur við the yfirborð framreiðslumaður, ekki við (0) sjálft. </ P>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p> slóð á LDAP miðlara. Samskiptareglan verður annaðhvort <em> LDAP ://</ em> eða <em> ldaps ://</ í> \
+ (fyrir LDAP yfir SSL). Sjá <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank"> hér </ a> \
+ fyrir nákvæmari lýsingu. </ p>
+helppopup.ldapsearchfilter.title = LDAP Search Filter
+helppopup.ldapsearchfilter.text = <p> sía tjáning notuð sem notandinn leitar. Þetta er LDAP leit síu \
+ (eins og skilgreint í <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank"> RFC 2254 </ a>). \
+ The mynstur " '(0')" komi username, til dæmis: \
+ <ul> \
+ <li> (uid = '(0')) - þetta væri að leita að notandanafn samsvörun á UID eiginleiki. </ li> \
+ <li> (sAMAccountName = '(0')) - oftast notuð fyrir staðfesting á Microsoft Active Directory. </ li> \
+ </ ul> </ p>
+helppopup.ldapmanagerdn.title = LDAP framkvæmdastjóri DN
+helppopup.ldapmanagerdn.text = <p> Ef LDAP miðlara doesn''t styðja nafnlaus bindandi þú verður að tilgreina DN \
+ (<em> Distinguished Name </ em>) og lykilorð í LDAP notandi til að nota þegar bindandi. </ p>
+helppopup.ldapautoshadowing.title = sjálfkrafa búa LDAP notenda í (0)
+helppopup.ldapautoshadowing.text = <p> Með þessum valkosti valinn, LDAP notenda don''t að vera með höndunum búið í (0) fyrir skógarhögg á. </ p> \
+ <p> ATHUGIÐ! Þetta þýðir að allir notendur með gilt LDAP notandanafn og lykilorð geta tengja til (0), \
+ sem má ekki vera það sem þú vilt. </ p>
+helppopup.playername.title = Player nafn
+helppopup.playername.text = <p> skulum sem þú skilgreinir sem auðvelt er að muna nafn fyrir a leikmaður, svo sem "Work" eða "Stofa". </ p>
+helppopup.autocontrol.title = Control spilari sjálfkrafa
+helppopup.autocontrol.text = <p> Með þessum valkosti valinn, (0) sjálfkrafa byrja leikmaðurinn þegar þú smellir á "Play" \
+ í lagalista. Annars verður þú að byrja og tengja spilarinn sjálfur. </ P>
+helppopup.dynamicip.title = Dynamic IP tölu
+helppopup.dynamicip.text = <p> Slökkva á þennan möguleika ef leikmaðurinn notar fasta IP tölu. </ p>
+
+# wap/index.jsp
+wap.index.missing = Engin tónlist fannst
+wap.index.playlist = Playlist
+wap.index.search = Search
+wap.index.settings = Stillingar
+
+# Wap / browse.jsp
+wap.browse.playone = Spila lag
+wap.browse.playall = Play allt
+wap.browse.addone = Bæta við lagið
+wap.browse.addall = Bæta við allar
+wap.browse.downloadone = Sækja lag
+wap.browse.downloadall = Hlaða niður öllum
+
+# Wap / playlist.jsp
+wap.playlist.title = Playlist
+wap.playlist.noplayer = No leikmaður tengdur
+wap.playlist.clear = Hreinsa
+wap.playlist.load = Hlaða
+wap.playlist.random = Random
+wap.playlist.play = Play símanum
+
+# Wap / search.jsp
+wap.search.title = Search
+
+# Wap / searchResult.jsp
+wap.searchresult.index = Leitin vísitölu er verið að skapa. Vinsamlegast reyndu aftur síðar.
+
+# Wap / settings.jsp
+wap.settings.selectplayer = Veldu leikmaður
+wap.settings.allplayers = All
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_it.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_it.properties
new file mode 100644
index 00000000..ecf9538f
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_it.properties
@@ -0,0 +1,684 @@
+#
+# Italian localization.
+# Author: Michele Petrecca (michelinux at alice.it); Luca Perri (kurama_luka at yahoo.it)
+#
+
+common.home = Casa
+common.back = Indietro
+common.help = Aiuto
+common.play = Suona
+common.add = Aggiungi
+common.download = Scarica
+common.close = Chiudi
+common.refresh = Aggiorna
+common.next = Successivo
+common.previous = Precedente
+common.more = Di più
+common.ok = OK
+common.cancel = Cancella
+common.save = Salva
+common.create = Crea
+common.delete = Elimina
+common.unknown = (Sconosciuto)
+common.default = (Predefinito)
+
+# login.jsp
+login.username = Nome utente
+login.password = Password
+login.login = Accedi
+login.remember = Ricorda credenziali
+login.logout = Ora sei uscito.
+login.error = Nome utente o passoword errati
+login.insecure = Con le attuali credenziali {0} l'accesso non è sicuro. Devi accedere con nome utente e<br>password "admin" e cambiare la password immediatamente!
+
+# accessDenied.jsp
+accessDenied.title = Accesso negato
+accessDenied.text = Spiacente, non sei autorizzato a compiere l'azione richiesta.
+
+# top.jsp
+top.home = Casa
+top.now_playing = In riproduzione
+top.settings = Impostazioni
+top.status = Stato
+top.podcast = Podcast
+top.more = Di più
+top.help = Aiuto
+top.search = Ricerca
+top.upgrade = <b>Attenzione!</b> E' disponibile una nuova versione.<br>Scaricala {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">qui</a>.
+top.missing = Non è stata trovata nessuna cartella contenente musica, verifica le impostazioni.
+top.logout = Esci {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artisti<br>\
+ {1}&nbsp;album<br>\
+ {2}&nbsp;canzoni<br>\
+ {3} (&#126; {4} ore)
+left.shortcut = Scorciatoie
+left.radio = Internet TV/radio
+left.allfolders = Tutte le cartelle
+
+# playlist.jsp
+playlist.stop = Stop
+playlist.start = Suona
+playlist.confirmclear = Veramente vuoi ripulire la coda di riproduzione?
+playlist.clear = Pulisci
+playlist.shuffle = Casuale
+playlist.repeat_on = Ripetizione attiva
+playlist.repeat_off = Ripetizione disattivata
+playlist.undo = Annulla
+playlist.settings = Impostazioni
+playlist.more = Più azioni...
+playlist.more.playlist = Lista di riproduzione
+playlist.more.sortbytrack = Ordina per traccia
+playlist.more.sortbyartist = Ordina per artista
+playlist.more.sortbyalbum = Ordina per album
+playlist.more.selection = Canzoni selezionate
+playlist.more.selectall = Seleziona tutto
+playlist.more.selectnone = Nessuna selezione
+playlist.getflash = Ottieni Flash player
+playlist.load = Carica
+playlist.save = Salva
+playlist.append = Aggiungi alla coda di riproduzione
+playlist.remove = Rimuovi
+playlist.up = Sopra
+playlist.down = Sotto
+playlist.empty = La coda di riproduzione è vuota
+
+# status.jsp
+status.title = Stato
+status.type = Tipo
+status.stream = Stream
+status.download = Scarica
+status.upload = Upload
+status.player = Riproduttore
+status.user = Utente
+status.current = File corrente
+status.transmitted = Trasmessi
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Cerca
+search.query = Artista, album o titolo della canzone
+search.search = Cerca
+search.index = L'indicizzazione della ricerca è in corso di creazione. Prova di nuovo in seguito.
+search.hits.none = Nessuna occorrenza trovata.
+search.hits.more = Di più
+search.hits.artists = Artisti
+search.hits.albums = Album
+search.hits.songs = Canzoni
+
+# gettingStarted.jsp
+gettingStarted.title = Comincia ad utilizzare Subsonic
+gettingStarted.text = <p>Benvenuto in Subsonic! La configurazione non ti porterà via tempo, basterà seguire i semplici passaggi riportati di seguito.<br> \
+ Premi il bottone "Casa" nella barra degl strumenti qui sopra per tornare a questa schermata.</p> \
+ <p>Per maggiori informazioni, per favore consulta la guida <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Comincia ad utilizzare Subsonic</b></a>.</p>
+gettingStarted.step1.title = Cambia la password di amministrazione.
+gettingStarted.step1.text = Cambiando la password predefinita dell'utente di amministrazione renderai più sicuro il tuo server. \
+ Puoi anche scegliere di creare un altro utente con differenti privilegi di amministrazione.
+gettingStarted.step2.title = Imposta le cartelle della musica.
+gettingStarted.step2.text = Di' a Subsonic dove tieni la tua musica.
+gettingStarted.step3.title = Configura le imposatzioni di rete.
+gettingStarted.step3.text = Qualche utile impostazione ti permetterà di apprezzare la tua musica in via remota tramite internet, \
+ o di condividerla con la famiglia e gli amici. Ottieni il tuo indirizzo personale <b><em>tuonome</em>.subsonic.org</b>.
+gettingStarted.hide = Non mostrare questa schermata di nuovo.
+gettingStarted.hidealert = Per visualizzare nuovamente la schermata, vai in Impostazioni > Generale.
+
+
+# home.jsp
+home.random.title = Album Casuali
+home.newest.title = I più nuovi
+home.highest.title = I voti più alti
+home.frequent.title = I più richiesti
+home.recent.title = I più recenti
+home.users.title = Utenti
+home.random.text = Album casuali
+home.newest.text = Album aggiunti più di recente
+home.highest.text = Album con i voti più alti
+home.frequent.text = Album ascoltati più spesso
+home.recent.text = Album ascoltati più di recente
+home.users.text = Statistiche utente
+home.scan = La scansione delle cartelle della musica è ancora in corso, non sono ancora disponibili tutte le caratteristiche.
+home.listsize = {0} album per pagina
+home.albums = {0} - {1} Album
+home.playcount = {0} Canzoni suonate
+home.lastplayed = {0} Suonate
+home.created = {0} Create
+home.chart.total = Totale (MB)
+home.chart.stream = Trasmesse (MB)
+home.chart.download = Scaricate (MB)
+home.chart.upload = Caricate (MB)
+
+# more.jsp
+more.title = Di più
+more.random.title = Lista di riproduzione casuale
+more.random.text = Crea una lista di riproduzione casuale con
+more.random.songs = {0} canzoni
+more.random.auto = Riproduci varie canzoni casuali quando viene raggiunta la fine della lista di riproduzione.
+more.random.ok = OK
+more.random.genre = Per genere
+more.random.anygenre = Qualsiasi
+more.random.year = e anno
+more.random.anyyear = Qualsiasi
+more.random.folder = nella cartella
+more.random.anyfolder = Qualsiasi
+more.apps.title = Applicazioni Subsonic
+more.apps.text = <p>Le <a href="http://subsonic.org/pages/apps.jsp" target="_blank">applicazioni Subsonic</a> sono disponibili per <b>iPhone</b>, \
+ <b>Android</b> e <b>AIR</b>.</p>
+more.mobile.title = Telefono cellulare
+more.mobile.text = <p>Puoi controllare {0} da qualsiasi telefono cellulare o PDA con WAP abilitato.<br> \
+ Semplicemente visita l''indirizzo del sito con il tuo cellulare, ad esempio: <b>http://yourhostname/wap</b></p> \
+ <p>Ciò richiede che il tuo server, o il tuo sito, possano essere raggiungibili via Internet.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Le liste di riproduzione salvate sono disponibili come Podcast.<br>\
+ Usa il seguente indirizzo URL nel tuo software per i Podcast: <b>http://yourhostname/podcast</b>, \
+ o <b><a href="podcast.view?suffix=.rss">clicca qui</a>.</b></p>
+more.upload.title = Carica file
+more.upload.source = Seleziona file
+more.upload.target = Carica in
+more.upload.browse = Scegli
+more.upload.ok = Carica
+
+more.upload.unzip = Estrai automaticamente gli archivi zip.
+more.upload.progress = % completato. Attendere...
+
+# upload.jsp
+upload.title = Caricamento file
+upload.success = Completato con successo <b>{0}</b>
+upload.empty = Nessun file da caricare.
+upload.failed = Il caricamento è fallito per il seguente errore:<br><b>"{0}"</b>
+upload.unzipped = Zip estratto {0}
+
+# help.jsp
+help.title = Riguardo {0}
+help.upgrade = <b>Attenzione!</b> Una nuova versione è disponibile. Scaricala {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">qui</a>.
+help.version.title = Versione
+help.builddate.title = Data di build
+help.server.title = Server
+help.license.title = Termini&nbsp;di&nbsp;utilizzo
+help.license.text = {0} è un software libero distribuito sotto i termini della licenza open source <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>. \
+ {0} utilizza <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">librerie autorizzate di terze parti</a>. Per favore ricorda che {0} <em>non</em> è \
+ uno strumento per la distribuzione illegale di materiale coperto dal diritto d'autore. Rispetta sempre le disciplina legale pertinente per il tuo Paese.
+help.homepage.title = Homepage
+help.forum.title = Forum
+help.shop.title = Negozio
+help.contact.title = Contatti
+help.contact.text = {0} è sviluppato e mantenuto da Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Se hai domande, commenti o suggerimenti per migliorare il programma, visita il \
+ <a href="http://forum.subsonic.org" target="_blank">Forum di Subsonic</a>.
+help.donate = {0} è libero, ma puoi contribuire al progetto facendo una donazione<b><a href="donate.view?"></a></b>.
+help.log = Log
+help.logfile = Il file di log completo è salvato in {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Impostazioni
+settingsheader.general = Generale
+settingsheader.advanced = Avanzate
+settingsheader.personal = Personale
+settingsheader.musicFolder = Cartelle per la musica
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Riproduttori
+settingsheader.network = Rete
+settingsheader.transcoding = Transcoding
+settingsheader.user = Utenti
+settingsheader.search = Cerca
+settingsheader.coverArt = Copertine
+settingsheader.password = Password
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Cartella delle liste di riproduzione
+generalsettings.musicmask = Maschera della musica
+generalsettings.coverartmask = Maschera delle copertine
+generalsettings.index = Indice
+generalsettings.ignoredarticles = Articoli da ignorare
+generalsettings.shortcuts = Scorciatoie
+generalsettings.welcometitle = Titolo di Benvenuto
+generalsettings.welcomesubtitle = Sottotitolo di Benvenuto
+generalsettings.welcomemessage = Messaggio di Benvenuto
+generalsettings.loginmessage = Messaggio di Accesso
+generalsettings.language = Lingua predefinita
+generalsettings.theme = Tema predefinito
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Comando di Downsample
+advancedsettings.coverartlimit = Limite di copertine<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.downloadlimit = Limite di Dowlonad (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.uploadlimit = Limite di Upload (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.streamport = Porta di Strem Non-SSL <br><div class="detail">(0 = Disabled)</div>
+advancedsettings.ldapenabled = Abilita autenticazione LDAP
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = Filtro di ricerca LDAP
+advancedsettings.ldapmanagerdn = Gestore LDAP DN<br><div class="dettagli">(Opzionale)</div>
+advancedsettings.ldapmanagerpassword = Password
+advancedsettings.ldapautoshadowing = Crea automaticante gli utenti in {0}
+
+
+
+# personalSettings.jsp
+personalsettings.title = Impostazioni personali per {0}
+personalsettings.language = Lingua
+personalsettings.theme = Tema
+personalsettings.display = Visualizza
+personalsettings.browse = Naviga
+personalsettings.playlist = Lista di riproduzione
+personalsettings.tracknumber = Traccia #
+personalsettings.artist = Artista
+personalsettings.album = Album
+personalsettings.genre = Genere
+personalsettings.year = Anno
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Durata
+personalsettings.format = Formato
+personalsettings.filesize = Grandezza file
+personalsettings.captioncutoff = Limite del titolo
+personalsettings.partymode = Modalità party
+personalsettings.shownowplaying = Mostra chi altro è attivo
+personalsettings.nowplayingallowed = Permetti agli altri di vedere quando sono attivo
+personalsettings.showchat = Visualizza messaggi della chat
+personalsettings.finalversionnotification = Notificami nuove versioni
+personalsettings.betaversionnotification = Notificami nuove versioni beta
+personalsettings.lastfmenabled = Registra quello che sto ascoltando su <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Nome utente Last.fm
+personalsettings.lastfmpassword = Password Last.fm
+personalsettings.avatar.title = Immagine personale
+personalsettings.avatar.none = Nessuna immagine
+personalsettings.avatar.custom = Immagine personalizzata
+personalsettings.avatar.changecustom = Cambia immagine personalizzata
+personalsettings.avatar.upload = Carica
+personalsettings.avatar.courtesy = Icone cortesemnte fornite da <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, e \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Cambia immagine personale
+avataruploadresult.success = Immagine personale caricata con successo "{0}".
+avataruploadresult.failure = Errore durante il caricamento della immaginepersonale. Vedere il file di <a href="help.view?">log</a> per ulteriori dettagli.
+
+# passwordSettings.jsp
+passwordsettings.title = Cambia password per {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Cartella
+musicfoldersettings.name = Nome
+musicfoldersettings.enabled = Abilitato
+musicfoldersettings.add = Aggiungi cartella per la musica
+musicfoldersettings.nopath = Specifica una cartella.
+
+# networkSettings.jsp
+networksettings.text = Utilizza le impostazioni qui sotto per regolare l'accesso a Subsonic da internet.<br> \
+ Se hai problemi, per favore consulta la guida di <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Introduzione a Subsonic</b></a>.
+networksettings.portforwardingenabled = Configura automaticamente il tuo router per permettere le connessioni in entrata per Subsonic (utilizzando il port forwarding UPnP o NAT-PMP).
+networksettings.portforwardinghelp = Se il tuo router non è configurabile automaticamente, puoi configurarlo manualmente. \
+ Segui le istruzioni su <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Devi aprire la porta {0} per il computer sul quale è in esecuzione Subsonic.
+networksettings.urlredirectionenabled = Accedi al tuo server attraverso internet utilizzando un indirizzo facile da ricordare.
+networksettings.status = Status:
+networksettings.trialexpired = Il periodo di prova è finito il {0}. Per favore <b><a href="donate.view?">dona</a></b> per abilitare questa caratteristica in via permanente.
+networksettings.trialnotexpired = La caratteristica è disponibile fino al {0}. Dopo di che dovrai <b><a href="donate.view?">donare</a></b> per poterla usare permanentemente.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nome
+transcodingsettings.sourceformat = Converti da
+transcodingsettings.targetformat = Converti a
+transcodingsettings.step1 = Passo 1
+transcodingsettings.step2 = Passo 2
+transcodingsettings.step3 = Passo 3
+transcodingsettings.defaultactive = Predefinito
+transcodingsettings.enabled = Abilitato
+transcodingsettings.add = Aggiungi transcoding
+transcodingsettings.noname = Specifica un nome.
+transcodingsettings.nosourceformat = Specifica il formato dal quale convertire.
+transcodingsettings.notargetformat = Specifica il formato in cui convertire.
+transcodingsettings.nostep1 = Specifica almeno un passo del Transcoding.
+transcodingsettings.info = <p class="detail">(%s = File da convertire, %b = Bitrate massimo del riproduttore, %t = Titolo, %a = Artista, %l = Album)</p> \
+ <p> Il Transcoding è il processo di conversione da un formato multimediale ad un altro. Il motore di Transcoding di {1} \
+ permette lo streaming di media che altrimenti non lo sarebbero. Il transcoding è fatto al volo e non \
+ necessita di alcun utilizzo del disco.<p/> \
+ <p>Attualmente il transcoding attuale è fatto da programmi a linea di comando di terze parti, che, dunque, devono essere installati in {0}. \
+ Un pacchetto di Transcofing per Windows \
+ è disponibile <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>qui</b></a>. Puoi aggiungere i tuoi transcoder personalizzati \
+ purché soddisfino i seguenti requisiti: \
+ <ul> \
+ <li>Devono avere una interfaccia a linea di comando.</li> \
+ <li>Devono essere in grado di inviare l'output a stdout.</li> \
+ <li>Se sono utilizzati gli step 2 o 3, devono poter leggere l'input da stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Nota che il transcodingsono va attivato per il singolo lettore, attraverso la pagina delle impostanzioni dei lettori. Se è spuntato "Predefinito", \
+ il transcoding è attivato automaticamente per i nuovi riproduttori.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradiosettings.homepageurl = Homepage
+internetradiosettings.name = Nome
+internetradiosettings.enabled = Abilitato
+internetradiosettings.add = Aggiungi Internet TV/radio
+internetradiosettings.nourl = Specifica un URL.
+internetradiosettings.noname = Specifica un nome.
+
+# podcastSettings.jsp
+podcastsettings.update = Verifica nuovi episodi
+podcastsettings.keep = Tieni
+podcastsettings.keep.all = Tutti gli episodi
+podcastsettings.keep.one = Episodi più recenti
+podcastsettings.keep.many = Ultimi {0} episodi
+podcastsettings.download = Quando dei nuovi episodi sono disponibili
+podcastsettings.download.all = Scaricali tutti
+podcastsettings.download.one = Scarica solo quelli più recenti
+podcastsettings.download.many = Scarica solo gli ultimi {0} episodi
+podcastsettings.download.none = Non fare nulla
+podcastsettings.interval.manually = Manualmente
+podcastsettings.interval.hourly = Ogni ora
+podcastsettings.interval.daily = Ogni giorno
+podcastsettings.interval.weekly = Ogni settimana
+podcastsettings.folder = Salva Podcast in
+
+# playerSettings.jsp
+playersettings.noplayers = Nessun riproduttore trovato.
+playersettings.type = Tipo
+playersettings.lastseen = Visualizzato l'ultima volta
+playersettings.title = Seleziona riproduttore
+playersettings.technology.web.title = Riproduttore web
+playersettings.technology.external.title = Riproduttore esterno
+playersettings.technology.external_with_playlist.title = Riproduttore esterno con lista di riproduzione
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Riproduci la musica direttamente dal web browser utilizzando il riproduttore Flash integrato.
+playersettings.technology.external.text = Riproduci la musica nel tuo programma di riproduzione preferito, come WinAmp o Windows Media Player.
+playersettings.technology.external_with_playlist.text = Come sopra, ma la lista di riproduzione è gestita dal programma di riproduzione, invece \
+ che dal server Subsonic. In questa modalità è possibile passare da una canzone all'altra.
+playersettings.technology.jukebox.text = Riproduci la musica direttamente dalla macchina su cui è installato il sever di Subsonic. (Solo utenti autorizzati).
+playersettings.name = Nome Riproduttore
+playersettings.coverartsize = Dimensione delle copertine
+playersettings.maxbitrate = Bitrate massimo
+playersettings.coverart.off = Nessuna
+playersettings.coverart.small = Piccola
+playersettings.coverart.medium = Media
+playersettings.coverart.large = Grande
+playersettings.nolame = <em>Attenzione:</em> LAME non sembra essere installato.<br>Clicca sul pulsante d'Aiuto per maggiori informazioni.
+playersettings.autocontrol = Controlla il Riproduttore automaticamente
+playersettings.dynamicip = Il Riproduttore ha un indirizzo IP dinamico
+playersettings.transcodings = Attiva Transcoding
+playersettings.ok = Salva
+playersettings.forget = Elimina riproduttore
+playersettings.clone = Clona riproduttore
+
+# userSettings.jsp
+usersettings.title = Seleziona utente
+usersettings.newuser = Nuovo utente
+usersettings.admin = L'utente è anche un amministratore
+usersettings.settings = L'utente è abilitato al cambio delle impostazioni e della password
+usersettings.stream = All'utente è permesso suonare i file
+usersettings.jukebox = All'utente è permesso riprodurre file in modalità Jukebox
+usersettings.download = All'utente è permesso scaricare i file
+usersettings.upload = All'utente è permesso aggiungere file
+usersettings.playlist= All'utente è permesso creare e cancellare le liste di riproduzione
+usersettings.coverart = All'utente è permesso cambiare le copertine e modificare i tag
+usersettings.comment= All'utente è permesso creare e modificare commenti e votazioni
+usersettings.podcast= All'utente è permesso amministrate i Podcast
+usersettings.username = Nome utente
+usersettings.changepassword = Cambia password
+usersettings.password = Password
+usersettings.newpassword = Nuova password
+usersettings.confirmpassword = Conferma password
+usersettings.delete = Cancella questo utente
+usersettings.ldap = Autentica l'utente tramite LDAP
+usersettings.nousername = Username mancante
+usersettings.useralreadyexists = L'utente già esiste.
+usersettings.nopassword = E' richiesta la password.
+usersettings.wrongpassword = Le password inserite non sono uguali.
+usersettings.ldapdisabled = L'autenticazione tramite LDAP non è abilitata. Vedi le Impostazioni avanzate.
+usersettings.passwordnotsupportedforldap = Non è possibile impostare o cambiare la password per gli utenti autenticati tramite LDAP.
+usersettings.ok = La password è stata cambiata con successo per l'utente {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Mai
+musicfoldersettings.interval.one = Ogni giorno
+musicfoldersettings.interval.many = Ogni {0} days
+musicfoldersettings.hour = alle {0}:00
+
+# main.jsp
+main.up = Su
+main.playall = Riproduci tutto
+main.playrandom = Riproduci casualmente
+main.addall = Aggiungi tutto
+main.tags = Modifica tag
+main.playcount = Riprodotto {0} volte.
+main.lastplayed = Riprodotto l''ultima volta il {0}.
+main.comment = Commenta
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__testo__</td><td>Testo in grassetto </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Interruzione di linea</td></tr>\
+ <tr><td style="padding-right:1em">~~testo~~</td><td>Testo in corsivo </td><td style="padding-left:3em;padding-right:1em">(linea vuota) </td><td>Nuovo paragrafo</td></tr>\
+ <tr><td style="padding-right:1em">* testo </td><td>Lista di oggetti </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Collegamento</td></tr>\
+ <tr><td style="padding-right:1em">1. testo </td><td>Lista numerata</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Collegamento con nome</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Dona</a> a {1}!<br>(e rimuovi questa inserzione)
+main.nowplaying = Ora in riproduzione
+main.lyrics = Testo
+main.minutesago = minuti fa
+main.chat = Messaggi di chat
+main.message = Scrivi un messaggio
+main.clearchat = Cancella messaggi
+
+# rating.jsp
+rating.rating = Valutazione
+rating.clearrating = Cancella valutazione
+
+# coverArt.jsp
+coverart.change = Cambia
+coverart.zoom = Zoom
+
+# allmusic.jsp
+allmusic.text = Cerca dell'Album <em>{0}</em> in allmusic.com - Attendi.
+
+# changeCoverArt.jsp
+changecoverart.title = Cambia copertina
+changecoverart.address = O inserisci l'indirizzo dell'immagine
+changecoverart.artist = Artista
+changecoverart.album = Album
+changecoverart.searchdiscogs = Cerca discografia
+changecoverart.wait = Attendere prego...
+changecoverart.success = L'immagine è stata scaricata con successo.
+changecoverart.error = Fallimento nel download dell'immagine.
+changecoverart.noimagesfound = Nessuna immagine trovata.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Fallimento nel cambio di copertina:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Modifica tag
+edittags.file = File
+edittags.track = Traccia
+edittags.songtitle = Titolo
+edittags.artist = Artista
+edittags.album = Album
+edittags.year = Anno
+edittags.genre = Genere
+edittags.status = Stato
+edittags.suggest = Suggerisci
+edittags.reset = Azzera
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Imposta
+edittags.working = In corso
+edittags.updated = Aggiornato
+edittags.skipped = Saltato
+edittags.error = Errore
+
+# donate.jsp
+donate.title = Dona
+donate.invalidlicense = Invalid license key.
+donate.amount = Dona {0}
+donate.textbefore = <p>Grazie per aver preso in considerazione una donazione al progetto {0}! \
+ I donatori hanno accesso ad alcune caratteristiche aggiuntive:</p> \
+ <ul> \
+ <li>Utilizzo illimitato delle <a href="http://subsonic.org/pages/apps.jsp" target="blank">applicazioni</a> Subsonic per iPhone, Android e AIR.</li> \
+ <li>Il tuo indirizzo personale del server: <em>tuonome</em>.subsonic.org (guarda <a href="networkSettings.view">Impostazioni &gt; Rete</a>).</li> \
+ <li>Nessuna inserzione nell''interfaccia web.</li> \
+ <li>Altre caratteristiche che verranno aggiunte in seguito.</li> \
+ </ul> \
+ <p> \
+ Come donatore riceverai una chiave di licenza valida per questa \
+ e tutte le future release {0}.</p> \
+ <p>L'ammontare suggerito per la donazione è di <b>&euro;20</b>, ma puoi selezionare qualsiasi ammontare tu preferisca:</p>
+donate.textafter = <p>Clicca un bottone per entrare in PayPal dove potrai pagare con carta di credito o utilizzando \
+ il tuo account PayPal (se ne hai uno). Riceverai la chiave di licenza via email in pochi minuti.</p> \
+ <p>Per qualsiasi domanda invia una email a \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Questa copia di Subsonic {2} è stata licenziata a {0} il {1}. Grazie per il tuo supporto!
+donate.register = Dopo aver ricevuto la chiave di licenza, registrala qui sotto.
+donate.register.email = Email
+donate.register.license = Licenza
+
+# podcastReceiver.jsp
+podcastreceiver.title = Ricevitore Podcast
+podcastreceiver.expandall = Mostra episodi
+podcastreceiver.collapseall = Nascondi episodi
+podcastreceiver.status.new = Nuovo
+podcastreceiver.status.downloading = Scaricamento
+podcastreceiver.status.completed = Completato
+podcastreceiver.status.error = Errore
+podcastreceiver.status.deleted = Eliminato
+podcastreceiver.status.skipped = Saltato
+podcastreceiver.downloadselected= Scarica selezionato
+podcastreceiver.deleteselected= Elimina selezionato
+podcastreceiver.confirmdelete= Cancellare davvero i Podcast selezionati?
+podcastreceiver.check = Verifica nuovi episodi
+podcastreceiver.refresh = Aggiorna pagina
+podcastreceiver.settings = Impostazioni Podcast
+podcastreceiver.subscribe = Iscriviti a Podcast
+
+# lyrics.jsp
+lyrics.title = Testi
+lyrics.artist = Artista
+lyrics.song = Canzone
+lyrics.search = Cerca
+lyrics.wait = Ricerca dei testi in corso. Attendere...
+lyrics.courtesy = (Testi di <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Nessun testo trovato.
+
+# helpPopup.jsp
+helppopup.title = {0} Aiuto
+helppopup.cover.title = Dimensione copertina
+helppopup.cover.text = <p>Permette di specificare la dimensione della copertina visualizzata, con la possibilità di eliminarla completamente.</p>
+helppopup.transcode.title = Bitrate massimo
+helppopup.transcode.text = <p>Se hai un''ampiezza di banda limitata, potresti voler impostare un limite maggiore al bitrate della musica trasmessa. \
+ Per esempio, se il tuo file mp3 originale è codificato utilizzando 256 Kbps (kilobit al secondo), impostando il bitrate massimo \
+ a 128 {0} ricampionerà automaticamente la musica da 256 a 128 Kbps.</p> \
+ <p>Questa opzione richiede che LAME sia installato. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ è un decodificatore open source per mp3. Puoi <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">scaricarlo qui</a>. \
+ Assicurati di installarlo in SUBSONIC_HOME/transcode, o in una directory che sia presente nella tua variabile d'ambiente PATH.</p>
+helppopup.playlistfolder.title = Cartella delle liste di riproduzione
+helppopup.playlistfolder.text = <p>Permette di specificare la cartella dove sono collocate le tue liste di riproduzione.</p>
+helppopup.musicmask.title = Maschera musicale
+helppopup.musicmask.text = <p>Permette di specificare il tipo di file che dovrebbero esser riconosciuti come musica quando avviene la ricerca nelle cartelle della musica.</p>
+helppopup.coverartmask.title = Maschera delle copertine
+helppopup.coverartmask.text = <p>Permette di specificare il tipo di file che dovrebbero esser riconosciuti come copertine quando avviene la ricerca nelle cartelle della musica..</p>
+helppopup.downsamplecommand.title = Comando Downsample
+helppopup.downsamplecommand.text = <p>Permette di specificare il comando da eseguirsi quando avviene il downsampling ad un minore bitrate.</p>\
+ <p>(%s = E'' il file che deve essere sottoposto a downsample, %b = Il numero massimo di bitrate del riproduttore)</p>
+helppopup.index.title = Indice
+helppopup.index.text = <p>Permette di specificare l''aspetto dell'indice (posizionato sulla sinistra dello schermo). Con l''indice si può accedere \
+ in modo semplice ai file e alle cartelle posizionale nella cartella della musica.</p> \
+ <p> L''indicizzazione è composta da una lista di varie voci separate da spazi. Normalmente ogni voce è composta da un singolo carattere, \
+ ma puoi anche inserire più caratteri. Per esempio, la voce <em>The</em> indirizzerà a tutti i file e \
+ cartelle che iniziano per "The".</p> \
+ <p>Puoi anche creare una voce utilizzando un indice di caratteri in parentesi. Per esempio la voce \
+ <em>A-E(ABCDE)</em> verrà visualizzata come <em>A-E</em> e indirizzerà a tutti i file e alle cartelle che iniziano per \
+ A, B, C, D o E. Può essere utile per raggruppare caratteri utilizzati meno di frequente (come X, Y e Z), o \
+ per raggruppare caratteri accentati (come A, \u00c0 e \u00c1)</p> \
+ <p>I file e le cartelle che non sono ricomprese in alcuna voce dell''indice verranno visualizzati alla voce "#".</p>
+helppopup.ignoredarticles.title = Articoli da ignorare
+helppopup.ignoredarticles.text = <p>Permette di specificare una lista degli articoli (come "The") che verranno ignorati nel creare l''indice.</p>
+helppopup.shortcuts.title = Collegamenti
+helppopup.shortcuts.text = <p>Una lista, separata da spazi, delle cartelle di livello più alto in cui creare collegamenti. Utilizza i doppi apici per creare gruppi di parole, per esempio:</p> \
+ <p><em>Nuovi ingressi "Colonne sonore"</em></p>
+helppopup.language.title = Lingue
+helppopup.language.text = <p>Permette di selezionare la lingua da utilizzare.</p>
+helppopup.visibility.title = Visibilità
+helppopup.visibility.text = <p>Seleziona quali dettagli vadano visualizzati per ogni canzone, come il Limite del titolo. Quest''ultimo è il massimo \
+ numero di caratteri da visualizzarsi per titolo della canzone, album e artista.</p>
+helppopup.partymode.title = Modalità Party
+helppopup.partymode.text = <p>Quando la modalità Party è abilitata l''interfaccia utente è semplificata e di più facile utilizzo per gli utenti non esperti. \
+ In particolare, è impedito lo stravolgimento accidentale della lista di riproduzione.</p>
+helppopup.theme.title = Temi
+helppopup.theme.text = <p>Permette di selezionare il tema da usare. Il tema definisce il look e la sensazione di {0} in quanto a colori, caratteri, immagini ecc.</p>
+helppopup.welcomemessage.title = Messaggio di Benvenuto
+helppopup.welcomemessage.text = <p>Il messaggio che è visualizzato nella pagina principale.</p>
+helppopup.loginmessage.title = Messaggio di accesso
+helppopup.loginmessage.text = <p>Il messaggio che è visualizzato nella pagina d''accesso.</p>
+helppopup.coverartlimit.title = Limite alle copertine
+helppopup.coverartlimit.text = <p>Il numero massimo di copertine che può essere visualizzato in una singola pagina.</p>
+helppopup.downloadlimit.title = Limite allo scaricamento
+helppopup.downloadlimit.text = <p>Il limite massimo di banda che può essere utilizzata per lo scaricamento dei file.</p>
+helppopup.uploadlimit.title = Limite al caricamento
+helppopup.uploadlimit.text = <p>Il limite massimo di banda che può essere utilizzata per il caricamento dei file.</p>
+helppopup.streamport.title = Porta di stream Non-SSL
+helppopup.streamport.text = <p>Questa opzione è rilevante solo se {0} è su un server con SSL (HTTPS).</p><p> Alcuni riproduttori \
+ (come Winamp) non supportano lo streaming su SSL. Specifica il numero di porta per il normale http (di solito 80 \
+ o 4040) se non vuoi che lo stream sia trasmesso tramite SSL. Nota che lo stream non verrà criptato.</p>
+helppopup.ldap.title = Autenticazione con LDAP
+helppopup.ldap.text = <p>Gli utenti possono essere autenticati tramite un server esterno LDAP (compreso Windows Active Directory). \
+ Quanto gli utenti sono connessi a {0} tramite LDAP, il nome utente e la password sono controllati dal server esterno e non dallo stesso {0}.</p>
+helppopup.ldapurl.title = URL LDAP
+helppopup.ldapurl.text = <p>L''URL del server LDAP. Il protocollo deve essere <em>ldap://</em> o <em>ldaps://</em> \
+ (per LDAP su SSL). Guarda <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">qui</a> \
+ per una descrizione più dettagliata.</p>
+helppopup.ldapsearchfilter.title = Filtro di ricerca LDAP
+helppopup.ldapsearchfilter.text = <p>L''espressione filtro utilizzata nella ricerca utente. E'' un filtro di ricerca LDAP \
+ (come definito dall'' <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ L''espressione "'{0'}" è sostituita dal nome utente, per esempio: \
+ <ul>\
+ <li>(uid='{0'}) - questa sarebbe la ricerca del nome utente combaciante con l''uid attribuito.</li> \
+ <li>(sAMAccountName='{0'}) - tipicamente utilizzato nella Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>Se il server LDAP non supporta alcun binding anonimo devi specififare il DN \
+ (<em>Distinguished Name</em>) e la password dell''utente LDAP da utilizzare per il binding.</p>
+helppopup.ldapautoshadowing.title = Crea automaticamente gli utenti LDAP in {0}
+helppopup.ldapautoshadowing.text = <p>Con questa opzione attivata gli utenti LDAP non devono essere creati manualmente in {0} prima della loro autenticazione.</p> \
+ <p>NOTA! Ciò significa che qualsiasi utente LDAP con un nome utente ed una password valida può effettuare l''accesso a {0}, \
+ e potrebbe non essere quello che desideri.</p>
+helppopup.playername.title = Nome riproduttore
+helppopup.playername.text = <p>Permette di specifica un nome per il riproduttore facile da ricordare, come "Lavoro" o "Salotto".</p>
+helppopup.autocontrol.title = Controlla il riproduttore automaticamente
+helppopup.autocontrol.text = <p>Se questa opzione è selezionata, {0} avvierà automaticamente il riproduttore quando clicchi "Suona" \
+ nella coda di riproduzione. Altrimenti dovrai avviare e connettere il riproduttore automaticamente.</p>
+helppopup.dynamicip.title = Indirizzo IP Dinamico
+helppopup.dynamicip.text = <p>Disattiva questa opzione se il riproduttore utilizza un indirizzo IP statico.</p>
+
+# wap/index.jsp
+wap.index.missing = Musica non trovata
+wap.index.playlist = Lista di riproduzione
+wap.index.search = Cerca
+wap.index.settings = Impostazioni
+
+# wap/browse.jsp
+wap.browse.playone = Suona canzone
+wap.browse.playall = Suona tutto
+wap.browse.addone = Aggiungi canzone
+wap.browse.addall = Aggiungi tutto
+wap.browse.downloadone = Scarica canzone
+wap.browse.downloadall = Scarica tutto
+
+# wap/playlist.jsp
+wap.playlist.title = Playlist
+wap.playlist.noplayer = Nessun riproduttore connesso
+wap.playlist.clear = Cancella
+wap.playlist.load = Carica
+wap.playlist.random = Casuale
+wap.playlist.play = Riproduci su telefono
+
+# wap/search.jsp
+wap.search.title = Cerca
+
+# wap/searchResult.jsp
+wap.searchresult.index = L'indice di ricerca è in corso di creazione. Riprova più tardi.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Seleziona riproduttore
+wap.settings.allplayers = Tutto
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ja_JP.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ja_JP.properties
new file mode 100644
index 00000000..b3705d8f
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ja_JP.properties
@@ -0,0 +1,641 @@
+#
+# Japanese localization.
+# Author: Takahiro Suzuki (takahiro.suzuki.ja GMAIL)
+# Updated: 20090416 fixed a few errors
+# 20090415
+
+common.home = \u30db\u30fc\u30e0
+common.back = \u623b\u308b
+common.help = \u30d8\u30eb\u30d7
+common.play = \u518d\u751f
+common.add = \u8ffd\u52a0
+common.download = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+common.close = \u9589\u3058\u308b
+common.refresh = \u66f4\u65b0
+common.next = \u6b21
+common.previous = \u524d
+common.more = \u6b21..
+common.ok = OK
+common.cancel = \u30ad\u30e3\u30f3\u30bb\u30eb
+common.save = \u4fdd\u5b58
+common.create = \u65b0\u898f\u4f5c\u6210
+common.delete = \u524a\u9664
+common.unknown = (\u4e0d\u660e)
+common.default = (\u30c7\u30d5\u30a9\u30eb\u30c8)
+
+# login.jsp
+login.username = \u30e6\u30fc\u30b6\u540d
+login.password = \u30d1\u30b9\u30ef\u30fc\u30c9
+login.login = \u30ed\u30b0\u30a4\u30f3
+login.remember = \u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u8a18\u61b6
+login.logout = \u30ed\u30b0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002
+login.error = \u30e6\u30fc\u30b6\u540d\u3082\u3057\u304f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9055\u3044\u307e\u3059\u3002
+login.insecure = {0} \u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u30e6\u30fc\u30b6\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u3068\u3082\u306b"admin"\u3068\u3057\u3066\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u304b\u3001<br /><a href="login.view?user=admin&amp;password=admin">\u3053\u3053</a>\u3092\u30af\u30ea\u30c3\u30af\u3057\u3001\u305f\u3060\u3061\u306b\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+
+# accessDenied.jsp
+accessDenied.title = \u30a2\u30af\u30bb\u30b9\u62d2\u5426
+accessDenied.text = \u8981\u6c42\u3055\u308c\u305f\u64cd\u4f5c\u3092\u884c\u3046\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093\u3002
+
+# top.jsp
+top.home = \u30db\u30fc\u30e0
+top.now_playing = \u518d\u751f\u4e2d
+top.settings = \u8a2d\u5b9a
+top.status = \u72b6\u614b
+top.podcast = Podcast
+top.more = \u305d\u306e\u4ed6
+top.help = \u30d8\u30eb\u30d7
+top.search = \u691c\u7d22
+top.upgrade = <b>\u6ce8\u610f!</b> \u65b0\u3057\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u3066\u3044\u307e\u3059\u3002<br>{0} {1} \
+ \u3092<a href="#" onclick="window.open(''http://subsonic.org/'')">\u3053\u3053</a>\u304b\u3089\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+top.missing = \u97f3\u697d\u30d5\u30a9\u30eb\u30c0\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u8a2d\u5b9a\u3092\u898b\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+top.logout = {0} \u3092\u30ed\u30b0\u30a2\u30a6\u30c8\u3059\u308b
+
+# left.jsp
+left.statistics = \u30a2\u30fc\u30c6\u30a3\u30b9\u30c8\u6570&nbsp;{0}<br>\
+ \u30a2\u30eb\u30d0\u30e0\u6570&nbsp;{1}<br>\
+ \u66f2\u76ee\u6570&nbsp;{2}<br>\
+ {3} (&#126; {4} \u6642\u9593)
+left.shortcut = \u30b7\u30e7\u30fc\u30c8\u30ab\u30c3\u30c8
+left.radio = \u30cd\u30c3\u30c8TV/\u30e9\u30b8\u30aa
+left.allfolders = \u5168\u3066\u306e\u30d5\u30a9\u30eb\u30c0
+
+# playlist.jsp
+playlist.stop = \u505c\u6b62
+playlist.start = \u518d\u751f
+playlist.confirmclear = \u672c\u5f53\u306b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u307e\u3059\u304b\uff1f
+playlist.clear = \u7a7a\u306b\u3059\u308b
+playlist.shuffle = \u30b7\u30e3\u30c3\u30d5\u30eb
+playlist.repeat_on = \u30ea\u30d4\u30fc\u30c8ON
+playlist.repeat_off = \u30ea\u30d4\u30fc\u30c8OFF
+playlist.undo = \u5143\u306b\u623b\u3059
+playlist.settings = \u8a2d\u5b9a
+playlist.more = \u305d\u306e\u4ed6...
+playlist.more.playlist = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u64cd\u4f5c
+playlist.more.sortbytrack = \u30bf\u30a4\u30c8\u30eb\u3067\u30bd\u30fc\u30c8
+playlist.more.sortbyartist = \u30a2\u30fc\u30c6\u30a3\u30b9\u30c8\u3067\u30bd\u30fc\u30c8
+playlist.more.sortbyalbum = \u30a2\u30eb\u30d0\u30e0\u3067\u30bd\u30fc\u30c8
+playlist.more.selection = \u66f2\u76ee\u306e\u9078\u629e
+playlist.more.selectall = \u5168\u9078\u629e
+playlist.more.selectnone = \u5168\u89e3\u9664
+playlist.getflash = Flash Player\u3092\u5165\u624b\u3059\u308b
+playlist.load = \u8aad\u8fbc
+playlist.save = \u4fdd\u5b58
+playlist.append = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306b\u8ffd\u52a0
+playlist.remove = \u524a\u9664
+playlist.up = \u4e0a\u3078
+playlist.down = \u4e0b\u3078
+playlist.empty = \u7a7a\u306e\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8
+
+# status.jsp
+status.title = \u72b6\u614b
+status.type = \u7a2e\u985e
+status.stream = \u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f
+status.download = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+status.upload = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9
+status.player = \u30d7\u30ec\u30fc\u30e4
+status.user = \u30e6\u30fc\u30b6
+status.current = \u73fe\u5728\u306e\u30d5\u30a1\u30a4\u30eb
+status.transmitted = \u8ee2\u9001\u91cf
+status.bitrate = \u30d3\u30c3\u30c8\u30ec\u30fc\u30c8 (Kbps)
+
+# search.jsp
+search.title = \u691c\u7d22
+search.search = \u691c\u7d22
+search.index = \u691c\u7d22\u7528\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3092\u4f5c\u6210\u4e2d\u3067\u3059\u3002\u6642\u9593\u3092\u304a\u3044\u3066\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+search.hits.none = \u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+
+# home.jsp
+home.random.title = \u30e9\u30f3\u30c0\u30e0
+home.newest.title = \u6700\u8fd1\u306e\u66f2
+home.highest.title = \u30ec\u30fc\u30c8\u306e\u9ad8\u3044\u66f2
+home.frequent.title = \u3088\u304f\u518d\u751f\u3059\u308b\u66f2
+home.recent.title = \u6700\u8fd1\u518d\u751f\u3057\u305f\u66f2
+home.users.title = \u30e6\u30fc\u30b6
+home.random.text = \u30e9\u30f3\u30c0\u30e0
+home.newest.text = \u6700\u8fd1\u8ffd\u52a0\u3055\u308c\u305f\u304b\u3001\u5909\u66f4\u3055\u308c\u305f\u30a2\u30eb\u30d0\u30e0
+home.highest.text = \u30ec\u30fc\u30c8\u306e\u9ad8\u3044\u30a2\u30eb\u30d0\u30e0
+home.frequent.text = \u3088\u304f\u518d\u751f\u3059\u308b\u30a2\u30eb\u30d0\u30e0
+home.recent.text = \u6700\u8fd1\u518d\u751f\u3057\u305f\u30a2\u30eb\u30d0\u30e0
+home.users.text = \u30e6\u30fc\u30b6\u306e\u7d71\u8a08\u60c5\u5831
+home.scan = \u97f3\u697d\u30d5\u30a9\u30eb\u30c0\u3092\u30b9\u30ad\u30e3\u30f3\u4e2d\u3067\u3059\u3002\u307e\u3060\u5168\u6a5f\u80fd\u306f\u6709\u52b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
+home.listsize = 1 \u30da\u30fc\u30b8 {0} \u30a2\u30eb\u30d0\u30e0
+home.albums = \u30a2\u30eb\u30d0\u30e0: {0} - {1}
+home.playcount = {0} \u66f2\u518d\u751f
+home.lastplayed = \u6700\u8fd1\u306e\u518d\u751f: {0}
+home.created = \u6700\u8fd1\u306e\u5909\u66f4: {0}
+home.chart.total = \u7dcf\u8ee2\u9001\u91cf (MB)
+home.chart.stream = \u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f (MB)
+home.chart.download = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9 (MB)
+home.chart.upload = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 (MB)
+
+# more.jsp
+more.title = \u305d\u306e\u4ed6
+more.random.title = \u30e9\u30f3\u30c0\u30e0\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8
+more.random.text = \u4f5c\u6210\u3059\u308b\u30e9\u30f3\u30c0\u30e0\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u5927\u304d\u3055
+more.random.songs = {0} \u66f2
+more.random.auto = \u6700\u5f8c\u307e\u3067\u518d\u751f\u3057\u305f\u3089\u66f4\u306b\u30e9\u30f3\u30c0\u30e0\u306a\u66f2\u3092\u518d\u751f\u3059\u308b
+more.random.ok = OK
+more.random.genre = \u30b8\u30e3\u30f3\u30eb:
+more.random.anygenre = \u4efb\u610f
+more.random.year = \u5e74:
+more.random.anyyear = \u4efb\u610f
+more.random.folder = \u30d5\u30a9\u30eb\u30c0:
+more.random.anyfolder = \u4efb\u610f
+more.mobile.title = \u30e2\u30d0\u30a4\u30eb\u6a5f\u5668\u304b\u3089\u306e\u30a2\u30af\u30bb\u30b9
+more.mobile.text = <p>WAP\u306b\u5bfe\u5fdc\u3057\u305f\u643a\u5e2f\u96fb\u8a71\u3084PDA\u304b\u3089 {0} \u3092\u64cd\u4f5c\u3067\u304d\u307e\u3059\u3002<br> \
+ \u6b21\u306eURL\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044: <b>http://yourhostname/wap</b></p> \
+ <p>\u3053\u306e\u6a5f\u80fd\u3092\u5229\u7528\u3059\u308b\u306b\u306f\u3001\u30b5\u30fc\u30d0\u304c\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u306e\u63a5\u7d9a\u3092\u53d7\u3051\u5165\u308c\u308b\u3088\u3046\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>\u4fdd\u5b58\u3055\u308c\u305f\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306fPodcast\u3068\u3057\u3066\u3082\u4f7f\u3048\u307e\u3059\u3002<br>\
+ Podcast\u5bfe\u5fdc\u30bd\u30d5\u30c8\u304b\u3089\u6b21\u306eURL\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044: <b>http://yourhostname/podcast</b>, \
+ \u307e\u305f\u306f\u3001 <b><a href="podcast.view?suffix=.rss">\u3053\u3053\u3092\u30af\u30ea\u30c3\u30af</a>\u3057\u3066\u304f\u3060\u3055\u3044\u3002</b></p>
+more.upload.title = \u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b
+more.upload.source = \u30d5\u30a1\u30a4\u30eb\u3092\u9078\u629e\u3059\u308b
+more.upload.target = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u5148
+more.upload.browse = \u53c2\u7167
+more.upload.ok = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9
+more.upload.unzip = zip\u30d5\u30a1\u30a4\u30eb\u3092\u81ea\u52d5\u5c55\u958b\u3059\u308b
+more.upload.progress = % \u9032\u884c\u3002\u304a\u5f85\u3061\u304f\u3060\u3055\u3044...
+
+# upload.jsp
+upload.title = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u4e2d
+upload.success = <b>{0}</b> \u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f
+upload.empty = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3059\u308b\u30d5\u30a1\u30a4\u30eb\u304c\u3042\u308a\u307e\u305b\u3093\u3002
+upload.failed = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f:<br><b>"{0}"</b>
+upload.unzipped = zip\u30d5\u30a1\u30a4\u30eb {0} \u3092\u5c55\u958b\u3057\u307e\u3057\u305f\u3002
+
+# help.jsp
+help.title = {0} \u306b\u3064\u3044\u3066
+help.upgrade = <b>\u6ce8\u610f!</b> \u65b0\u3057\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u3066\u3044\u307e\u3059\u3002<br>{0} {1} \
+ \u3092<a href="#" onclick="window.open(''http://subsonic.org/'')">\u3053\u3053</a>\u304b\u3089\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+help.version.title = \u30d0\u30fc\u30b8\u30e7\u30f3
+help.builddate.title = \u30d3\u30eb\u30c9\u65e5
+help.server.title = \u30b5\u30fc\u30d0
+help.license.title = \u30e9\u30a4\u30bb\u30f3\u30b9
+help.license.text = {0} \u306f <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> \u30e9\u30a4\u30bb\u30f3\u30b9\u306e\u4e0b\u306b\u914d\u5e03\u3055\u308c\u308b\u30d5\u30ea\u30fc\u30bd\u30d5\u30c8\u30a6\u30a7\u30a2\u3067\u3059\u3002 \
+ {0} \u306f <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">\u30b5\u30fc\u30c9\u30d1\u30fc\u30c6\u30a3\u306e\u30e9\u30a4\u30d6\u30e9\u30ea</a>\u3092\u5404\u3005\u306e\u30e9\u30a4\u30bb\u30f3\u30b9\u306b\u5247\u3063\u3066\u5229\u7528\u3057\u3066\u3044\u307e\u3059\u3002<br />\
+{0} is free software distributed under the <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source license. \
+ {0} uses <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>.
+help.homepage.title = \u30db\u30fc\u30e0\u30da\u30fc\u30b8
+help.forum.title = \u30d5\u30a9\u30fc\u30e9\u30e0
+help.shop.title = \u30b7\u30e7\u30c3\u30d7
+help.contact.title = \u9023\u7d61\u65b9\u6cd5
+help.contact.text = {0} \u306f Sindre Mehus (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>) \
+ \u306b\u3088\u3063\u3066\u958b\u767a\u30fb\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u3055\u308c\u3066\u3044\u307e\u3059\u3002\
+ \u8cea\u554f\u3084\u30b3\u30e1\u30f3\u30c8\u3001\u610f\u898b\u3001\u6539\u5584\u6848\u304c\u3042\u308c\u3070 \
+ <a href="http://forum.subsonic.org" target="_blank">\u30d5\u30a9\u30fc\u30e9\u30e0</a>\u3078\u3069\u3046\u305e\u3002<br /> \
+ {0} is developed and maintained by Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ If you have any questions, comments or suggestions for improvements, please visit the \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} \u306f\u7121\u6599\u3067\u4f7f\u7528\u3067\u304d\u307e\u3059\u304c\u3001\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u3078\u306e\u652f\u63f4\u3068\u3057\u3066 <b><a href="donate.view?">\u52df\u91d1</a></b> \u3092\u53d7\u3051\u4ed8\u3051\u3066\u3044\u307e\u3059\u3002
+help.log = \u30ed\u30b0
+help.logfile = \u30ed\u30b0\u306e\u6700\u7d42\u66f4\u65b0\u6642\u9593: {0}
+
+# settingsHeader.jsp
+settingsheader.title = \u8a2d\u5b9a
+settingsheader.general = \u4e00\u822c
+settingsheader.advanced = \u9ad8\u5ea6\u306a\u8a2d\u5b9a
+settingsheader.personal = \u500b\u4eba\u8a2d\u5b9a
+settingsheader.musicFolder = \u97f3\u697d\u30d5\u30a9\u30eb\u30c0
+settingsheader.internetRadio = \u30cd\u30c3\u30c8TV/\u30e9\u30b8\u30aa
+settingsheader.podcast = Podcast
+settingsheader.player = \u30d7\u30ec\u30fc\u30e4
+settingsheader.transcoding = \u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0
+settingsheader.user = \u30e6\u30fc\u30b6
+settingsheader.search = \u691c\u7d22
+settingsheader.password = \u30d1\u30b9\u30ef\u30fc\u30c9\u5909\u66f4
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u30d5\u30a9\u30eb\u30c0
+generalsettings.musicmask = \u697d\u66f2\u306e\u62e1\u5f35\u5b50
+generalsettings.coverartmask = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u62e1\u5f35\u5b50
+generalsettings.index = \u7d22\u5f15\u306e\u982d\u6587\u5b57
+generalsettings.ignoredarticles = \u7121\u8996\u3059\u308b\u51a0\u8a5e
+generalsettings.shortcuts = \u30b7\u30e7\u30fc\u30c8\u30ab\u30c3\u30c8
+generalsettings.welcometitle = \u300c\u3088\u3046\u3053\u305d\u300d\u30bf\u30a4\u30c8\u30eb
+generalsettings.welcomesubtitle = \u300c\u3088\u3046\u3053\u305d\u300d\u30b5\u30d6\u30bf\u30a4\u30c8\u30eb
+generalsettings.welcomemessage = \u300c\u3088\u3046\u3053\u305d\u300d\u30e1\u30c3\u30bb\u30fc\u30b8
+generalsettings.language = \u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u8a00\u8a9e
+generalsettings.theme = \u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30c6\u30fc\u30de
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = \u30c0\u30a6\u30f3\u30b5\u30f3\u30d7\u30eb\u7528\u306e\u30b3\u30de\u30f3\u30c9
+advancedsettings.coverartlimit = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u6570\u306e\u4e0a\u9650<br><div class="detail">(0 = \u7121\u5236\u9650)</div>
+advancedsettings.downloadlimit = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u901f\u5ea6\u306e\u4e0a\u9650 (Kbps)<br><div class="detail">(0 = \u7121\u5236\u9650)</div>
+advancedsettings.uploadlimit = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u901f\u5ea6\u306e\u4e0a\u9650 (Kbps)<br><div class="detail">(0 = \u7121\u5236\u9650)</div>
+advancedsettings.streamport = \u975e SSL \u306e\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f<br><div class="detail">(0 = \u7121\u52b9)</div>
+advancedsettings.ldapenabled = LDAP \u8a8d\u8a3c\u3092\u6709\u52b9\u306b\u3059\u308b
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP \u691c\u7d22\u30d5\u30a3\u30eb\u30bf
+advancedsettings.ldapmanagerdn = LDAP manager DN<br><div class="detail">(\u30aa\u30d7\u30b7\u30e7\u30f3)</div>
+advancedsettings.ldapmanagerpassword = \u30d1\u30b9\u30ef\u30fc\u30c9
+advancedsettings.ldapautoshadowing = LDAP \u306e\u30e6\u30fc\u30b6\u3092\u81ea\u52d5\u7684\u306b {0} \u306b\u8ffd\u52a0\u3059\u308b
+
+# personalSettings.jsp
+personalsettings.title = \u30e6\u30fc\u30b6 {0} \u306e\u500b\u4eba\u8a2d\u5b9a
+personalsettings.language = \u8a00\u8a9e
+personalsettings.theme = \u30c6\u30fc\u30de
+personalsettings.display = \u8868\u793a\u9805\u76ee
+personalsettings.browse = \u66f2\u76ee\u4e00\u89a7
+personalsettings.playlist = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8
+personalsettings.tracknumber = \u30c8\u30e9\u30c3\u30af\u756a\u53f7
+personalsettings.artist = \u30a2\u30fc\u30c6\u30a3\u30b9\u30c8
+personalsettings.album = \u30a2\u30eb\u30d0\u30e0
+personalsettings.genre = \u30b8\u30e3\u30f3\u30eb
+personalsettings.year = \u5e74
+personalsettings.bitrate = \u30d3\u30c3\u30c8\u30ec\u30fc\u30c8
+personalsettings.duration = \u518d\u751f\u6642\u9593
+personalsettings.format = \u30d5\u30a1\u30a4\u30eb\u5f62\u5f0f
+personalsettings.filesize = \u30d5\u30a1\u30a4\u30eb\u30b5\u30a4\u30ba
+personalsettings.captioncutoff = \u9577\u3044\u30bf\u30a4\u30c8\u30eb\u3092\u7701\u7565
+personalsettings.partymode = \u30d1\u30fc\u30c6\u30a3\u30fc\u30e2\u30fc\u30c9
+personalsettings.shownowplaying = \u4ed6\u4eba\u306e\u518d\u751f\u4e2d\u306e\u66f2\u3092\u8868\u793a
+personalsettings.nowplayingallowed = \u4ed6\u4eba\u306b\u81ea\u5206\u304c\u518d\u751f\u4e2d\u306e\u66f2\u3092\u898b\u305b\u308b
+personalsettings.finalversionnotification = \u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u901a\u77e5
+personalsettings.betaversionnotification = \u65b0\u3057\u3044\u30d9\u30fc\u30bf\u7248\u3092\u901a\u77e5
+personalsettings.lastfmenabled = \u518d\u751f\u4e2d\u306e\u66f2\u3092 <a href="http://last.fm/" target="_blank">Last.fm</a> \u306b\u767b\u9332
+personalsettings.lastfmusername = Last.fm \u30e6\u30fc\u30b6\u540d
+personalsettings.lastfmpassword = Last.fm \u30d1\u30b9\u30ef\u30fc\u30c9
+personalsettings.avatar.title = \u30a2\u30a4\u30b3\u30f3
+personalsettings.avatar.none = \u30a2\u30a4\u30b3\u30f3\u306a\u3057
+personalsettings.avatar.custom = \u30ab\u30b9\u30bf\u30e0
+personalsettings.avatar.changecustom = \u30ab\u30b9\u30bf\u30e0\u30a2\u30a4\u30b3\u30f3\u306e\u5909\u66f4
+personalsettings.avatar.upload = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9
+personalsettings.avatar.courtesy = <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a> \
+ \u306e\u30a2\u30a4\u30b3\u30f3\u3092\u4f7f\u7528\u3055\u305b\u3066\u9802\u304d\u307e\u3057\u305f<br /> \
+ Icons courtesy of <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = \u30a2\u30a4\u30b3\u30f3\u306e\u5909\u66f4
+avataruploadresult.success = \u30ab\u30b9\u30bf\u30e0\u30a2\u30a4\u30b3\u30f3 "{0}" \u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3057\u307e\u3057\u305f\u3002
+avataruploadresult.failure = \u30a2\u30a4\u30b3\u30f3\u306e\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002 <a href="help.view?">\u30ed\u30b0</a> \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+
+# passwordSettings.jsp
+passwordsettings.title = {0} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \u30d5\u30a9\u30eb\u30c0\u306e\u5834\u6240
+musicfoldersettings.name = \u540d\u524d
+musicfoldersettings.enabled = \u6709\u52b9
+musicfoldersettings.add = \u97f3\u697d\u30d5\u30a9\u30eb\u30c0\u3092\u8ffd\u52a0
+musicfoldersettings.nopath = \u30d5\u30a9\u30eb\u30c0\u306e\u5834\u6240\u304c\u4e0d\u6b63\u3067\u3059\u3002
+
+# transcodingSettings.jsp
+transcodingsettings.name = \u540d\u524d
+transcodingsettings.sourceformat = \u5909\u63db\u5143
+transcodingsettings.targetformat = \u5909\u63db\u5148
+transcodingsettings.step1 = \u7b2c1\u6bb5\u968e
+transcodingsettings.step2 = \u7b2c2\u6bb5\u968e
+transcodingsettings.step3 = \u7b2c3\u6bb5\u968e
+transcodingsettings.defaultactive = \u30c7\u30d5\u30a9\u30eb\u30c8
+transcodingsettings.enabled = \u6709\u52b9
+transcodingsettings.add = \u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u8a2d\u5b9a\u3092\u8ffd\u52a0
+transcodingsettings.noname = \u540d\u524d\u304c\u3042\u308a\u307e\u305b\u3093\u3002
+transcodingsettings.nosourceformat = \u5909\u63db\u5143\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+transcodingsettings.notargetformat = \u5909\u63db\u5148\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+transcodingsettings.nostep1 = 1\u6bb5\u968e\u4ee5\u4e0a\u306e\u5909\u63db\u30b3\u30de\u30f3\u30c9\u3092\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+transcodingsettings.info = <p class="detail">(%s = \u5909\u63db\u5143\u30d5\u30a1\u30a4\u30eb\u540d, %b = \u6700\u5927\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8)</p> \
+ <p>\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u3068\u306f\u3001\u66f2\u306e\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3092\u5909\u63db\u3059\u308b\u3053\u3068\u3067\u3059\u3002 {1} \u306e\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u6a5f\u80fd\u306b\u3088\u308a\u3001\
+ \u672c\u6765\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f\u306b\u5bfe\u5fdc\u3057\u3066\u3044\u306a\u3044\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u306e\u66f2\u3067\u3082\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f\u3067\u304d\u308b\u3088\u3046\u306b\u306a\u308a\u307e\u3059\u3002 \
+ \u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u306f\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u305d\u306e\u90fd\u5ea6\u884c\u308f\u308c\u3001\u30c7\u30a3\u30b9\u30af\u5bb9\u91cf\u306f\u6d88\u8cbb\u3057\u307e\u305b\u3093\u3002<p/> \
+ <p>\u5b9f\u969b\u306e\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u4f5c\u696d\u3092\u884c\u3046\u30b5\u30fc\u30c9\u30d1\u30fc\u30c6\u30a3\u306e\u30d7\u30ed\u30b0\u30e9\u30e0\u306f\u3001 {0} \u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\
+ Windows\u3067\u306f <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>\u3053\u3053</b></a> \
+ \u304b\u3089\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u7528\u306e\u30d7\u30ed\u30b0\u30e9\u30e0\u30d1\u30c3\u30af\u3092\u5165\u624b\u3067\u304d\u307e\u3059\u3002\
+ \u307e\u305f\u3001\u6b21\u306e\u8981\u6c42\u6a5f\u80fd\u3092\u6e80\u305f\u305b\u3070\u72ec\u81ea\u306e\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c0\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002\
+ <ul> \
+ <li>\u30b3\u30de\u30f3\u30c9\u30e9\u30a4\u30f3\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9(CLI)\u3067\u3042\u308b\u3053\u3068</li> \
+ <li>\u6a19\u6e96\u51fa\u529b\u3078\u306e\u5909\u63db\u7d50\u679c\u51fa\u529b\u304c\u53ef\u80fd\u3067\u3042\u308b\u3053\u3068</li> \
+ <li>\u7b2c2, 3\u6bb5\u968e\u3067\u4f7f\u3046\u5834\u5408\u306f\u3001\u6a19\u6e96\u5165\u529b\u304b\u3089\u306e\u5165\u529b\u304c\u53ef\u80fd\u3067\u3042\u308b\u3053\u3068</li> \
+ </ul> \
+ </p> \
+ <p>\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c0\u306f\u30d7\u30ec\u30fc\u30e4\u8a2d\u5b9a\u306e\u30da\u30fc\u30b8\u3067\u6709\u52b9\u306b\u3057\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002"\u30c7\u30d5\u30a9\u30eb\u30c8"\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u308c\u3070\u3001\
+ \u65b0\u3057\u3044\u30d7\u30ec\u30fc\u30e4\u304c\u4f5c\u3089\u308c\u308b\u969b\u3001\u81ea\u52d5\u7684\u306b\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\u304c\u6709\u52b9\u306b\u306a\u308a\u307e\u3059\u3002</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = \u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0 \u914d\u4fe1URL
+internetradiosettings.homepageurl = \u30db\u30fc\u30e0\u30da\u30fc\u30b8
+internetradiosettings.name = \u540d\u524d
+internetradiosettings.enabled = \u6709\u52b9
+internetradiosettings.add = \u30cd\u30c3\u30c8TV/\u30e9\u30b8\u30aa\u3092\u8ffd\u52a0
+internetradiosettings.nourl = URL\u304c\u4e0d\u6b63\u3067\u3059\u3002
+internetradiosettings.noname = \u540d\u524d\u304c\u3042\u308a\u307e\u305b\u3093\u3002
+
+# podcastSettings.jsp
+podcastsettings.update = \u65b0\u7740\u3092\u30c1\u30a7\u30c3\u30af\u3059\u308b
+podcastsettings.keep = \u4fdd\u8b77\u3059\u308b
+podcastsettings.keep.all = \u5168\u3066\u306e\u9805\u76ee
+podcastsettings.keep.one = \u6700\u65b0\u306e 1 \u9805\u76ee
+podcastsettings.keep.many = \u6700\u8fd1\u306e {0} \u9805\u76ee
+podcastsettings.download = \u65b0\u3057\u3044\u9805\u76ee\u304c\u5229\u7528\u53ef\u80fd\u306b\u306a\u3063\u305f\u3068\u304d
+podcastsettings.download.all = \u3059\u3079\u3066\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+podcastsettings.download.one = \u6700\u65b0\u306e 1 \u9805\u76ee\u306e\u307f\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+podcastsettings.download.many = \u6700\u8fd1\u306e {0} \u9805\u76ee\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+podcastsettings.download.none = \u4f55\u3082\u3057\u306a\u3044
+podcastsettings.interval.manually = \u624b\u52d5
+podcastsettings.interval.hourly = \u6bce\u6642\u9593
+podcastsettings.interval.daily = \u6bce\u65e5
+podcastsettings.interval.weekly = \u6bce\u9031
+podcastsettings.folder = Podcast \u306e\u4fdd\u5b58\u5148
+
+# playerSettings.jsp
+playersettings.noplayers = \u30d7\u30ec\u30fc\u30e4\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002
+playersettings.type = \u7a2e\u985e
+playersettings.lastseen = \u6700\u5f8c\u306b\u4f7f\u308f\u308c\u305f\u65e5\u6642
+playersettings.title = \u30d7\u30ec\u30fc\u30e4\u306e\u9078\u629e
+
+playersettings.technology.web.title = Web \u30d7\u30ec\u30fc\u30e4
+playersettings.technology.external.title = \u5916\u90e8\u30d7\u30ec\u30fc\u30e4
+playersettings.technology.external_with_playlist.title = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u3092\u4ecb\u3057\u305f\u5916\u90e8\u30d7\u30ec\u30fc\u30e4
+playersettings.technology.jukebox.title = \u30b8\u30e5\u30fc\u30af\u30dc\u30c3\u30af\u30b9
+playersettings.technology.web.text = \u30d6\u30e9\u30a6\u30b6\u4e0a\u3067Flash \u88fd\u30d7\u30ec\u30fc\u30e4\u3092\u4f7f\u3063\u3066\u76f4\u63a5\u518d\u751f\u3057\u307e\u3059\u3002
+playersettings.technology.external.text = \u4efb\u610f\u306e\u5916\u90e8\u30d7\u30ec\u30fc\u30e4 (Winamp \u3084 Windows Media Player \u306a\u3069) \u3067\u518d\u751f\u3057\u307e\u3059\u3002
+playersettings.technology.external_with_playlist.text = \u540c\u4e0a\u3002\u305f\u3060\u3057\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u3054\u3068\u5916\u90e8\u30d7\u30ec\u30fc\u30e4\u304c\u7ba1\u7406\u3057\u307e\u3059\u3002\
+ \u66f2\u306e\u30b9\u30ad\u30c3\u30d7\u304c\u53ef\u80fd\u306b\u306a\u308a\u307e\u3059\u3002
+playersettings.technology.jukebox.text = Subsonic \u30b5\u30fc\u30d0\u306e\u30aa\u30fc\u30c7\u30a3\u30aa\u30c7\u30d0\u30a4\u30b9\u3092\u7528\u3044\u3066\u76f4\u63a5\u518d\u751f\u3057\u307e\u3059\u3002\u76f8\u5fdc\u306e\u6a29\u9650\u304c\u5fc5\u8981\u3067\u3059\u3002
+playersettings.name = \u30d7\u30ec\u30fc\u30e4\u540d
+playersettings.coverartsize = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u30b5\u30a4\u30ba
+playersettings.maxbitrate = \u6700\u5927\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8
+playersettings.coverart.off = \u8868\u793a\u3057\u306a\u3044
+playersettings.coverart.small = \u5c0f\u3055\u3044
+playersettings.coverart.medium = \u666e\u901a
+playersettings.coverart.large = \u5927\u304d\u3044
+playersettings.nolame = <em>\u6ce8\u610f:</em> LAME \u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u306a\u3044\u3088\u3046\u3067\u3059\u3002<br>\u8a73\u7d30\u306f\u30d8\u30eb\u30d7\u30dc\u30bf\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+playersettings.autocontrol = \u5916\u90e8\u30d7\u30ec\u30fc\u30e4\u3067\u81ea\u52d5\u518d\u751f\u3059\u308b
+playersettings.dynamicip = \u30d7\u30ec\u30fc\u30e4\u306eIP\u30a2\u30c9\u30ec\u30b9\u304c\u56fa\u5b9a\u3067\u306f\u306a\u3044
+playersettings.transcodings = \u6709\u52b9\u306a\u30c8\u30e9\u30f3\u30b9\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0
+playersettings.ok = \u4fdd\u5b58
+playersettings.forget = \u30d7\u30ec\u30fc\u30e4\u306e\u524a\u9664
+playersettings.clone = \u30d7\u30ec\u30fc\u30e4\u306e\u8907\u88fd
+
+# userSettings.jsp
+usersettings.title = \u30e6\u30fc\u30b6\u3092\u9078\u629e
+usersettings.newuser = \u65b0\u3057\u3044\u30e6\u30fc\u30b6
+usersettings.admin = \u7ba1\u7406\u8005
+usersettings.stream = \u30d5\u30a1\u30a4\u30eb\u3092\u518d\u751f\u3067\u304d\u308b
+usersettings.download = \u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3067\u304d\u308b
+usersettings.upload = \u30d5\u30a1\u30a4\u30eb\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3067\u304d\u308b
+usersettings.playlist= \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u4f5c\u6210\u3068\u524a\u9664\u304c\u3067\u304d\u308b
+usersettings.coverart = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u3068\u30bf\u30b0\u3092\u5909\u66f4\u3067\u304d\u308b
+usersettings.comment= \u30b3\u30e1\u30f3\u30c8\u3092\u7de8\u96c6\u3057\u305f\u308a\u3001\u30ec\u30fc\u30c8\u3092\u5909\u66f4\u3067\u304d\u308b
+usersettings.podcast= Podcasst \u306e\u7ba1\u7406\u304c\u3067\u304d\u308b
+usersettings.username = \u30e6\u30fc\u30b6\u540d
+usersettings.changepassword = \u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u5909\u66f4
+usersettings.password = \u30d1\u30b9\u30ef\u30fc\u30c9
+usersettings.newpassword = \u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9
+usersettings.confirmpassword = \u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u78ba\u8a8d
+usersettings.delete = \u30e6\u30fc\u30b6\u3092\u524a\u9664
+usersettings.ldap = LDAP \u3092\u7528\u3044\u305f\u8a8d\u8a3c
+usersettings.nousername = \u30e6\u30fc\u30b6\u540d\u304c\u3042\u308a\u307e\u305b\u3093\u3002
+usersettings.useralreadyexists = \u305d\u306e\u30e6\u30fc\u30b6\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002
+usersettings.nopassword = \u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u3042\u308a\u307e\u305b\u3093\u3002
+usersettings.wrongpassword = \u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002
+usersettings.ldapdisabled = LDAP \u8a8d\u8a3c\u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002\u9ad8\u5ea6\u306a\u8a2d\u5b9a\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+usersettings.passwordnotsupportedforldap = LDAP \u8a8d\u8a3c\u306e\u30e6\u30fc\u30b6\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002
+usersettings.ok = \u30e6\u30fc\u30b6 {0} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3057\u307e\u3057\u305f\u3002
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = \u3057\u306a\u3044
+musicfoldersettings.interval.one = \u6bce\u65e5
+musicfoldersettings.interval.many = {0} \u65e5\u6bce
+musicfoldersettings.hour = {0}:00
+
+# main.jsp
+main.up = \u4e0a\u3078
+main.playall = \u5168\u3066\u518d\u751f
+main.playrandom = \u30e9\u30f3\u30c0\u30e0\u518d\u751f
+main.addall = \u5168\u3066\u8ffd\u52a0
+main.tags = \u30bf\u30b0\u7de8\u96c6
+main.playcount = {0} \u56de\u518d\u751f\u6e08\u307f
+main.lastplayed = \u6700\u7d42\u518d\u751f\u65e5 {0}
+main.comment = \u30b3\u30e1\u30f3\u30c8
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__\u6587\u5b57\u5217__</td><td>\u592a\u5b57 </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>\u6539\u884c</td></tr>\
+ <tr><td style="padding-right:1em">~~\u6587\u5b57\u5217~~</td><td>\u659c\u4f53 </td><td style="padding-left:3em;padding-right:1em">(\u7a7a\u884c) </td><td>\u6539\u6bb5\u843d</td></tr>\
+ <tr><td style="padding-right:1em">* \u6587\u5b57\u5217 </td><td>\u7b87\u6761\u66f8\u304d </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>\u30ea\u30f3\u30af</td></tr>\
+ <tr><td style="padding-right:1em">1. \u6587\u5b57\u5217 </td><td>\u6570\u5b57\u3064\u304d\u306e\u7b87\u6761\u66f8\u304d</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>\u540d\u524d\u4ed8\u304d\u306e\u30ea\u30f3\u30af</td></tr>\
+ </table>
+main.donate = {1} \u306b <a href="{0}" style="text-decoration:underline">\u5bc4\u4ed8</a> \u3057\u3066\u304f\u3060\u3055\u3044\uff01<br>(\u3053\u306e\u5e83\u544a\u304c\u6d88\u3048\u307e\u3059)
+main.nowplaying = \u518d\u751f\u4e2d
+main.lyrics = \u6b4c\u8a5e
+main.minutesago = \u5206\u524d
+
+# rating.jsp
+rating.rating = \u30ec\u30fc\u30c8
+rating.clearrating = \u30ec\u30fc\u30c8\u306e\u30af\u30ea\u30a2
+
+# coverArt.jsp
+coverart.change = \u5909\u66f4\u3059\u308b
+coverart.zoom = \u62e1\u5927
+
+# allmusic.jsp
+allmusic.text = \u30a2\u30eb\u30d0\u30e0 <em>{0}</em> \u3092 allmusic.com \u3067\u691c\u7d22\u4e2d - \u304a\u5f85\u3061\u304f\u3060\u3055\u3044...
+
+# changeCoverArt.jsp
+changecoverart.title = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u5909\u66f4
+changecoverart.address = \u753b\u50cf\u306eURL\u3092\u5165\u529b
+changecoverart.artist = \u30a2\u30fc\u30c6\u30a3\u30b9\u30c8
+changecoverart.album = \u30a2\u30eb\u30d0\u30e0
+changecoverart.searchdiscogs = Discogs \u3067\u691c\u7d22
+changecoverart.wait = \u691c\u7d22\u4e2d...
+changecoverart.success = \u753b\u50cf\u3092\u53d6\u5f97\u3057\u307e\u3057\u305f\u3002
+changecoverart.error = \u753b\u50cf\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002
+changecoverart.noimagesfound = \u753b\u50cf\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u5909\u66f4\u306b\u5931\u6557\u3057\u307e\u3057\u305f:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = \u30bf\u30b0\u306e\u5909\u66f4
+edittags.file = \u30d5\u30a1\u30a4\u30eb
+edittags.track = \u30c8\u30e9\u30c3\u30af\u756a\u53f7
+edittags.songtitle = \u66f2\u540d
+edittags.artist = \u30a2\u30fc\u30c6\u30a3\u30b9\u30c8
+edittags.album = \u30a2\u30eb\u30d0\u30e0
+edittags.year = \u5e74
+edittags.genre = \u30b8\u30e3\u30f3\u30eb
+edittags.status = \u72b6\u614b
+edittags.suggest = \u63a8\u6e2c
+edittags.reset = \u5143\u306b\u623b\u3059
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = \u4e00\u62ec\u8a2d\u5b9a
+edittags.working = \u51e6\u7406\u4e2d
+edittags.updated = \u66f4\u65b0
+edittags.skipped = \u30b9\u30ad\u30c3\u30d7
+edittags.error = \u30a8\u30e9\u30fc
+
+# donate.jsp
+donate.title = \u5bc4\u4ed8\u306e\u304a\u9858\u3044
+donate.invalidlicense = \u4e0d\u6b63\u306a\u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc\u3067\u3059\u3002
+donate.amount = {0} \u5bc4\u4ed8\u3059\u308b
+donate.textbefore = <p>{0} \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u3078\u306e\u5bc4\u4ed8\u3092\u3054\u691c\u8a0e\u9802\u304d\u3042\u308a\u304c\u3068\u3046\u3054\u3056\u3044\u307e\u3059\uff01 \
+ \u5bc4\u4ed8\u3092\u3044\u305f\u3060\u304f\u3068\u3001\u5e83\u544a\u3068\u5bc4\u4ed8\u306e\u304a\u9858\u3044\u3092\u975e\u8868\u793a\u306b\u3059\u308b\u305f\u3081\u306e\u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc\u304c\u767a\u884c\u3055\u308c\u307e\u3059\u3002\
+ \u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc\u306f\u3001{0} \u306e\u4eca\u5f8c\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u6e21\u3063\u3066\u6c38\u7d9a\u7684\u306b\u6709\u52b9\u3067\u3059\u3002</p> \
+ <p>\u5bc4\u4ed8\u306f\u4e00\u53e3 <b>&euro;20</b> \u3092\u304a\u9858\u3044\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3088\u308a\u591a\u3044\u984d\u3084\u5c11\u306a\u3044\u984d\u3067\u3082\u53ef\u80fd\u3067\u3059\u3002 \
+ \u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc\u306f\u6307\u5b9a\u3055\u308c\u305f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u5b9b\u306b\u9001\u3089\u308c\u308b\u305f\u3081\u3001\
+ PayPal\u3067\u306e\u5bc4\u4ed8\u3067\u767b\u9332\u3057\u305f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3092\u6b63\u3057\u304f\u8a18\u5165\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002</p>
+donate.textafter = <p>\u30af\u30ec\u30b8\u30c3\u30c8\u30ab\u30fc\u30c9\u3092\u3082\u3063\u3066\u3044\u3066PayPal\u304b\u3089\u5bc4\u4ed8\u3092\u306a\u3055\u308b\u5834\u5408\u306b\u306f\u3001\u4e0a\u306e\u30dc\u30bf\u30f3\u3092\u7d4c\u7531\u3057\u3066PayPal\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \
+ \u5bc4\u4ed8\u306e\u51e6\u7406\u5f8c\u3001\u30e1\u30fc\u30eb\u306b\u3066\u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc\u304c\u767a\u884c\u3055\u308c\u307e\u3059\u3002</p> \
+ <p>\u3054\u8cea\u554f\u304c\u3042\u308c\u3070\u3001\u30e1\u30fc\u30eb\u3092 \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a> \
+ \u307e\u3067\u304a\u9858\u3044\u3057\u307e\u3059 (\u8a33\u6ce8: \u539f\u6587\u306f\u82f1\u8a9e\u3067\u3059\u3002\u65e5\u672c\u8a9e\u3067\u30e1\u30fc\u30eb\u3092\u9802\u3044\u3066\u3082\u5bfe\u5fdc\u3067\u304d\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059)\u3002</p>
+donate.licensed = \u3053\u306e\u30bd\u30d5\u30c8\u30a6\u30a7\u30a2 {2} \u306f {0} \u306b {1} \u306b\u30e9\u30a4\u30bb\u30f3\u30b9\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u3054\u652f\u63f4\u611f\u8b1d\u3057\u307e\u3059\uff01
+donate.register = \u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc\u304c\u5c4a\u3044\u3066\u304b\u3089\u3001\u4ee5\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+donate.register.email = \u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9
+donate.register.license = \u30e9\u30a4\u30bb\u30f3\u30b9\u30ad\u30fc
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast\u306e\u53d7\u4fe1
+podcastreceiver.expandall = \u9805\u76ee\u306e\u8868\u793a
+podcastreceiver.collapseall = \u9805\u76ee\u306e\u975e\u8868\u793a
+podcastreceiver.status.new = \u65b0\u7740
+podcastreceiver.status.downloading = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+podcastreceiver.status.completed = \u5b8c\u4e86
+podcastreceiver.status.error = \u30a8\u30e9\u30fc
+podcastreceiver.status.deleted = \u524a\u9664\u6e08\u307f
+podcastreceiver.status.skipped = \u30b9\u30ad\u30c3\u30d7
+podcastreceiver.downloadselected= \u6307\u5b9a\u9805\u76ee\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+podcastreceiver.deleteselected= \u6307\u5b9a\u9805\u76ee\u3092\u524a\u9664
+podcastreceiver.confirmdelete= \u672c\u5f53\u306b Podcast \u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f
+podcastreceiver.check = \u65b0\u7740\u3092\u78ba\u8a8d
+podcastreceiver.refresh = \u66f4\u65b0
+podcastreceiver.settings = Podcast \u306e\u8a2d\u5b9a
+podcastreceiver.subscribe = Podcast \u306e\u767b\u9332
+
+# lyrics.jsp
+lyrics.title = \u6b4c\u8a5e
+lyrics.artist = \u30a2\u30fc\u30c6\u30a3\u30b9\u30c8
+lyrics.song = \u66f2
+lyrics.search = \u691c\u7d22
+lyrics.wait = \u6b4c\u8a5e\u3092\u691c\u7d22\u3057\u3066\u3044\u307e\u3059\u3002\u304a\u5f85\u3061\u304f\u3060\u3055\u3044...
+lyrics.courtesy = (\u6b4c\u8a5e\u63d0\u4f9b: <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = \u6b4c\u8a5e\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+
+# helpPopup.jsp
+helppopup.title = {0} \u30d8\u30eb\u30d7
+helppopup.cover.title = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u30b5\u30a4\u30ba
+helppopup.cover.text = <p>\u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u30b5\u30a4\u30ba\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u975e\u8868\u793a\u306b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002</p>
+helppopup.transcode.title = \u6700\u5927\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8
+helppopup.transcode.text = <p>\u5e2f\u57df\u304c\u72ed\u3044\u5834\u5408\u3001\u6700\u5927\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8\u3092\u8a2d\u5b9a\u3059\u308b\u3068\u826f\u3044\u3067\u3057\u3087\u3046\u3002\
+ \u4f8b\u3048\u3070\u3001\u3082\u3068\u306e mp3 \u30d5\u30a1\u30a4\u30eb\u304c 256 Kbps (K\u30d3\u30c3\u30c8\u6bce\u79d2) \u3067\u30a8\u30f3\u30b3\u30fc\u30c9\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\
+ \u6700\u5927\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8\u3092 128 \u306b\u3059\u308b\u3068\u3001 {0} \u306f\u81ea\u52d5\u7684\u306b\u66f2\u3092 256 \u304b\u3089 128 Kbps \u306b\u518d\u30b5\u30f3\u30d7\u30ea\u30f3\u30b0\u3057\u307e\u3059\u3002</p> \
+ <p>\u3053\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f LAME \u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\
+ LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \u306f\u30aa\u30fc\u30d7\u30f3\u30bd\u30fc\u30b9\u306e mp3 \u30a8\u30f3\u30b3\u30fc\u30c0\u3067\u3001<a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">\u3053\u3053</a> \
+ \u304b\u3089\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\
+ SUBSONIC_HOME/transcode \u304b\u3001\u74b0\u5883\u5909\u6570 PATH \u304c\u901a\u3063\u305f\u5834\u6240\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002</p>
+helppopup.playlistfolder.title = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u306e\u30d5\u30a9\u30eb\u30c0
+helppopup.playlistfolder.text = <p>\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u304c\u4fdd\u5b58\u3055\u308c\u308b\u30d5\u30a9\u30eb\u30c0\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.musicmask.title = \u697d\u66f2\u306e\u62e1\u5f35\u5b50
+helppopup.musicmask.text = <p>\u97f3\u697d\u30d5\u30a9\u30eb\u30c0\u4e2d\u306e\u3001\u697d\u66f2\u3068\u3057\u3066\u767b\u9332\u3055\u308c\u308b\u3079\u304d\u30d5\u30a1\u30a4\u30eb\u306e\u7a2e\u985e\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.coverartmask.title = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u62e1\u5f35\u5b50
+helppopup.coverartmask.text = <p>\u97f3\u697d\u30d5\u30a9\u30eb\u30c0\u4e2d\u306e\u3001\u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u3068\u3057\u3066\u767b\u9332\u3055\u308c\u308b\u3079\u304d\u30d5\u30a1\u30a4\u30eb\u306e\u7a2e\u985e\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.downsamplecommand.title = \u30c0\u30a6\u30f3\u30b5\u30f3\u30d7\u30eb\u7528\u306e\u30b3\u30de\u30f3\u30c9
+helppopup.downsamplecommand.text = <p>\u4f4e\u3044\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8\u306b\u30c0\u30a6\u30f3\u30b5\u30f3\u30d7\u30ea\u30f3\u30b0\u3059\u308b\u3068\u304d\u306b\u7528\u3044\u308b\u30b3\u30de\u30f3\u30c9\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002</p>\
+ <p>(%s = \u5143\u306e\u30d5\u30a1\u30a4\u30eb, %b = \u6700\u5927\u30d3\u30c3\u30c8\u30ec\u30fc\u30c8)</p>
+helppopup.index.title = \u7d22\u5f15\u306e\u63a5\u982d\u8f9e
+helppopup.index.text = <p>\u30c8\u30c3\u30d7\u30da\u30fc\u30b8\u306e\u7d22\u5f15\u306e\u8868\u793a\u65b9\u6cd5\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\
+ \u97f3\u697d\u30d5\u30a9\u30eb\u30c0\u76f4\u4e0b\u306e\u30d5\u30a1\u30a4\u30eb\u3068\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u304c\u7d22\u5f15\u304b\u3089\u76f4\u63a5\u30a2\u30af\u30bb\u30b9\u3067\u304d\u307e\u3059\u3002</p>\
+ <p>\u30b9\u30da\u30fc\u30b9\u3067\u533a\u5207\u3063\u3066\u8907\u6570\u306e\u9805\u76ee\u3092\u4e26\u3079\u307e\u3059\u3002\u901a\u5e38\u7d22\u5f15\u306e\u9805\u76ee\u306f1\u6587\u5b57\u3067\u3059\u304c\u3001\u8907\u6570\u6587\u5b57\u306b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002\
+ \u4f8b\u3048\u3070\u3001<em>The</em> \u3068\u3044\u3046\u9805\u76ee\u306f "The" \u3067\u59cb\u307e\u308b\u5168\u3066\u306e\u30d5\u30a1\u30a4\u30eb\u3068\u30d5\u30a9\u30eb\u30c0\u3092\u307e\u3068\u3081\u307e\u3059\u3002</p> \
+ <p>\u307e\u305f\u3001\u30b0\u30eb\u30fc\u30d7\u5316\u3057\u305f\u6307\u5b9a\u3082\u53ef\u80fd\u3067\u3059\u3002\
+ <em>A-E(ABCDE)</em> \u3068\u6307\u5b9a\u3059\u308c\u3070\u3001\u7d22\u5f15\u306b\u306f <em>A-E</em> \u3068\u8868\u793a\u3055\u308c\u3001\
+ A, B, C, D, E \u306e\u3069\u308c\u304b\u3067\u59cb\u307e\u308b\u30d5\u30a1\u30a4\u30eb\u3068\u30d5\u30a9\u30eb\u30c0\u3092\u307e\u3068\u3081\u307e\u3059\u3002X\u3084Y, Z\u3068\u3044\u3063\u305f\u3042\u307e\u308a\u983b\u7e41\u306b\u73fe\u308c\u306a\u3044\u6587\u5b57\u3084\u3001\u30a2\u30af\u30bb\u30f3\u30c8\u8a18\u53f7 (\u4f8b\u3048\u3070 A, \u00c0 \u3068 \u00c1) \u3092\u307e\u3068\u3081\u308b\u306e\u306b\u4fbf\u5229\u3067\u3059\u3002 </p> \
+ <p>\u7d22\u5f15\u306e\u6587\u5b57\u304b\u3089\u6f0f\u308c\u305f\u30d5\u30a1\u30a4\u30eb\u3068\u30d5\u30a9\u30eb\u30c0\u306f"#"\u3068\u3044\u3046\u9805\u76ee\u306b\u307e\u3068\u3081\u3089\u308c\u307e\u3059\u3002</p>
+helppopup.ignoredarticles.title = \u7121\u8996\u3059\u308b\u51a0\u8a5e
+helppopup.ignoredarticles.text = <p>\u7d22\u5f15\u3092\u4f5c\u308b\u969b\u306b\u7121\u8996\u3059\u308b\u51a0\u8a5e ("The" \u306a\u3069) \u306e\u30ea\u30b9\u30c8\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.shortcuts.title = \u30b7\u30e7\u30fc\u30c8\u30ab\u30c3\u30c8
+helppopup.shortcuts.text = <p>\u30b7\u30e7\u30fc\u30c8\u30ab\u30c3\u30c8\u3092\u4f5c\u6210\u3059\u308b\u6700\u4e0a\u4f4d\u306e\u30d5\u30a9\u30eb\u30c0\u540d\u3092\u30b9\u30da\u30fc\u30b9\u533a\u5207\u308a\u3067\u6307\u5b9a\u3057\u307e\u3059\u3002\
+ \u30b9\u30da\u30fc\u30b9\u3092\u542b\u3080\u540d\u524d\u306f\u3001\u6b21\u306e\u3088\u3046\u306b\u30c0\u30d6\u30eb\u30af\u30aa\u30fc\u30c6\u30fc\u30b7\u30e7\u30f3\u3067\u304f\u304f\u308a\u307e\u3059:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = \u8a00\u8a9e
+helppopup.language.text = <p>\u7528\u3044\u308b\u8a00\u8a9e\u3092\u6307\u5b9a\u3057\u307e\u3059</p>
+helppopup.visibility.title = \u8868\u793a\u9805\u76ee
+helppopup.visibility.text = <p>\u697d\u66f2\u306e\u8a73\u7d30\u60c5\u5831\u306e\u8868\u793a\u30fb\u975e\u8868\u793a\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u307e\u305f\u3001\u30bf\u30a4\u30c8\u30eb\u306e\u9577\u3055\u304c\u4f55\u6587\u5b57\u4ee5\u4e0a\u306b\u306a\u308b\u3068\u7701\u7565\u3059\u308b\u304b\u3092\u3057\u3066\u3044\u3057\u307e\u3059\u3002\u3053\u306e\u6570\u5024\u306f\u66f2\u540d\u3001\u30a2\u30fc\u30c6\u30a3\u30b9\u30c8\u540d\u3001\u30a2\u30eb\u30d0\u30e0\u540d\u306b\u9069\u7528\u3055\u308c\u307e\u3059\u3002</p>
+helppopup.partymode.title = \u30d1\u30fc\u30c6\u30a3\u30fc\u30e2\u30fc\u30c9
+helppopup.partymode.text = <p>\u30d1\u30fc\u30c6\u30a3\u30fc\u30e2\u30fc\u30c9\u3092\u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9\u304c\u4e0d\u6163\u308c\u306a\u30e6\u30fc\u30b6\u3067\u3082\u7c21\u5358\u306b\u6271\u3048\u308b\u3088\u3046\u3001\u5358\u7d14\u306b\u306a\u308a\u307e\u3059\u3002\
+ \u7279\u306b\u3001\u4e0d\u610f\u306b\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u3092\u6ec5\u8336\u82e6\u8336\u306b\u3057\u3066\u3057\u307e\u3044\u306b\u304f\u304f\u306a\u308a\u307e\u3059\u3002</p>
+helppopup.theme.title = \u30c6\u30fc\u30de
+helppopup.theme.text = <p>\u30c6\u30fc\u30de\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u30c6\u30fc\u30de\u3068\u306f\u3001{0} \u306e\u898b\u305f\u76ee\u3092\u8272\u30fb\u30d5\u30a9\u30f3\u30c8\u30fb\u753b\u50cf\u306a\u3069\u3092\u307e\u3068\u3081\u3066\u8868\u73fe\u3057\u305f\u3082\u306e\u3067\u3059\u3002</p>
+helppopup.welcomemessage.title = \u300c\u3088\u3046\u3053\u305d\u300d\u30e1\u30c3\u30bb\u30fc\u30b8
+helppopup.welcomemessage.text = <p>\u3053\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u30c8\u30c3\u30d7\u30da\u30fc\u30b8\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002</p>
+helppopup.coverartlimit.title = \u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u6570\u306e\u4e0a\u9650
+helppopup.coverartlimit.text = <p>\u4e00\u753b\u9762\u306b\u8868\u793a\u53ef\u80fd\u306a\u30b8\u30e3\u30b1\u30c3\u30c8\u753b\u50cf\u306e\u679a\u6570\u306e\u4e0a\u9650\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.downloadlimit.title = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u901f\u5ea6\u306e\u4e0a\u9650
+helppopup.downloadlimit.text = <p>\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b\u4f7f\u7528\u3067\u304d\u308b\u5e2f\u57df\u306e\u4e0a\u9650\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.uploadlimit.title = \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u901f\u5ea6\u306e\u4e0a\u9650
+helppopup.uploadlimit.text = <p>\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u4f7f\u7528\u3067\u304d\u308b\u5e2f\u57df\u306e\u4e0a\u9650\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002</p>
+helppopup.streamport.title = \u975eSSL\u306e\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f
+helppopup.streamport.text = <p>\u3053\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u306f {0} \u3092 SSL (HTTPS) \u3067\u4f7f\u7528\u3057\u3066\u3044\u308b\u3068\u304d\u306e\u307f\u52b9\u679c\u3092\u767a\u63ee\u3057\u307e\u3059\u3002 \
+ Winamp \u306a\u3069\u306e\u30d7\u30ec\u30fc\u30e4\u306f SSL \u3054\u3057\u306e\u518d\u751f\u306b\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002\
+ HTTPS \u3092\u4f7f\u308f\u305a\u306b HTTP \u3067\u30b9\u30c8\u30ea\u30fc\u30df\u30f3\u30b0\u518d\u751f\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u3053\u306e\u6b04\u306b HTTP \u901a\u4fe1\u3067\u7528\u3044\u308b\u30dd\u30fc\u30c8\u756a\u53f7 \
+ (\u901a\u5e38\u306f 80 \u304b 4040) \u3092\u6307\u5b9a\u3057\u307e\u3059\u3002\u305f\u3060\u3057\u3001HTTP \u3067\u306f\u901a\u4fe1\u306e\u5185\u5bb9\u304c\u6697\u53f7\u5316\u3055\u308c\u306a\u3044\u3053\u3068\u306b\u7559\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002</p>
+helppopup.ldap.title = LDAP \u8a8d\u8a3c
+helppopup.ldap.text = <p>\u5916\u90e8\u306e LDAP \u30b5\u30fc\u30d0 (Windows Active Directory \u3092\u542b\u3080) \u3092\u7528\u3044\u3066\u30e6\u30fc\u30b6\u306e\u8a8d\u8a3c\u3092\u884c\u3044\u307e\u3059\u3002 \
+ LDAP \u8a8d\u8a3c\u304c\u6709\u52b9\u306a\u30e6\u30fc\u30b6\u304c {0} \u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u3068\u3001\u30e6\u30fc\u30b6\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u306f {0} \u3067\u306f\u306a\u304f LDAP \u30b5\u30fc\u30d0\u306b\u3088\u3063\u3066\u30c1\u30a7\u30c3\u30af\u3055\u308c\u307e\u3059\u3002</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>LDAP \u30b5\u30fc\u30d0\u306e URL\u3067\u3059\u3002\u30d7\u30ed\u30c8\u30b3\u30eb\u306f <em>ldap://</em> \u3082\u3057\u304f\u306f <em>ldaps://</em> \
+ (LDAP over SSL \u306e\u5834\u5408) \u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\
+ \u8a73\u3057\u304f\u306f <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">\u3053\u3053</a> \
+ \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002</p>
+helppopup.ldapsearchfilter.title = LDAP \u691c\u7d22\u30d5\u30a3\u30eb\u30bf
+helppopup.ldapsearchfilter.text = <p>\u3053\u306e\u30d5\u30a3\u30eb\u30bf\u306f\u30e6\u30fc\u30b6\u306e\u691c\u7d22\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u3053\u308c\u306f LDAP \u691c\u7d22\u30d5\u30a3\u30eb\u30bf\
+ (<a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a> \u3067\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u3059) \u3067\u3059\u3002 \
+ "'{0'}" \u3068\u3044\u3046\u30d1\u30bf\u30fc\u30f3\u306f\u30e6\u30fc\u30b6\u540d\u3067\u7f6e\u63db\u3055\u308c\u307e\u3059\u3002\u4f8b\u3048\u3070\u3001\
+ <ul>\
+ <li>(uid='{0'}) - \u3053\u308c\u306fuid\u5c5e\u6027\u306b\u30de\u30c3\u30c1\u3059\u308b\u30e6\u30fc\u30b6\u540d\u3092\u691c\u7d22\u3057\u307e\u3059\u3002</li> \
+ <li>(sAMAccountName='{0'}) - \u3053\u308c\u306f Microsoft Active Directory \u306e\u8a8d\u8a3c\u3092\u7528\u3044\u308b\u969b\u306b\u4f7f\u308f\u308c\u307e\u3059\u3002</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>LDAP \u30b5\u30fc\u30d0\u304c\u533f\u540d\u30d0\u30a4\u30f3\u30c9\u3092\u8a31\u53ef\u3057\u3066\u306a\u3044\u5834\u5408\u3001LDAP \u30e6\u30fc\u30b6\u3092\u30d0\u30a4\u30f3\u30c9\u3059\u308b\u306b\u306f \
+ DN (<em>Distinguished Name</em>) \u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u767b\u9332\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002</p>
+helppopup.ldapautoshadowing.title = LDAP \u306e\u30e6\u30fc\u30b6\u3092\u81ea\u52d5\u7684\u306b {0} \u306b\u8ffd\u52a0\u3059\u308b
+helppopup.ldapautoshadowing.text = <p>LDAP \u8a8d\u8a3c\u306e\u30e6\u30fc\u30b6\u3092\u4e8b\u524d\u306b {0} \u306b\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u306a\u304f\u306a\u308a\u307e\u3059\u3002</p> \
+ <p>LDAP \u306b\u304a\u3044\u3066\u6709\u52b9\u306a\u30e6\u30fc\u30b6\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u6301\u3064\u5168\u3066\u306e\u30e6\u30fc\u30b6\u304c {0} \
+ \u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u308b\u3088\u3046\u306b\u306a\u3063\u3066\u3057\u307e\u3046\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002</p>
+helppopup.playername.title = \u30d7\u30ec\u30fc\u30e4\u540d
+helppopup.playername.text = <p>\u300c\u66f8\u658e\u300d\u3068\u304b\u300c\u5c45\u9593\u300d\u306a\u3069\u306e\u30d7\u30ec\u30fc\u30e4\u3092\u8b58\u5225\u3059\u308b\u9069\u5f53\u306a\u540d\u524d\u3092\u3064\u3051\u3089\u308c\u307e\u3059\u3002</p>
+helppopup.autocontrol.title = \u5916\u90e8\u30d7\u30ec\u30fc\u30e4\u3067\u81ea\u52d5\u518d\u751f\u3059\u308b
+helppopup.autocontrol.text = <p>\u3053\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3059\u308b\u3068\u3001\u30d7\u30ec\u30a4\u30ea\u30b9\u30c8\u4e2d\u3067"\u518d\u751f"\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u81ea\u52d5\u3067 {0} \u304c\u30d7\u30ec\u30fc\u30e4\u3092\u958b\u304d\u3001\u66f2\u306e\u518d\u751f\u3092\u306f\u3058\u3081\u307e\u3059\u3002\
+ \u9078\u629e\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u624b\u52d5\u3067\u30d7\u30ec\u30fc\u30e4\u3092\u63a5\u7d9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002</p>
+helppopup.dynamicip.title = \u30d7\u30ec\u30fc\u30e4\u306eIP\u30a2\u30c9\u30ec\u30b9\u304c\u56fa\u5b9a\u3067\u306f\u306a\u3044
+helppopup.dynamicip.text = <p>\u30d7\u30ec\u30fc\u30e4\u306eIP\u30a2\u30c9\u30ec\u30b9\u304c\u56fa\u5b9a\u306e\u5834\u5408\u306f\u3053\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u5207\u308a\u307e\u3059\u3002</p>
+
+# wap/index.jsp
+wap.index.missing = \u66f2\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093
+wap.index.playlist = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8
+wap.index.search = \u691c\u7d22
+wap.index.settings = \u8a2d\u5b9a
+
+# wap/browse.jsp
+wap.browse.playone = \u4e00\u66f2\u518d\u751f
+wap.browse.playall = \u5168\u66f2\u518d\u751f
+wap.browse.addone = \u8ffd\u52a0
+wap.browse.addall = \u5168\u3066\u8ffd\u52a0
+wap.browse.downloadone = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+wap.browse.downloadall = \u5168\u3066\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+
+# wap/playlist.jsp
+wap.playlist.title = \u30d7\u30ec\u30a4\u30ea\u30b9\u30c8
+wap.playlist.noplayer = \u30d7\u30ec\u30fc\u30e4\u304c\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093
+wap.playlist.clear = \u7a7a\u306b\u3059\u308b
+wap.playlist.load = \u8aad\u8fbc
+wap.playlist.random = \u30e9\u30f3\u30c0\u30e0\u518d\u751f
+wap.playlist.play = \u518d\u751f
+
+# wap/search.jsp
+wap.search.title = \u691c\u7d22
+
+# wap/searchResult.jsp
+wap.searchresult.index = \u691c\u7d22\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306e\u4f5c\u6210\u4e2d\u3067\u3059\u3002\u6642\u9593\u3092\u304a\u3044\u3066\u518d\u8a66\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+
+# wap/settings.jsp
+wap.settings.selectplayer = \u30d7\u30ec\u30fc\u30e4\u306e\u9078\u629e
+wap.settings.allplayers = \u5168\u3066
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ko.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ko.properties
new file mode 100644
index 00000000..06625912
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ko.properties
@@ -0,0 +1,679 @@
+#
+# Korean localization.
+# Author: Choi Jong-seok. (rhetor.choi@gmail.com)
+#
+
+common.home = \ud648
+common.back = \ub4a4\ub85c\uac00\uae30
+common.help = \ub3c4\uc6c0\ub9d0
+common.play = \uc7ac\uc0dd
+common.add = \ucd94\uac00
+common.download = \ub2e4\uc6b4\ub85c\ub4dc
+common.close = \ub2eb\uae30
+common.refresh = \uc0c8\ub85c\uace0\uce68
+common.next = \ub2e4\uc74c
+common.previous = \uc774\uc804
+common.more = \ub354\ubcf4\uae30
+common.ok = \ud655\uc778
+common.cancel = \ucde8\uc18c
+common.save = \uc800\uc7a5
+common.create = \ub9cc\ub4e4\uae30
+common.delete = \uc0ad\uc81c
+common.unknown = (\uc54c\uc218\uc5c6\uc74c)
+common.default = (\uae30\ubcf8\uac12)
+
+# login.jsp
+login.username = \uc0ac\uc6a9\uc790\uc774\ub984
+login.password = \uc554\ud638
+login.login = \ub85c\uadf8\uc778
+login.remember = \uae30\uc5b5\ud558\uae30
+login.logout = \ub85c\uadf8\uc544\uc6c3 \ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+login.error = \uc0ac\uc6a9\uc790\uc774\ub984\uc774\ub098 \uc554\ud638\uac00 \ud2c0\ub9bd\ub2c8\ub2e4.
+login.insecure = {0}\uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \uc554\ud638\ub97c "admin"\uc73c\ub85c \ub85c\uadf8\uc778 \ud558\uc2dc\uac70\ub098, <br><a href="login.view?user=admin&amp;password=admin">\uc5ec\uae30</a>\ub97c \ud074\ub9ad\ud558\uc5ec \ub85c\uadf8\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub9ac\uace0 \uc989\uc2dc \uc554\ud638\ub97c \ubcc0\uacbd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+
+# accessDenied.jsp
+accessDenied.title = \uc5f0\uacb0\uc774 \uac70\ubd80\ub428
+accessDenied.text = \uc8c4\uc1a1\ud569\ub2c8\ub2e4. \ub2f9\uc2e0\uc740 \uc5f0\uacb0 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.
+
+# top.jsp
+top.home = \ud648
+top.now_playing = \uc7ac\uc0dd
+top.settings = \uc124\uc815
+top.status = \uc0c1\ud0dc
+top.podcast = \ud31f\uce90\uc2a4\ud2b8
+top.more = \ub354\ubcf4\uae30
+top.help = About
+top.search = \uac80\uc0c9
+top.upgrade = <b>\uc54c\ub9bc!</b> \uc0c8\ub85c\uc6b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br>\ub2e4\uc6b4\ub85c\ub4dc {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\uc5ec\uae30</a>.
+top.missing = \uc9c0\uc815\ub41c \uc74c\uc545 \uc800\uc7a5\uc18c\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.
+top.logout = {0} \ub85c\uadf8\uc544\uc6c3
+
+# left.jsp
+left.statistics = {0}&nbsp;\uc544\ud2f0\uc2a4\ud2b8<br>\
+ {1}&nbsp;\uc568\ubc94<br>\
+ {2}&nbsp;\ub178\ub798<br>\
+ {3} (&#126; {4} \uc2dc\uac04)
+left.shortcut = \ubc14\ub85c\uac00\uae30
+left.radio = \uc778\ud130\ub137 TV/radio
+left.allfolders = \ubaa8\ub4e0 \uc800\uc7a5\uc18c
+
+# playlist.jsp
+playlist.stop = \uc815\uc9c0
+playlist.start = \uc7ac\uc0dd\ud558\uae30
+playlist.confirmclear = \uc7ac\uc0dd\ubaa9\ub85d\uc744 \uc9c0\uc6b0\uaca0\uc2b5\ub2c8\uae4c?
+playlist.clear = \ube44\uc6b0\uae30
+playlist.shuffle = \ub79c\ub364
+playlist.repeat_on = \ubc18\ubcf5\uc7ac\uc0dd \ucf1c\uc9d0
+playlist.repeat_off = \ubc18\ubcf5\uc7ac\uc0dd \uaebc\uc9d0
+playlist.undo = \uc2e4\ud589\ucde8\uc18c
+playlist.settings = \uc124\uc815
+playlist.more = \ucd94\uac00 \ud589\ub3d9...
+playlist.more.playlist = \uc7ac\uc0dd\ubaa9\ub85d
+playlist.more.sortbytrack = \ub178\ub798\uc21c\uc73c\ub85c \uc815\ub82c
+playlist.more.sortbyartist = \uc544\ud2f0\uc2a4\ud2b8\uc21c\uc73c\ub85c \uc815\ub82c
+playlist.more.sortbyalbum = \uc568\ubc94\uc21c\uc73c\ub85c \uc815\ub82c
+playlist.more.selection = \uc120\ud0dd\ud55c \ub178\ub798
+playlist.more.selectall = \ubaa8\ub450 \uc120\ud0dd
+playlist.more.selectnone = \uc120\ud0dd \uc5c6\uc74c
+playlist.getflash = \ud50c\ub798\uc2dc \ud50c\ub808\uc774\uc5b4 \uc5bb\uae30
+playlist.load = \ubd88\ub7ec\uc624\uae30
+playlist.save = \uc800\uc7a5\ud558\uae30
+playlist.append = \uc7ac\uc0dd\ubaa9\ub85d\uc5d0 \ucd94\uac00
+playlist.remove = \uc81c\uac70
+playlist.up = \uc704\ub85c
+playlist.down = \uc544\ub798\ub85c
+playlist.empty = \uc7ac\uc0dd\ubaa9\ub85d\uc774 \ube44\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.
+
+# status.jsp
+status.title = \uc0c1\ud0dc
+status.type = \ud0c0\uc785
+status.stream = \uc2a4\ud2b8\ub9ac\ubc0d
+status.download = \ub2e4\uc6b4\ub85c\ub4dc
+status.upload = \uc5c5\ub85c\ub4dc
+status.player = \ud50c\ub808\uc774\uc5b4
+status.user = \uc0ac\uc6a9\uc790
+status.current = \ud604\uc7ac \ud30c\uc77c
+status.transmitted = \uc804\uc1a1
+status.bitrate = \ube44\ud2b8\ub808\uc774\ud2b8 (Kbps)
+
+# search.jsp
+search.title = \uac80\uc0c9
+search.query = \uc544\ud2f0\uc2a4\ud2b8, \uc568\ubc94\uba85\uc774\ub098 \ub178\ub798\uc81c\ubaa9
+search.search = \uac80\uc0c9
+search.index = \uac80\uc0c9 \uc0c9\uc778\uc774 \uc0c8\ub85c \uc0dd\uc131\ub418\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uc2dc \ud6c4\uc5d0 \uc774\uc6a9\ud558\uc138\uc694.
+search.hits.none = \uac80\uc0c9 \uacb0\uacfc\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.
+search.hits.more = \ub354\ubcf4\uae30
+search.hits.artists = \uc544\ud2f0\uc2a4\ud2b8
+search.hits.albums = \uc568\ubc94
+search.hits.songs = \ub178\ub798
+
+# gettingStarted.jsp
+gettingStarted.title = \uc2dc\uc791\ud558\uae30
+gettingStarted.text = <p>\uc11c\ube0c\uc18c\ub2c9\uc5d0 \uc624\uc2e0 \uac83\uc744 \ud658\uc601\ud569\ub2c8\ub2e4.! \uba87\uac00\uc9c0\uc758 \uac04\ub2e8\ud55c \ub2e8\uacc4\ub9cc \ub530\ub77c\ud558\uc2dc\uba74 \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br> \
+ \uc774 \ud654\uba74\uc744 \ubcf4\uace0 \uc2f6\uc73c\uba74 \uc5b8\uc81c\ub77c\ub3c4 \uc0c1\ub2e8\uc758 \ud648\ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694..</p> \
+ <p>\uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \ub3c4\uc6c0\ub9d0\uc744 \ucc38\uc870\ud558\uc2dc\uae30 \ubc14\ub78d\ub2c8\ub2e4.<a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\uc2dc\uc791\ud558\uae30</b></a></p>
+gettingStarted.step1.title = \uad00\ub9ac\uc790 \uc554\ud638 \ubcc0\uacbd\ud558\uae30.
+gettingStarted.step1.text = \uad00\ub9ac\uc790\uc758 \uc554\ud638\ub97c \ubcc0\uacbd\ud568\uc73c\ub85c\uc368 \ub2f9\uc2e0\uc758 \uc11c\ubc84\ub97c \ub354\uc6b1 \uc548\uc804\ud558\uac8c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \
+ \uc11c\ub85c \ub2e4\ub978 \uad8c\ud55c\uc744 \uac00\uc9c4 \uc0ac\uc6a9\uc790\ub97c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+gettingStarted.step2.title = \uc74c\uc545 \uc800\uc7a5\uc18c \uc124\uc815\ud558\uae30.
+gettingStarted.step2.text = \uc11c\ube0c\uc18c\ub2c9\uc5d0\uac8c \ub2f9\uc2e0\uc758 \uc74c\uc545\uc774 \uc5b4\ub514\uc5d0 \uc800\uc7a5\ub418\uc5b4 \uc788\ub294\uc9c0 \uc54c\ub824\uc8fc\uc2ed\uc2dc\uc624.
+gettingStarted.step3.title = \ub124\ud2b8\uc6cc\ud06c \uc124\uc815\ud558\uae30.
+gettingStarted.step3.text = \ub2f9\uc2e0\uc774 \uc778\ud130\ub137\uc744 \ud1b5\ud574 \uc74c\uc545\uc744 \uc990\uae30\uac70\ub098 \uce5c\uad6c\uc640 \uac00\uc871\uacfc \uacf5\uc720\ud558\ub824\ub294 \uacbd\uc6b0\uc5d0\ub294 \uc774 \uc124\uc815\uc774 \uc720\uc6a9\ud569\ub2c8\ub2e4. \
+ <b><em>\ub2f9\uc2e0\uc758 \uc774\ub984</em>.subsonic.org</b> \uc758 \uc8fc\uc18c\ub97c \uc774\uc6a9\ud558\uc138\uc694.
+gettingStarted.hide = \ub2e4\uc2dc \ubcf4\uc9c0 \uc54a\uae30
+gettingStarted.hidealert = \uc774 \ud654\uba74\uc744 \ub2e4\uc2dc \ubcf4\uace0 \uc2f6\ub2e4\uba74 \uc124\uc815 > \uc77c\ubc18 \uc744 \ucc38\uc870\ud558\uc138\uc694.
+
+# home.jsp
+home.random.title = \ub79c\ub364
+home.newest.title = \uc2e0\uace1
+home.highest.title = \ucd5c\uace0 \ud3c9\uc810
+home.frequent.title = \uc790\uc8fc \uc7ac\uc0dd
+home.recent.title = \ucd5c\uadfc \uc7ac\uc0dd
+home.users.title = \uc0ac\uc6a9\uc790
+home.random.text = \ub79c\ub364 \uc568\ubc94
+home.newest.text = \uac00\uc7a5 \ucd5c\uadfc\uc5d0 \ucd94\uac00\ub41c \uc568\ubc94
+home.highest.text = \ucd5c\uace0 \ud3c9\uc810\uc778 \uc568\ubc94
+home.frequent.text = \uac00\uc7a5 \uc790\uc8fc \uc7ac\uc0dd\ub41c \uc568\ubc94
+home.recent.text = \uac00\uc7a5 \ucd5c\uadfc\uc5d0 \uc7ac\uc0dd\ub41c \uc568\ubc94
+home.users.text = \uc0ac\uc6a9\uc790 \ud1b5\uacc4
+home.scan = \uc74c\uc545\uc800\uc7a5\uc18c\ub294 \uc544\uc9c1 \uac80\uc0c9 \uc911\uc785\ub2c8\ub2e4. \uc77c\ubd80 \uae30\ub2a5\uc5d0 \uc81c\ud55c\uc774 \uc788\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+home.listsize = 1 \ud398\uc774\uc9c0\ub2f9 {0}\uac1c
+home.albums = \uc568\ubc94 {0} - {1}
+home.playcount = {0} \uac1c\uc758 \ub178\ub798\uac00 \uc7ac\uc0dd\ub428
+home.lastplayed = Played {0}
+home.created = Created {0}
+home.chart.total = \ucd1d\ub7c9 (MB)
+home.chart.stream = \uc2a4\ud2b8\ub9ac\ubc0d (MB)
+home.chart.download = \ub2e4\uc6b4\ub85c\ub4dc (MB)
+home.chart.upload = \uc5c5\ub85c\ub4dc (MB)
+
+# more.jsp
+more.title = \ub354 \ubcf4\uae30
+more.random.title = \ub79c\ub364 \uc7ac\uc0dd\ubaa9\ub85d
+more.random.text = \uc774 \uc124\uc815\uc73c\ub85c \ub79c\ub364 \uc7ac\uc0dd\ubaa9\ub85d \ub9cc\ub4e4\uae30
+more.random.songs = {0} \ub178\ub798\ub4e4
+more.random.auto = \uc7ac\uc0dd\ubaa9\ub85d\uc758 \ub05d\uc5d0 \ub3c4\ub2ec\ud558\uba74 \ub354 \ub9ce\uc740 \uace1\uc744 \ubb34\uc791\uc704\uc73c\ub85c \uc7ac\uc0dd\ud569\ub2c8\ub2e4.
+more.random.ok = OK
+more.random.genre = \uc7a5\ub974
+more.random.anygenre = \ubaa8\ub4e0
+more.random.year = \uc5f0\ub3c4
+more.random.anyyear = \ubaa8\ub4e0
+more.random.folder = \uc800\uc7a5\uc18c\uc5d0
+more.random.anyfolder = \ubaa8\ub4e0
+more.apps.title = \uc11c\ube0c\uc18c\ub2c9 \uc571\uc2a4
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">\uc11c\ube0c\uc18c\ub2c9 \uc571\uc2a4</a> <b>\uc544\uc774\ud3f0</b>, \
+ <b>\uc548\ub4dc\ub85c\uc774\ub4dc</b> \uc640 <b>AIR</b>\uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.</p>
+more.mobile.title = \ubaa8\ubc14\uc77c \ud3f0
+more.mobile.text = <p>{0}\ub2f9\uc2e0\uc758 WAP\uac00 \uc0ac\uc6a9\uac00\ub2a5\ud55c \ubaa8\ubc14\uc77c\ud3f0\uc774\ub098 PDA\uc5d0\uc11c \uc74c\uc545\uc744 \uac10\uc0c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br> \
+ \ub2f9\uc2e0\uc758 \uc804\ud654\uae30\uc5d0\uc11c URL\uc744 \uc811\uc18d\ud558\uae30\ub9cc \ud558\uba74 \ub429\ub2c8\ub2e4.: <b>http://yourhostname/wap</b></p> \
+ <p>\ub2e8, \ub2f9\uc2e0\uc758 \uc11c\ubc84\uac00 \uc778\ud130\ub137\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.</p>
+more.podcast.title = \ud31f\uce90\uc2a4\ud2b8
+more.podcast.text = <p>\uc800\uc7a5\ub41c \uc7ac\uc0dd\ubaa9\ub85d\uc73c\ub85c \ud31f\uce90\uc2a4\ud2b8\uc5d0 \uc774\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.<br>\
+ \ud31f\uce90\uc2a4\ud2b8 \uc218\uc2e0\uae30\uc5d0 \uc774 \uc8fc\uc18c\ub97c \uc785\ub825\ud558\uac70\ub098: <b>http://yourhostname/podcast</b>, \
+ \ub610\ub294 <b><a href="podcast.view?suffix=.rss">\uc5ec\uae30\ub97c \ud074\ub9ad\ud558\uc138\uc694</a>.</b></p>
+more.upload.title = \ud30c\uc77c \uc5c5\ub85c\ub4dc
+more.upload.source = \ud30c\uc77c \uc120\ud0dd
+more.upload.target = \uc5c5\ub85c\ub4dc \ud560 \uacf3
+more.upload.browse = \ucc3e\uc544\ubcf4\uae30
+more.upload.ok = \uc5c5\ub85c\ub4dc
+more.upload.unzip = \uc790\ub3d9\uc73c\ub85c \uc555\ucd95(zip) \ud480\uae30.
+more.upload.progress = % \uc644\ub8cc\ub428. \uae30\ub2e4\ub824 \uc8fc\uc138\uc694...
+
+# upload.jsp
+upload.title = \ud30c\uc77c \uc5c5\ub85c\ub4dc
+upload.success = \uc5c5\ub85c\ub4dc\uac00 \uc131\uacf5\ud558\uc600\uc2b5\ub2c8\ub2e4. <b>{0}</b>
+upload.empty = \uc5c5\ub85c\ub4dc\ud560 \ud30c\uc77c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.
+upload.failed = \uc5c5\ub85c\ub4dc\uac00 \uc2e4\ud328 \ud558\uc600\uc2b5\ub2c8\ub2e4. \uc624\ub958:<br><b>"{0}"</b>
+upload.unzipped = \uc555\ucd95\uc744 \ud480\uc5c8\uc2b5\ub2c8\ub2e4. {0}
+
+# help.jsp
+help.title = About {0}
+help.upgrade = <b>\uc54c\ub9bc!</b> \uc0c8\ub85c\uc6b4 \ubc84\uc804\uc774 \ub098\uc654\uc2b5\ub2c8\ub2e4. \ub2e4\uc6b4\ub85c\ub4dc {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\uc5ec\uae30</a>.
+help.version.title = \ubc84\uc804
+help.builddate.title = \ube4c\ub4dc \ub0a0\uc9dc
+help.server.title = \uc11c\ubc84
+help.license.title = Terms&nbsp;of&nbsp;use
+help.license.text = {0} is free software distributed under the <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source license. \
+ {0} uses <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensed third-party libraries</a>. Please note that {0} is <em>not</em> \
+ a tool for illegal distribution of copyrighted material. Always pay attention to and follow the relevant laws specific to your country.
+help.homepage.title = \ud648\ud398\uc774\uc9c0
+help.forum.title = \ud3ec\ub7fc
+help.shop.title = Merchandise
+help.contact.title = Contact
+help.contact.text = {0} is developed and maintained by Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ If you have any questions, comments or suggestions for improvements, please visit the \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} is free, but you can contribute to the project by giving a <b><a href="donate.view?">donation</a></b>.
+help.log = \ub85c\uadf8
+help.logfile = \ub85c\uadf8\uac00 {0} \uc5d0 \uc815\uc0c1\uc801\uc73c\ub85c \uc800\uc7a5\ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+
+# settingsHeader.jsp
+settingsheader.title = \uc124\uc815
+settingsheader.general = \uc77c\ubc18
+settingsheader.advanced = \uace0\uae09
+settingsheader.personal = \uac1c\uc778
+settingsheader.musicFolder = \uc74c\uc545 \uc800\uc7a5\uc18c
+settingsheader.internetRadio = \uc778\ud130\ub137 TV/\ub77c\ub514\uc624
+settingsheader.podcast = \ud31f\uce90\uc2a4\ud2b8
+settingsheader.player = \uc7ac\uc0dd\uae30
+settingsheader.share = \uacf5\uc720\ub41c \ubbf8\ub514\uc5b4
+settingsheader.network = \ub124\ud2b8\uc6cc\ud06c
+settingsheader.transcoding = \ubcc0\ud658
+settingsheader.user = \uc0ac\uc6a9\uc790
+settingsheader.search = \uac80\uc0c9
+settingsheader.coverArt = \uc568\ubc94\uc544\ud2b8
+settingsheader.password = \uc554\ud638
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \uc7ac\uc0dd\ubaa9\ub85d \uc800\uc7a5\uc18c
+generalsettings.musicmask = \uc74c\uc545\ud30c\uc77c \uc885\ub958
+generalsettings.videomask = \ub3d9\uc601\uc0c1\ud30c\uc77c \uc885\ub958
+generalsettings.coverartmask = \uc568\ubc94\uc544\ud2b8 \uc885\ub958
+generalsettings.index = \uc0c9\uc778
+generalsettings.ignoredarticles = \ubb34\uc2dc\ud560 \uc811\ub450\uc0ac
+generalsettings.shortcuts = \ubc14\ub85c\uac00\uae30
+generalsettings.showgettingstarted = "\uc2dc\uc791\ud558\uae30"\ub97c \uccab\ud654\uba74\uc5d0 \ud45c\uc2dc
+generalsettings.welcometitle = \ud658\uc601\uba54\uc2dc\uc9c0 \uc81c\ubaa9
+generalsettings.welcomesubtitle = \ud658\uc601\uba54\uc2dc\uc9c0 \ubd80\uc81c\ubaa9
+generalsettings.welcomemessage = \ud658\uc601\uba54\uc2dc\uc9c0
+generalsettings.loginmessage = \ub85c\uadf8\uc778 \uba54\uc2dc\uc9c0
+generalsettings.language = \uae30\ubcf8 \uc5b8\uc5b4\uc124\uc815
+generalsettings.theme = \uae30\ubcf8 \ud14c\ub9c8
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = \ub2e4\uc6b4\ub85c\ub4dc \uc0d8\ud50c\ub9c1 \uba85\ub839\uc5b4
+advancedsettings.coverartlimit = \uc568\ubc94\uc544\ud2b8 \uc81c\ud55c<br><div class="detail">(0 = \uc81c\ud55c\uc5c6\uc74c)</div>
+advancedsettings.downloadlimit = \ub2e4\uc6b4\ub85c\ub4dc \uc81c\ud55c (Kbps)<br><div class="detail">(0 = \uc81c\ud55c\uc5c6\uc74c)</div>
+advancedsettings.uploadlimit = \uc5c5\ub85c\ub4dc \uc81c\ud55c (Kbps)<br><div class="detail">(0 = \uc81c\ud55c\uc5c6\uc74c)</div>
+advancedsettings.streamport = Non-SSL \uc2a4\ud2b8\ub9ac\ubc0d \ud3ec\ud2b8<br><div class="detail">(0 = \uc0ac\uc6a9\uc548\ud568)</div>
+advancedsettings.ldapenabled = LDAP \uc778\uc99d \uc0ac\uc6a9\ud558\uae30
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP \uac80\uc0c9 \ud544\ud130
+advancedsettings.ldapmanagerdn = LDAP \uad00\ub9ac\uc790 DN<br><div class="detail">(Optional)</div>
+advancedsettings.ldapmanagerpassword = \uc554\ud638
+advancedsettings.ldapautoshadowing = {0} \uc5d0 \uc0ac\uc6a9\uc790 \uc790\ub3d9 \uc0dd\uc131
+
+# personalSettings.jsp
+personalsettings.title = {0} \uc758 \uac1c\uc778 \uc124\uc815
+personalsettings.language = \uc0ac\uc6a9\uc5b8\uc5b4
+personalsettings.theme = \ud14c\ub9c8
+personalsettings.display = \ub514\uc2a4\ud50c\ub808\uc774
+personalsettings.browse = \ud45c\uc2dc\ud654\uba74
+personalsettings.playlist = \uc7ac\uc0dd\ubaa9\ub85d
+personalsettings.tracknumber = \ud2b8\ub799 #
+personalsettings.artist = \uc544\ud2f0\uc2a4\ud2b8
+personalsettings.album = \uc568\ubc94
+personalsettings.genre = \uc7a5\ub974
+personalsettings.year = \uc5f0\ub3c4
+personalsettings.bitrate = \ube44\ud2b8 \uc804\uc1a1\ub960
+personalsettings.duration = \uc7ac\uc0dd\uc2dc\uac04
+personalsettings.format = \ud3ec\ub9f7
+personalsettings.filesize = \ud30c\uc77c \ud06c\uae30
+personalsettings.captioncutoff = \uc81c\ubaa9\ud45c\uc2dc \uae38\uc774
+personalsettings.partymode = \ud30c\ud2f0\ubaa8\ub4dc
+personalsettings.shownowplaying = \ub2e4\ub978\uc0ac\ub78c\uc774 \uac10\uc0c1\ud558\ub294 \ub178\ub798 \uc815\ubcf4\ub97c \ubcfc \uc218 \uc788\uc74c
+personalsettings.nowplayingallowed = \ub2e4\ub978 \uc0ac\ub78c\uc774 \ub0b4\uac00 \uac10\uc0c1\ud558\ub294 \ub178\ub798 \uc815\ubcf4\ub97c \ubcfc \uc218 \uc788\uc74c
+personalsettings.showchat = \ucc44\ud305 \uba54\uc2dc\uc9c0 \ubcf4\uc774\uae30
+personalsettings.finalversionnotification = \uc0c8\ub85c\uc6b4 \ubc84\uc804(\uc548\uc815)\uc774 \ub098\uc624\uba74 \ub098\uc5d0\uac8c \uc54c\ub9bc
+personalsettings.betaversionnotification = \uc0c8\ub85c\uc6b4 \ubc84\uc804(\ubca0\ud0c0)\uc774 \ub098\uc624\uba74 \ub098\uc5d0\uac8c \uc54c\ub9bc
+personalsettings.lastfmenabled = \ub0b4\uac00 \uac10\uc0c1\ud558\ub294 \ub178\ub798\ub97c <a href="http://last.fm/" target="_blank">Last.fm</a> \uc5d0 \ub4f1\ub85d\ud558\uae30
+personalsettings.lastfmusername = Last.fm \uc0ac\uc6a9\uc790 \uc774\ub984
+personalsettings.lastfmpassword = Last.fm \uc554\ud638
+personalsettings.avatar.title = \uac1c\uc778 \uc774\ubbf8\uc9c0
+personalsettings.avatar.none = \uc774\ubbf8\uc9c0 \uc5c6\uc74c
+personalsettings.avatar.custom = \uc0ac\uc6a9\uc790 \uc774\ubbf8\uc9c0
+personalsettings.avatar.changecustom = \uc0ac\uc6a9\uc790 \uc774\ubbf8\uc9c0 \ubc14\uafb8\uae30
+personalsettings.avatar.upload = \uc5c5\ub85c\ub4dc
+personalsettings.avatar.courtesy = Icons courtesy of <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = \uac1c\uc778 \uc774\ubbf8\uc9c0 \ubc14\uafb8\uae30
+avataruploadresult.success = \uc774\ubbf8\uc9c0 "{0}" \ub97c \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub85c\ub4dc \ud558\uc600\uc2b5\ub2c8\ub2e4.
+avataruploadresult.failure = \uc774\ubbf8\uc9c0 \uc5c5\ub85c\ub4dc\uac00 \uc2e4\ud328 \ud558\uc600\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \uac83\uc740 <a href="help.view?">\ub85c\uadf8</a>\ub97c \ucc38\uace0\ud558\uc138\uc694.
+
+# passwordSettings.jsp
+passwordsettings.title = {0} \uc758 \uc554\ud638 \ubc14\uafb8\uae30
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \uc800\uc7a5\uc18c
+musicfoldersettings.name = \uc774\ub984
+musicfoldersettings.enabled = \uc0ac\uc6a9\ud568
+musicfoldersettings.add = \uc74c\uc545\uc800\uc7a5\uc18c \ucd94\uac00
+musicfoldersettings.nopath = \uc62c\ubc14\ub978 \uc800\uc7a5\uc18c \uacbd\ub85c\ub97c \uc785\ub825\ud558\uc138\uc694.
+
+# networkSettings.jsp
+networksettings.text = \uc778\ud130\ub137\uc744 \ud1b5\ud574 \uc11c\ube0c\uc18c\ub2c9 \uc11c\ubc84\uc5d0 \uc811\uadfc\ud558\ub294 \ubc29\ubc95\uc744 \uc124\uc815\ud569\ub2c8\ub2e4..<br> \
+ \ub9cc\uc57d \uc124\uc815\uc5d0 \uc774\uc0c1\uc774 \uc788\ub2e4\uba74 \uac00\uc774\ub4dc\ub97c \ucc38\uace0\ud558\uc2ed\uc2dc\uc624. <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\uc2dc\uc791\ud558\uae30</b></a>
+networksettings.portforwardingenabled = \uc11c\ube0c\uc18c\ub2c9\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ub418\ub3c4\ub85d \ub77c\uc6b0\ud130\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4. (UPnP \ub098 NAT-PMP \ud3ec\ud2b8 \ud3ec\uc6cc\ub529 \uc0ac\uc6a9\ud558\uae30).
+networksettings.portforwardinghelp = \ub77c\uc6b0\ud130\uac00 \uc790\ub3d9\uc73c\ub85c \uad6c\uc131\ub418\uc9c0 \uc54a\ub294\ub2e4\uba74 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \
+ <a href="http://portforward.com/" target="_blank">portforward.com</a> \uc758 \uc9c0\uce68\uc5d0 \ub530\ub974\uc2ed\uc2dc\uc624. \
+ \ub2f9\uc2e0\uc740 {0}\ubc88 \ud3ec\ud2b8\ub97c \uc11c\ube0c\uc18c\ub2c9 \uc11c\ubc84\ub85c \ud3ec\uc6cc\ub529 \ud574\uc57c \ud569\ub2c8\ub2e4..
+networksettings.urlredirectionenabled = \uae30\uc5b5\ud558\uae30 \uc26c\uc6b4 \uc8fc\uc18c\ub85c \uc778\ud130\ub137\uc744 \ud1b5\ud574 \uc11c\ube0c\uc18c\ub2c9 \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+networksettings.status = \uc0c1\ud0dc:
+networksettings.trialexpired = \ud3c9\uac00\ud310\uc774 \ub9cc\ub8cc\ub418\uc5c8\uc2b5\ub2c8\ub2e4 {0}. <b><a href="donate.view?">\uae30\ubd80\ud558\uae30</a></b> \ub97c \ud1b5\ud574 \uae30\uac04 \uc81c\ud55c \uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+networksettings.trialnotexpired = \uc774 \uae30\ub2a5\uc740 {0}\uc77c\ub9cc \uc0ac\uc6a9\uac00\ub2a5 \ud569\ub2c8\ub2e4. <b><a href="donate.view?">\uae30\ubd80\ud558\uae30</a></b> \ub97c \ud1b5\ud574 \uae30\uac04 \uc81c\ud55c \uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218\uc788\uc2b5\ub2c8\ub2e4.
+
+# transcodingSettings.jsp
+transcodingsettings.name = \ubcc0\ud658 \uc774\ub984
+transcodingsettings.sourceformat = \uc6d0\ubcf8 \ud30c\uc77c \ud0c0\uc785
+transcodingsettings.targetformat = \uacb0\uacfc \ud30c\uc77c \ud0c0\uc785
+transcodingsettings.step1 = 1 \ub2e8\uacc4
+transcodingsettings.step2 = 2 \ub2e8\uacc4
+transcodingsettings.step3 = 3 \ub2e8\uacc4
+transcodingsettings.defaultactive = \uae30\ubcf8\uac12
+transcodingsettings.enabled = \uc0ac\uc6a9
+transcodingsettings.add = \ud30c\uc77c \ubcc0\ud658 \ucd94\uac00
+transcodingsettings.recommended = \ucd94\ucc9c\ud558\ub294 \ubcc0\ud658 \ubc29\ubc95
+transcodingsettings.noname = \ubcc0\ud658 \uc774\ub984\uc744 \ub2e4\uc2dc \uc9c0\uc815\ud574\uc8fc\uc138\uc694.
+transcodingsettings.nosourceformat = \uc6d0\ubcf8 \ud30c\uc77c \ud0c0\uc785\uc744 \ub2e4\uc2dc \uc9c0\uc815\ud574\uc8fc\uc138\uc694.
+transcodingsettings.notargetformat = \uacb0\uacfc \ud30c\uc77c \ud0c0\uc785\uc744 \ub2e4\uc2dc \uc9c0\uc815\ud574\uc8fc\uc138\uc694.
+transcodingsettings.nostep1 = \ud558\ub098 \uc774\uc0c1\uc758 \ubcc0\ud658 \ub2e8\uacc4\ub97c \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.
+transcodingsettings.info = <p class="detail">(%s = \ubcc0\ud658\ud560 \ud30c\uc77c, %b = \uc7ac\uc0dd\uae30\uc758 \ucd5c\ub300 \ube44\ud2b8 \uc804\uc1a1\ub960, %t = \uc81c\ubaa9, %a = \uc544\ud2f0\uc2a4\ud2b8, %l = \uc568\ubc94)</p> \
+ <p>\ubcc0\ud658\uc740 \ubbf8\ub514\uc5b4 \ud3ec\ub9f7\uc744 \ubcc0\ud658\ud558\ub294 \uc808\ucc28\uc785\ub2c8\ub2e4. {1}''s \ubcc0\ud658 \
+ \uc5d4\uc9c4\uc740 \uc77c\ubc18\uc801\uc73c\ub85c \uc2a4\ud2b8\ub9ac\ubc0d\uc774 \ubd88\uac00\ub2a5\ud55c \ubbf8\ub514\uc5b4\ub3c4 \uc2a4\ud2b8\ub9ac\ubc0d\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d \ud569\ub2c8\ub2e4. \ubcc0\ud658\uc740 \ubc14\ub85c \uc2e4\ud589\ub418\uba70 \
+ \ucd94\uac00\uc801\uc778 \ub514\uc2a4\ud06c \uacf5\uac04\uc744 \ud544\uc694\ub85c \ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.<p/> \
+ <p>\uc2e4\uc81c \ubcc0\ud658\uc740 {0}\uc5d0 \ud0c0\uc0ac \uba85\ub839\uc904 \ud504\ub85c\uadf8\ub7a8\uc774 \uc124\uce58\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \
+ \uc708\ub3c4\uc6b0\uc6a9 \ubcc0\ud658 \ud328\ud0a4\uc9c0\ub294 <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>\uc5ec\uae30</b></a>\uc5d0 \uc788\uc2b5\ub2c8\ub2e4. \
+ \uc0ac\uc6a9\uc790 \uc815\uc758 \ubcc0\ud658 \ubc29\ubc95\uc740 \uc544\ub798\uc640 \uac19\uc740 \uc0ac\ud56d\uc744 \ub9cc\uc871\ud574\uc57c \ud569\ub2c8\ub2e4. \
+ <ul> \
+ <li>\uba85\ub839\uc904 \uc778\ud130\ud398\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4.</li> \
+ <li>\ud45c\uc900\ucd9c\ub825\uc73c\ub85c \uacb0\uacfc\ubb3c\uc744 \ub0b4\ubcf4\ub0bc \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.</li> \
+ <li>2\ub2e8\uacc4\ub098 3\ub2e8\uacc4\uc5d0\uc11c \uc774\uc6a9\ud558\ub824\uba74 \ud45c\uc900\uc785\ub825\uc73c\ub85c \uc785\ub825\uc744 \ubc1b\uc544\uc57c \ud569\ub2c8\ub2e4.</li> \
+ </ul> \
+ </p> \
+ <p> \uc7ac\uc0dd\uae30 \uc124\uc815\ud398\uc774\uc9c0\uc5d0\uc11c \ud65c\uc131\ud654\ub418\ub294 \ubcc0\ud658\uc744 \ubcc0\uacbd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \
+ "\uae30\ubcf8\uac12"\uc774 \uc120\ud0dd\ub418\uc5b4 \uc788\uc73c\uba74 \ubcc0\ud658\uc774 \uc790\ub3d9\uc73c\ub85c \uc0c8\ub85c\uc6b4 \uc7ac\uc0dd\uae30\uc5d0\uc11c \ud65c\uc131\ud654 \ub429\ub2c8\ub2e4. </p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = \uc2a4\ud2b8\ub9ac\ubc0d URL
+internetradiosettings.homepageurl = \ud648\ud398\uc774\uc9c0
+internetradiosettings.name = \uc774\ub984
+internetradiosettings.enabled = \uc0ac\uc6a9\ud568
+internetradiosettings.add = TV/\ub77c\ub514\uc624 \ucd94\uac00\ud558\uae30
+internetradiosettings.nourl = URL\uc744 \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.
+internetradiosettings.noname = \uc774\ub984\uc744 \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.
+
+# podcastSettings.jsp
+podcastsettings.update = \uc0c8\ub85c\uc6b4 \uc5d0\ud53c\uc18c\ub4dc \ud655\uc778
+podcastsettings.keep = \uc720\uc9c0\ud558\uae30
+podcastsettings.keep.all = \ubaa8\ub4e0 \uc5d0\ud53c\uc18c\ub4dc
+podcastsettings.keep.one = \uac00\uc7a5 \ucd5c\uadfc \uc5d0\ud53c\uc18c\ub4dc
+podcastsettings.keep.many = \ub9c8\uc9c0\ub9c9 {0} \uac1c\uc758 \uc5d0\ud53c\uc18c\ub4dc
+podcastsettings.download = \uc0c8\ub85c\uc6b4 \uc5d0\ud53c\uc18c\ub4dc\ub97c \uc0ac\uc6a9 \uac00\ub2a5 \ud560 \ub54c
+podcastsettings.download.all = \ubaa8\ub450 \ub2e4\uc6b4\ub85c\ub4dc
+podcastsettings.download.one = \ucd5c\uadfc 1\uac1c \ub2e4\uc6b4\ub85c\ub4dc
+podcastsettings.download.many = \ucd5c\uadfc {0} \uac1c \uc5d0\ud53c\uc18c\ub4dc \ub2e4\uc6b4\ub85c\ub4dc
+podcastsettings.download.none = \uc544\ubb34\uac83\ub3c4 \ud558\uc9c0 \uc54a\uae30
+podcastsettings.interval.manually = \uc218\ub3d9\uc73c\ub85c
+podcastsettings.interval.hourly = \ub9e4 \uc2dc\uac04\ub9c8\ub2e4
+podcastsettings.interval.daily = \ub9e4 \uc77c\uc77c\ub9c8\ub2e4
+podcastsettings.interval.weekly = \ub9e4 \uc8fc\ub9c8\ub2e4
+podcastsettings.folder = \ud31f\uce90\uc2a4\ud2b8 \uc800\uc7a5 \uc704\uce58
+
+# playerSettings.jsp
+playersettings.noplayers = \ubc1c\uacac\ub41c \uc7ac\uc0dd\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.
+playersettings.type = \ud0c0\uc785
+playersettings.lastseen = \ub9c8\uc9c0\ub9c9 \uc774\uc6a9
+playersettings.title = \uc7ac\uc0dd\uae30 \uc120\ud0dd
+
+playersettings.technology.web.title = \uc6f9 \uc7ac\uc0dd\uae30
+playersettings.technology.external.title = \uc678\ubd80 \uc7ac\uc0dd\uae30
+playersettings.technology.external_with_playlist.title = \uc7ac\uc0dd \ubaa9\ub85d\uc744 \uc678\ubd80 \uc7ac\uc0dd\uae30\uc5d0\uc11c \uc7ac\uc0dd
+playersettings.technology.jukebox.title = \uc8fc\ud06c\ubc15\uc2a4
+playersettings.technology.web.text = \uc6f9\ube0c\ub77c\uc6b0\uc800\uc5d0 \ub0b4\uc7a5\ub41c \ud50c\ub798\uc2dc \ud50c\ub808\uc774\uc5b4\ub97c \uc774\uc6a9\ud558\uc5ec \ubc14\ub85c \uc7ac\uc0dd\ud569\ub2c8\ub2e4.
+playersettings.technology.external.text = \uc708\uc570\ud504\ub098 \uc708\ub3c4\uc6b0 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4 \ub4f1\uacfc \uac19\uc774 \uc0ac\uc6a9\uc790\uac00 \uc88b\uc544\ud558\ub294 \uc7ac\uc0dd\uae30\ub85c \uc74c\uc545\uc744 \uc7ac\uc0dd\ud569\ub2c8\ub2e4.
+playersettings.technology.external_with_playlist.text = \uc704\uc640 \uac19\uc9c0\ub9cc, \ub3d9\uc77c \uc7ac\uc0dd \ubaa9\ub85d\uc740 \uc7ac\uc0dd\uae30\uac00 \uc544\ub2cc \uc11c\ube0c\uc18c\ub2c9 \uc11c\ubc84\uc5d0 \uc758\ud574 \uad00\ub9ac\ub429\ub2c8\ub2e4. \
+ \uc774 \ubaa8\ub4dc\uc5d0\uc11c \uc74c\uc545\uc2dc\uac04 \uac74\ub108\ub6f0\ub294 \uac83\uc774 \uac00\ub2a5\ud569\ub2c8\ub2e4.
+playersettings.technology.jukebox.text = \uc11c\ube0c\uc18c\ub2c9 \uc11c\ubc84\uc758 \uc624\ub514\uc624 \uc7a5\uce58\uc5d0\uc11c \uc9c1\uc811 \uc7ac\uc0dd\ud569\ub2c8\ub2e4. (\uc778\uc99d\ub41c \uc0ac\uc6a9\uc790\ub9cc \uac00\ub2a5\ud568).
+playersettings.name = \uc7ac\uc0dd\uae30 \uc774\ub984
+playersettings.coverartsize = \uc568\ubc94\uc544\ud2b8 \ud06c\uae30
+playersettings.maxbitrate = \ucd5c\ub300 \ube44\ud2b8\uc804\uc1a1\ub960
+playersettings.coverart.off = \uaebc\uc9d0
+playersettings.coverart.small = \uc791\uc74c
+playersettings.coverart.medium = \uc911\uac04
+playersettings.coverart.large = \ud07c
+playersettings.nolame = <em>\uc54c\ub9bc:</em> LAME \uc774 \uc124\uce58\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.<br>\uc790\uc138\ud55c \uc0ac\ud56d\uc740 \ub3c4\uc6c0\ub9d0\uc744 \ucc38\uc870\ud558\uc138\uc694.
+playersettings.autocontrol = \uc7ac\uc0dd\uae30 \uc790\ub3d9\uc81c\uc5b4
+playersettings.dynamicip = \uc7ac\uc0dd\uae30\ub294 \ub3d9\uc801 IP\ub97c \uc0ac\uc6a9\ud568.
+playersettings.transcodings = \ud65c\uc131\ud654\ud560 \ubcc0\ud658
+playersettings.ok = \uc800\uc7a5
+playersettings.forget = \uc7ac\uc0dd\uae30 \uc0ad\uc81c
+playersettings.clone = \uc7ac\uc0dd\uae30 \ubcf5\uc81c
+
+# shareSettings.jsp
+sharesettings.name = \uacf5\uc720\uc774\ub984
+sharesettings.owner = \uacf5\uc720\ud55c \uc0ac\ub78c
+sharesettings.description = \uc124\uba85
+sharesettings.visits = \ubc29\ubb38 \ud69f\uc218
+sharesettings.lastvisited = \ub9c8\uc9c0\ub9c9 \ubc29\ubb38
+sharesettings.expires = \ub9cc\ub8cc\uc77c
+sharesettings.files = \uacf5\uc720\ub41c \ud30c\uc77c
+sharesettings.expirein = \uacf5\uc720\uae30\uac04
+sharesettings.expirein.week = 1\uc8fc\uc77c
+sharesettings.expirein.month = \ud55c\ub2ec
+sharesettings.expirein.year = 1\ub144
+sharesettings.expirein.never = \ub9cc\ub8cc\ub418\uc9c0 \uc54a\uc74c
+
+# userSettings.jsp
+usersettings.title = \uc0ac\uc6a9\uc790 \uc120\ud0dd
+usersettings.newuser = \uc2e0\uaddc \uc0ac\uc6a9\uc790
+usersettings.admin = \uc0ac\uc6a9\uc790\ub294 \uad00\ub9ac\uc790\uc785\ub2c8\ub2e4.
+usersettings.settings = \uc0ac\uc6a9\uc790\ub294 \uac1c\uc778\uc124\uc815\uacfc \uc554\ud638\ub97c \ubcc0\uacbd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.stream = \uc0ac\uc6a9\uc790\ub294 \ud30c\uc77c\uc744 \uc7ac\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.jukebox = \uc0ac\uc6a9\uc790\ub294 \uc8fc\ud06c\ubc15\uc2a4\uc5d0\uc11c \ud30c\uc77c\uc744 \uc7ac\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.download = \uc0ac\uc6a9\uc790\ub294 \ud30c\uc77c\uc744 \ub2e4\uc6b4\ub85c\ub4dc \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.upload = \uc0ac\uc6a9\uc790\ub294 \ud30c\uc77c\uc744 \uc5c5\ub85c\ub4dc \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.playlist= \uc0ac\uc6a9\uc790\ub294 \uc7ac\uc0dd\ubaa9\ub85d\uc758 \uc0dd\uc131\uacfc \uc0ad\uc81c\uac00 \uac00\ub2a5\ud569\ub2c8\ub2e4.
+usersettings.coverart = \uc0ac\uc6a9\uc790\ub294 \uc568\ubc94\uc544\ud2b8\uc640 \ud0dc\uadf8\ub97c \uc218\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.comment= \uc0ac\uc6a9\uc790\ub294 \ucf54\uba58\ud2b8\uc758 \uc0dd\uc131 \ubc0f \uc218\uc815\uacfc \ud3c9\uc810\uc744 \uc218\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.podcast= \uc0ac\uc6a9\uc790\ub294 \ud31f\uce90\uc2a4\ud2b8\ub97c \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
+usersettings.username = \uc0ac\uc6a9\uc790\uc774\ub984
+usersettings.changepassword = \uc554\ud638\ubcc0\uacbd
+usersettings.password = \ud604\uc7ac\uc554\ud638
+usersettings.newpassword = \uc0c8\ub85c\uc6b4 \uc554\ud638
+usersettings.confirmpassword = \uc0c8\ub85c\uc6b4 \uc554\ud638 \ud655\uc778\uc785\ub825
+usersettings.delete = \uc774 \uc0ac\uc6a9\uc790\ub97c \uc0ad\uc81c\ud569\ub2c8\ub2e4.
+usersettings.ldap = LDAP \uc5d0\uc11c \uc778\uc99d\ud55c \uc0ac\uc6a9\uc790\uc785\ub2c8\ub2e4.
+usersettings.nousername = \uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.
+usersettings.useralreadyexists = \uc0ac\uc6a9\uc790\uac00 \uc874\uc7ac\ud569\ub2c8\ub2e4.
+usersettings.nopassword = \uc554\ud638\uac00 \ud544\uc694\ud569\ub2c8\ub2e4.
+usersettings.wrongpassword = \uc554\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
+usersettings.ldapdisabled = LDAP \uc778\uc99d\uc774 \ub3d9\uc791\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uace0\uae09 \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.
+usersettings.passwordnotsupportedforldap = LDAP\uc73c\ub85c \uc778\uc99d\ub41c \uc0ac\uc6a9\uc790\uc758 \uc554\ud638\ub97c \ubcc0\uacbd\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
+usersettings.ok = {0}\uc758 \uc554\ud638\uac00 \uc131\uacf5\uc801\uc73c\ub85c \ubcc0\uacbd\ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = \uac31\uc2e0 \uc548\ud568
+musicfoldersettings.interval.one = \ub9e4\uc77c
+musicfoldersettings.interval.many = \ub9e4 {0} \uc77c\ub9c8\ub2e4
+musicfoldersettings.hour = {0}\uc2dc:00
+
+# main.jsp
+main.up = \uc704\ub85c
+main.playall = \ubaa8\ub450 \uc7ac\uc0dd
+main.playrandom = \ub79c\ub364 \uc7ac\uc0dd
+main.addall = \ubaa8\ub450 \ucd94\uac00
+main.tags = \ud0dc\uadf8 \uc218\uc815
+main.playcount = {0}\ubc88 \uc7ac\uc0dd\ub428.
+main.lastplayed = {0}\uc5d0 \ub9c8\uc9c0\ub9c9 \uc7ac\uc0dd\ub428.
+main.comment = \ucf54\uba58\ud2b8
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__\ud14d\uc2a4\ud2b8__</td><td>\uad75\uc740 \ud14d\uc2a4\ud2b8 </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>\uc904\ubc14\uafc8</td></tr>\
+ <tr><td style="padding-right:1em">~~\ud14d\uc2a4\ud2b8~~</td><td>\uc774\ud0e4\ub9ad \ud14d\uc2a4\ud2b8 </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>\uc0c8 \ub2e8\ub77d</td></tr>\
+ <tr><td style="padding-right:1em">* \ud14d\uc2a4\ud2b8 </td><td>\ubaa9\ub85d </td><td style="padding-left:3em;padding-right:1em">http://naver.com/ </td><td>\ub9c1\ud06c</td></tr>\
+ <tr><td style="padding-right:1em">1. \ud14d\uc2a4\ud2b8 </td><td>\uc5f4\uac70\ub41c \ubaa9\ub85d </td><td style="padding-left:3em;padding-right:1em">{link:\ub124\uc774\ubc84|http://naver.com}</td><td>\uba85\uba85\ub41c \ub9c1\ud06c</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">\uae30\ubd80\ud558\uae30</a> to {1}!<br>(\uadf8\ub9ac\uace0 \uad11\uace0\ub97c \uc81c\uac70 \ud558\uc2ed\uc2dc\uc624.)
+main.nowplaying = \ud604\uc7ac \uc7ac\uc0dd
+main.lyrics = \uac00\uc0ac
+main.minutesago = \ubd84 \uc804
+main.chat = \ucc44\ud305
+main.message = \uba54\uc2dc\uc9c0 \uc4f0\uae30
+main.clearchat = \uba54\uc2dc\uc9c0 \uc9c0\uc6b0\uae30
+
+# rating.jsp
+rating.rating = \ud3c9\uc810
+rating.clearrating = \ud3c9\uc810 \uc9c0\uc6b0\uae30
+
+# coverArt.jsp
+coverart.change = \ubcc0\uacbd
+coverart.zoom = \ud655\ub300
+
+# allmusic.jsp
+allmusic.text = <em>{0}</em> allmusic.com \uc5d0\uc11c \uc568\ubc94\uc815\ubcf4\ub97c \ucc3e\uace0 \uc788\uc2b5\ub2c8\ub2e4. \uae30\ub2e4\ub824 \uc8fc\uc2ed\uc2dc\uc624.
+
+# changeCoverArt.jsp
+changecoverart.title = \uc568\ubc94\uc544\ud2b8 \ubcc0\uacbd
+changecoverart.address = \uc544\ub2c8\uba74 \uc568\ubc94\uc544\ud2b8 \uc8fc\uc18c\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624.(URL)
+changecoverart.artist = \uc544\ud2f0\uc2a4\ud2b8
+changecoverart.album = \uc568\ubc94
+changecoverart.search = \uad6c\uae00 \uc774\ubbf8\uc9c0 \uac80\uc0c9
+changecoverart.wait = \uae30\ub2e4\ub824 \uc8fc\uc2ed\uc2dc\uc624...
+changecoverart.success = \uc774\ubbf8\uc9c0\ub97c \uc131\uacf5\uc801\uc73c\ub85c \ub2e4\uc6b4\ub85c\ub4dc \ud558\uc600\uc2b5\ub2c8\ub2e4.
+changecoverart.error = \uc774\ubbf8\uc9c0 \ub2e4\uc6b4\ub85c\ub4dc\uac00 \uc2e4\ud328 \ud558\uc600\uc2b5\ub2c8\ub2e4.
+changecoverart.noimagesfound = \uc774\ubbf8\uc9c0\ub97c \ucc3e\uc9c0 \ubabb\ud558\uc600\uc2b5\ub2c8\ub2e4.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = <br><b>"{0}"</b>: \uc568\ubc94\uc544\ud2b8 \ubcc0\uacbd\uc774 \uc2e4\ud328 \ud588\uc2b5\ub2c8\ub2e4.
+
+# editTags.jsp
+edittags.title = \ud0dc\uadf8\uc218\uc815
+edittags.file = \ud30c\uc77c
+edittags.track = \ud2b8\ub799
+edittags.songtitle = \ub178\ub798 \uc81c\ubaa9
+edittags.artist = \uc544\ud2f0\uc2a4\ud2b8
+edittags.album = \uc568\ubc94
+edittags.year = \uc5f0\ub3c4
+edittags.genre = \uc7a5\ub974
+edittags.status = \uc0c1\ud0dc
+edittags.suggest = \uc81c\uc548
+edittags.reset = \ucd08\uae30\ud654
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = \uc124\uc815
+edittags.working = \uc801\uc6a9 \uc911
+edittags.updated = \uc801\uc6a9\ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+edittags.skipped = \uac74\ub108\ub6f0\uae30
+edittags.error = \uc624\ub958
+
+# donate.jsp
+donate.title = \uae30\ubd80\ud558\uae30
+donate.invalidlicense = \ub77c\uc774\uc13c\uc2a4\ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.
+donate.amount = {0} \uae30\ubd80\ud558\uae30
+
+# podcastReceiver.jsp
+podcastreceiver.title = \ud31f\uce90\uc2a4\ud2b8 \uc218\uc2e0\uae30
+podcastreceiver.expandall = \uc5d0\ud53c\uc18c\ub4dc \ubcf4\uae30
+podcastreceiver.collapseall = \uc5d0\ud53c\uc18c\ub4dc \uc228\uae30\uae30
+podcastreceiver.status.new = \uc0c8\ub85c\uc6b4
+podcastreceiver.status.downloading = \ub2e4\uc6b4\ub85c\ub4dc \uc911
+podcastreceiver.status.completed = \uc644\ub8cc
+podcastreceiver.status.error = \uc624\ub958
+podcastreceiver.status.deleted = \uc0ad\uc81c\ud558\uae30
+podcastreceiver.status.skipped = \uac74\ub108\ub6f0\uae30
+podcastreceiver.downloadselected= \uc120\ud0dd \ub2e4\uc6b4\ub85c\ub4dc
+podcastreceiver.deleteselected= \uc120\ud0dd \uc0ad\uc81c
+podcastreceiver.confirmdelete= \uc120\ud0dd\ud55c \ud31f\uce90\uc2a4\ud2b8\ub97c \uc0ad\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?
+podcastreceiver.check = \uc0c8\ub85c\uc6b4 \uc5d0\ud53c\uc18c\ub4dc \ud655\uc778
+podcastreceiver.refresh = \uc0c8\ub85c\uace0\uce68
+podcastreceiver.settings = \ud31f\uce90\uc2a4\ud2b8 \uc124\uc815
+podcastreceiver.subscribe = \ud31f\uce90\uc2a4\ud2b8 \uad6c\ub3c5\ud558\uae30
+
+# lyrics.jsp
+lyrics.title = \uac00\uc0ac
+lyrics.artist = \uc544\ud2f0\uc2a4\ud2b8
+lyrics.song = \ub178\ub798
+lyrics.search = \uac80\uc0c9
+lyrics.wait = \uac00\uc0ac \uac80\uc0c9 \uc911...
+lyrics.courtesy = (\uac00\uc0ac\ub294 <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>\uc5d0\uc11c \uc81c\uacf5\ud569\ub2c8\ub2e4.)
+lyrics.nolyricsfound = \uac80\uc0c9\ub41c \uac00\uc0ac\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.
+
+# helpPopup.jsp
+helppopup.title = {0} \ub3c4\uc6c0\ub9d0
+helppopup.cover.title = \uc568\ubc94\uc544\ud2b8 \ud06c\uae30
+helppopup.cover.text = <p>\ub2f9\uc2e0\uc740 \uae30\ub2a5\uc744 \uc644\uc804\ud788 \ud574\uc81c\ud558\ub294 \uc635\uc158\uacfc \ud568\uaed8 \ud45c\uc2dc\ud560 \uc568\ubc94\uc544\ud2b8\uc758 \ud06c\uae30\ub97c \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.</p>
+helppopup.transcode.title = \ucd5c\ub300 \uc804\uc1a1\ub960
+helppopup.transcode.text = <p>\ub300\uc5ed\ud3ed\uc5d0 \uc81c\ud55c\uc774 \uc788\ub2e4\uba74, \ub2f9\uc2e0\uc740 \uc74c\uc545 \uc2a4\ud2b8\ub9ac\ubc0d\uc758 \ube44\ud2b8 \uc804\uc1a1\ub960\uc5d0 \uc81c\ud55c\uc744 \uc124\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \
+ \uc608\ub97c \ub4e4\uc5b4 \uc6d0\ub798 MP3 \ud30c\uc77c\uc740 256kbps(kilobits per second)\ub85c \uc778\ucf54\ub529 \ub418\uc5b4 \uc788\uace0 \ube44\ud2b8 \uc804\uc1a1\ub960 \ucd5c\ub300\uce58\ub97c 128kpbs\ub85c \uc124\uc815\ud558\ub294 \uacbd\uc6b0\
+ {0} \uc740(\ub294) 256kpbs \uc5d0\uc11c 128kbps \ub85c \uc790\ub3d9\uc73c\ub85c \ub9ac\uc0d8\ud50c\ub9c1\ud560 \uac83\uc785\ub2c8\ub2e4.</p> \
+ <p> \uc774 \uc635\uc158\uc740 LAME \uc774 \uc124\uce58\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \uc740(\ub294) \uc624\ud508\uc18c\uc2a4 MP3 \uc778\ucf54\ub354 \uc785\ub2c8\ub2e4. \ub2f9\uc2e0\uc740 <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">\uc5ec\uae30</a> \uc5d0\uc11c \ub2e4\uc6b4\ub85c\ub4dc \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \
+ SUBSONIC_HOME/transcode \uc5d0 \uc124\uce58\ud558\uc2dc\uae30 \ubc14\ub78d\ub2c8\ub2e4.</p>
+helppopup.playlistfolder.title = \uc7ac\uc0dd\ubaa9\ub85d \uc800\uc7a5\uc18c
+helppopup.playlistfolder.text = <p>\uc7ac\uc0dd\ubaa9\ub85d\uc774 \uc704\uce58\ud55c \uc800\uc7a5\uc18c\ub97c \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.</p>
+helppopup.musicmask.title = \uc74c\uc545\ud30c\uc77c \uc885\ub958
+helppopup.musicmask.text = <p>\uc74c\uc545 \ud30c\uc77c\uc758 \ud0c0\uc785\uc744 \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.</p>
+helppopup.videomask.title = \ub3d9\uc601\uc0c1\ud30c\uc77c \uc885\ub958
+helppopup.videomask.text = <p>\ub3d9\uc601\uc0c1 \ud30c\uc77c\uc758 \ud0c0\uc785\uc744 \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.</p>
+helppopup.coverartmask.title = \uc568\ubc94\uc544\ud2b8 \uc885\ub958
+helppopup.coverartmask.text = <p>\uc74c\uc545 \uc800\uc7a5\uc18c\ub97c \ud0d0\uc0c9\ud560 \ub54c \ud45c\uc2dc\ud560 \uc568\ubc94 \uc544\ud2b8\uc758 \uc885\ub958\ub97c \uc9c0\uc815\ud558\uc2ed\uc2dc\uc624.</p>
+helppopup.downsamplecommand.title = downsampling \uba85\ub839\uc5b4
+helppopup.downsamplecommand.text = <p>\ub0ae\uc740 \ube44\ud2b8\uc804\uc1a1\ub960\ub85c downsampling \ud560 \ub54c, \uc2e4\ud589\ud560 \uba85\ub839\uc5b4\ub97c \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.</p>\
+ <p>(%s = \ub0b4\ub824 \ubc1b\uc744 \ud30c\uc77c, %b = \uc7ac\uc0dd\uae30\uc758 \ucd5c\ub300 \ube44\ud2b8 \uc804\uc1a1\ub960, %t = \uc81c\ubaa9, %a = \uc544\ud2f0\uc2a4\ud2b8, %l = \uc568\ubc94)</p>
+helppopup.index.title = \uc0c9\uc778
+helppopup.index.text = <p>Let's you specify how the index (located on the left side of the screen) should look like. Files and directories \
+ directly in the root music folder can be easily accessed using this index.</p> \
+ <p>The specification is a space-separated list of index entries. Normally, each entry is just a single character, \
+ but you may also specify multiple characters. For instance, the entry <em>The</em> will link to all files and \
+ folders starting with "The".</p> \
+ <p>You may also create an entry using a group of index characters in paranthesis. For instance, the entry \
+ <em>A-E(ABCDE)</em> will display as <em>A-E</em> and link to all files and folders starting with either \
+ A, B, C, D or E. This may be useful for grouping less-frequently used characters (such and X, Y and Z), or \
+ for grouping accented characters (such as A, \u00c0 and \u00c1)</p> \
+ <p>Files and folders that are not covered by an index entry will be placed under the index entry "#".</p>
+helppopup.ignoredarticles.title = \ubb34\uc2dc\ud560 \uc811\ub450\uc0ac
+helppopup.ignoredarticles.text = <p>\uc0c9\uc778 \uc791\uc131\uc2dc \ubb34\uc2dc\ud560 \uc811\ub450\uc0ac\uc758 \ubaa9\ub85d\uc744 \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. (\uc608: "The")</p>
+helppopup.shortcuts.title = \ubc14\ub85c\uac00\uae30
+helppopup.shortcuts.text = <p>\ucd5c\uc0c1\uc704 \uc800\uc7a5\uc18c\uc758 \uacf5\ubc31\uc73c\ub85c \uad6c\ubd84\ub41c \ubaa9\ub85d\uc5d0 \ubc14\ub85c\uac00\uae30\ub97c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub530\uc634\ud45c\ub97c \uc774\uc6a9\ud558\uc5ec \uadf8\ub8f9\uc744 \uad6c\ubd84\ud569\ub2c8\ub2e4. \uc608\ub97c \ub4e4\uc5b4:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = \uc5b8\uc5b4
+helppopup.language.text = <p>\uc0ac\uc6a9\ud560 \uc5b8\uc5b4\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.</p>
+helppopup.visibility.title = \ubcf4\uc5ec\uc904 \uac83
+helppopup.visibility.text = <p>\ubfd0\ub9cc \uc544\ub2c8\ub77c \uc81c\ubaa9 \uc790\ub974\uae30\ub97c \uc774\uc6a9\ud558\uc5ec \uac01\uac01\uc758 \ub178\ub798\uc5d0 \ub300\ud574\uc11c \ud45c\uc2dc\ud574\uc57c \ud558\ub294 \ub0b4\uc6a9\uc744 \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \
+ \uc774\uac83\uc740 \ub178\ub798 \uc81c\ubaa9, \uc568\ubc94 \ubc0f \uc544\ud2f0\uc2a4\ud2b8\uc5d0 \ud45c\uc2dc \uac00\ub2a5\ud55c \ubb38\uc790\uc758 \ucd5c\ub300 \uac1c\uc218\uc785\ub2c8\ub2e4.</p>
+helppopup.partymode.title = \ud30c\ud2f0\ubaa8\ub4dc
+helppopup.partymode.text = <p>\ud30c\ud2f0 \ubaa8\ub4dc\uac00 \ud65c\uc131\ud654 \ub41c \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc778\ud130\ud398\uc774\uc2a4\uac00 \ub2e8\uc21c\ud558\uace0 \uc0ac\uc6a9\uacbd\ud5d8\uc774 \uc801\uc740 \uc0ac\uc6a9\uc790\ub97c \uc704\ud574 \ub3d9\uc791\ud558\uac8c \ub429\ub2c8\ub2e4. \
+ \ud2b9\ud788 \uc758\ub3c4\ud558\uc9c0 \uc54a\uac8c \uc7ac\uc0dd\ubaa9\ub85d\uc744 \ub4e3\ub294 \uac83\uc744 \ud53c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.</p>
+helppopup.theme.title = \ud14c\ub9c8
+helppopup.theme.text = <p>\uc0ac\uc6a9\ud560 \ud14c\ub9c8\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624. \ud14c\ub9c8\ub294 \ubaa8\uc591\uacfc \uc0c9\uc0c1, \uae00\uaf34, \uc774\ubbf8\uc9c0 \ub4f1\uc5d0\uc11c {0} \uc758 \ub290\ub08c\uc744 \uc815\uc758\ud569\ub2c8\ub2e4.</p>
+helppopup.welcomemessage.title = \ud658\uc601 \uba54\uc2dc\uc9c0
+helppopup.welcomemessage.text = <p>\ud648\ud398\uc774\uc9c0\uc5d0 \ud45c\uc2dc\ub418\ub294 \uba54\uc2dc\uc9c0\uc785\ub2c8\ub2e4.</p>
+helppopup.loginmessage.title = \ub85c\uadf8\uc778 \uba54\uc2dc\uc9c0
+helppopup.loginmessage.text = <p>\ub85c\uadf8\uc778 \ud398\uc774\uc9c0\uc5d0 \ud45c\uc2dc\ub418\ub294 \uba54\uc2dc\uc9c0\uc785\ub2c8\ub2e4.</p>
+helppopup.coverartlimit.title = \uc568\ubc94\uc544\ud2b8 \uc81c\ud55c
+helppopup.coverartlimit.text = <p>\ud55c \ud654\uba74\uc5d0 \ud45c\uc2dc\ud560 \uc568\ubc94 \uc544\ud2b8\uc758 \ucd5c\ub300 \uac1c\uc218\uc785\ub2c8\ub2e4.</p>
+helppopup.downloadlimit.title = \ub2e4\uc6b4\ub85c\ub4dc \uc81c\ud55c
+helppopup.downloadlimit.text = <p>\ud30c\uc77c \ub2e4\uc6b4\ub85c\ub4dc\uc5d0 \uc0ac\uc6a9\ub418\ub294 \ub300\uc5ed\ud3ed\uc758 \uc0c1\ud55c\uc120\uc785\ub2c8\ub2e4.</p>
+helppopup.uploadlimit.title = \uc5c5\ub85c\ub4dc \uc81c\ud55c
+helppopup.uploadlimit.text = <p>\ud30c\uc77c \uc5c5\ub85c\ub4dc\uc5d0 \uc0ac\uc6a9\ub418\ub294 \ub300\uc5ed\ud3ed\uc758 \uc0c1\ud55c\uc120\uc785\ub2c8\ub2e4.</p>
+helppopup.streamport.title = Non-SSL \uc2a4\ud2b8\ub9ac\ubc0d \ud3ec\ud2b8
+helppopup.streamport.text = <p>This option is only relevant if you use {0} on a server with SSL (HTTPS).</p><p>Some players \
+ (such as Winamp) don''t support streaming over SSL. Specify the port number for regular http (usually 80 \
+ or 4040) if you don''t want the streams to be transmitted over SSL. Note that the streams will not be encrypted.</p>
+helppopup.ldap.title = LDAP \uc778\uc99d
+helppopup.ldap.text = <p>Users can be authenticated by an external LDAP server (including Windows Active Directory). \
+ When LDAP-enabled users log on to {0}, the username and password are checked by the external server, not by {0} itself.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>The URL of the LDAP server. The protocol must be either <em>ldap://</em> or <em>ldaps://</em> \
+ (for LDAP over SSL). See <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">here</a> \
+ for a more detailed description.</p>
+helppopup.ldapsearchfilter.title = LDAP \uac80\uc0c9\ud544\ud130
+helppopup.ldapsearchfilter.text = <p>The filter expression used in the user search. This is an LDAP search filter \
+ (as defined in <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ The pattern "'{0'}" is replaced by the username, for instance: \
+ <ul>\
+ <li>(uid='{0'}) - this would search for a username match on the uid attribute.</li> \
+ <li>(sAMAccountName='{0'}) - typically used for authentication in Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>If the LDAP server doesn''t support anonymous binding you must specify the DN \
+ (<em>Distinguished Name</em>) and password of the LDAP user to use when binding.</p>
+helppopup.ldapautoshadowing.title = Automatically create LDAP users in {0}
+helppopup.ldapautoshadowing.text = <p>With this option selected, LDAP users don''t have to be manually created in {0} before logging on.</p> \
+ <p>NOTE! This means that any user with a valid LDAP username and password can log on to {0}, \
+ which may not be what you want.</p>
+helppopup.playername.title = \uc7ac\uc0dd\uae30 \uc774\ub984
+helppopup.playername.text = <p>"\uac70\uc2e4"\uc774\ub098 "\uc11c\uc7ac" \uac19\uc740 \uae30\uc5b5\ud558\uae30 \uc26c\uc6b4 \uc774\ub984\uc744 \uc7ac\uc0dd\uae30\uc5d0 \uc9c0\uc815\ud569\ub2c8\ub2e4.</p>
+helppopup.autocontrol.title = \uc7ac\uc0dd\uae30\ub97c \uc790\ub3d9\uc73c\ub85c \uc81c\uc5b4
+helppopup.autocontrol.text = <p>\uc774 \uc635\uc158\uc774 \uc120\ud0dd\ub418\uc5b4 \uc788\ub2e4\uba74, "\uc7ac\uc0dd" \ubc84\ud2bc\uc744 \ub204\ub974\uba74 \uc7ac\uc0dd\ubaa9\ub85d\uc5d0\uc11c {0} \uc774(\uac00) \uc790\ub3d9\uc73c\ub85c \uc7ac\uc0dd\uc744 \uc2dc\uc791\ud569\ub2c8\ub2e4. \
+ \uadf8\ub807\uc9c0 \uc54a\ub2e4\uba74, \ub2f9\uc2e0\uc740 \uc2dc\uc791\ud558\uace0 \uc7ac\uc0dd\uae30\ub97c \uc9c1\uc811 \uc5f0\uacb0\ud574\uc57c \ud569\ub2c8\ub2e4. </p>
+helppopup.dynamicip.title = \ub3d9\uc801 IP \uc8fc\uc18c
+helppopup.dynamicip.text = <p>\uc7ac\uc0dd\uae30\uac00 \uc815\uc801 IP \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0, \uc774 \uc635\uc158\uc744 \ud574\uc81c \ud569\ub2c8\ub2e4.</p>
+
+# wap/index.jsp
+wap.index.missing = \uac80\uc0c9\ub41c \uc74c\uc545\ud30c\uc77c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.
+wap.index.playlist = \uc7ac\uc0dd\ubaa9\ub85d
+wap.index.search = \uac80\uc0c9
+wap.index.settings = \uc124\uc815
+
+# wap/browse.jsp
+wap.browse.playone = \uc7ac\uc0dd
+wap.browse.playall = \ubaa8\ub450 \uc7ac\uc0dd
+wap.browse.addone = \ucd94\uac00
+wap.browse.addall = \ubaa8\ub450 \ucd94\uac00
+wap.browse.downloadone = \ub2e4\uc6b4\ub85c\ub4dc
+wap.browse.downloadall = \ubaa8\ub450 \ub2e4\uc6b4\ub85c\ub4dc
+
+# wap/playlist.jsp
+wap.playlist.title = \uc7ac\uc0dd\ubaa9\ub85d
+wap.playlist.noplayer = \uc5f0\uacb0\ub41c \uc7ac\uc0dd\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.
+wap.playlist.clear = \ube44\uc6b0\uae30
+wap.playlist.load = \ubd88\ub7ec\uc624\uae30
+wap.playlist.random = \ub79c\ub364
+wap.playlist.play = \uc7ac\uc0dd
+
+# wap/search.jsp
+wap.search.title = \uac80\uc0c9
+
+# wap/searchResult.jsp
+wap.searchresult.index = \uac80\uc0c9 \uc0c9\uc778\uc774 \uc0dd\uc131\ub418\uace0 \uc788\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \uc774\uc6a9\ud558\uc2ed\uc2dc\uc624.
+
+# wap/settings.jsp
+wap.settings.selectplayer = \uc7ac\uc0dd\uae30 \uc120\ud0dd
+wap.settings.allplayers = \ubaa8\ub450 \ No newline at end of file
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_mk.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_mk.properties
new file mode 100644
index 00000000..7b895f3a
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_mk.properties
@@ -0,0 +1,302 @@
+#
+# Macedonian localization.
+# Author: Stefan Ivanovski (sivanovski at hotmail.com)
+#
+
+common.home = \u0414\u043e\u043c\u0430
+common.back = \u041d\u0430\u0437\u0430\u0434
+common.help = \u041f\u043e\u043c\u043e\u0448
+common.play = \u0421\u0432\u0438\u0440\u0438
+common.add = \u0414\u043e\u0434\u0430\u0434\u0438
+common.download = \u0421\u0438\u043c\u043d\u0438
+common.close = \u0417\u0430\u0442\u0432\u043e\u0440\u0438
+common.refresh = \u041e\u0441\u0432\u0435\u0436\u0438
+common.next = \u0421\u043b\u0435\u0434\u043d\u043e
+common.previous = \u0421\u043b\u0435\u0434\u043d\u043e
+common.more = \u041f\u043e\u0432\u0435\u045c\u0435
+common.ok = OK
+common.save = \u0421\u043d\u0438\u043c\u0438
+common.create = \u041d\u0430\u043f\u0440\u0430\u0432\u0438
+common.delete = \u0418\u0437\u0431\u0440\u0438\u0448\u0438
+common.unknown = (\u043d\u0435\u043f\u043e\u0437\u043d\u0430\u0442\u043e)
+
+# top.jsp
+top.home = \u0414\u043e\u043c\u0430
+top.now_playing = \u0421\u0435\u0433\u0430&nbsp;\u0441\u0432\u0438\u0440\u0438
+top.settings = \u041f\u043e\u0434\u0435\u0441\u0443\u0432\u0430\u045a\u0430
+top.status = \u0421\u0442\u0430\u0442\u0443\u0441
+top.more = \u041f\u043e\u0432\u0435\u045c\u0435
+top.help = \u041f\u043e\u043c\u043e\u0448
+top.search = \u0411\u0430\u0440\u0430\u0458
+top.upgrade = <b>\u0412\u043d\u0438\u043c\u0430\u043d\u0438\u0435!</b> \u041d\u043e\u0432\u0430 \u0432\u0435\u0440\u0437\u0438\u0458\u0430 \u0435 \u0434\u043e\u0441\u0442\u0430\u043f\u043d\u0430.<br>\u0421\u0438\u043c\u043d\u0438 \u0433\u043e {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+top.missing = \u041d\u0435 \u0435 \u043f\u0440\u043e\u043d\u0430\u0458\u0434\u0435\u043d \u0444\u043e\u043b\u0434\u0435\u0440 \u0441\u043e \u043c\u0443\u0437\u0438\u043a\u0430. \u0412\u0435 \u043c\u043e\u043b\u0438\u043c\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0435 \u0433\u0438 \u043f\u043e\u0434\u0435\u0441\u0443\u0432\u0430\u045a\u0430\u0442\u0430.
+
+# left.jsp
+left.statistics = {0}&nbsp;\u0430\u0440\u0442\u0438\u0441\u0442\u0438<br>\
+ {1}&nbsp;\u0430\u043b\u0431\u0443\u043c\u0438<br>\
+ {2}&nbsp;\u043f\u0435\u0441\u043d\u0438<br>\
+ \u0412\u043a\u0443\u043f\u043d\u043e {3} (&#126; {4} \u0441\u0430\u0430\u0442\u0438)
+left.radio = \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0422\u0412/\u0420\u0430\u0434\u0438\u043e
+left.allfolders = \u0421\u0438\u0442\u0435 \u0444\u043e\u043b\u0434\u0435\u0440\u0438
+
+# playlist.jsp
+playlist.stop = \u0421\u0442\u043e\u043f
+playlist.start = \u0421\u0442\u0430\u0440\u0442
+playlist.clear = \u0411\u0440\u0438\u0448\u0438
+playlist.shuffle = \u041c\u0435\u0448\u0430\u0458
+playlist.repeat_on = \u041f\u043e\u0432\u0442\u043e\u0440\u0443\u0432\u0430\u045a\u0435 \u0432\u043a\u043b\u0443\u0447\u0435\u043d\u043e
+playlist.repeat_off = \u041f\u043e\u0432\u0442\u043e\u0440\u0443\u0432\u0430\u045a\u0435 \u0438\u0441\u043a\u043b\u0443\u0447\u0435\u043d\u043e
+playlist.undo = \u041f\u043e\u043d\u0438\u0448\u0442\u0438
+playlist.load = \u0412\u0447\u0438\u0442\u0430\u0458
+playlist.save = \u0421\u043d\u0438\u043c\u0438
+playlist.remove = \u0418\u0437\u0431\u0440\u0438\u0448\u0438
+playlist.up = \u0413\u043e\u0440\u0435
+playlist.down = \u0414\u043e\u043b\u0435
+playlist.empty = \u041f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0430\u0442\u0430 \u0435 \u043f\u0440\u0430\u0437\u043d\u0430
+
+# status.jsp
+status.title = \u0421\u0442\u0430\u0442\u0443\u0441
+status.player = \u041f\u043b\u0435\u0435\u0440
+status.user = \u041a\u043e\u0440\u0438\u0441\u043d\u0438\u043a
+status.current = \u041c\u043e\u043c\u0435\u043d\u0442\u0430\u043b\u0435\u043d \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442
+status.transmitted = \u041f\u0440\u0435\u043d\u0435\u0441\u0435\u043d\u0438
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = \u0411\u0430\u0440\u0430\u0458
+search.search = \u0411\u0430\u0440\u0430\u0458
+search.index = \u0418\u043d\u0434\u0435\u043a\u0441\u043e\u0442 \u0437\u0430 \u043f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430\u045a\u0435 \u0441\u0435 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430. \u0412\u0435 \u043c\u043e\u043b\u0438\u043c\u0435 \u043e\u0431\u0438\u0434\u0435\u0442\u0435 \u0441\u0435 \u043f\u043e\u0434\u043e\u0446\u043d\u0430.
+search.hits.none = \u041d\u0435 \u0435 \u043f\u0440\u043e\u043d\u0430\u0458\u0434\u0435\u043d \u0437\u0430\u043f\u0438\u0441.
+
+# home.jsp
+home.random.title = \u0420\u0430\u043d\u0434\u043e\u043c
+home.newest.title = \u041d\u0430\u0458\u043d\u043e\u0432\u0438
+home.highest.title = \u041d\u0430\u0458\u0432\u0438\u0441\u043e\u043a\u043e \u043a\u043e\u0442\u0438\u0440\u0430\u043d\u0438
+home.frequent.title = \u041d\u0430\u0458\u0447\u0435\u0441\u0442\u043e \u0432\u0440\u0442\u0435\u043d\u0438
+home.recent.title = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438 \u043f\u0435\u0441\u043d\u0438
+home.random.text = \u0420\u0430\u043d\u0434\u043e\u043c \u0430\u043b\u0431\u0443\u043c
+home.newest.text = \u041f\u043e\u0441\u043b\u0435\u0434\u0435\u043d \u0434\u043e\u0434\u0430\u0434\u0435\u043d \u0438\u043b\u0438 \u043c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d \u0430\u043b\u0431\u0443\u043c
+home.highest.text = \u041d\u0430\u0458\u0432\u0438\u0441\u043e\u043a\u043e \u0440\u0430\u043d\u0433\u0438\u0440\u0430\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.frequent.text = \u0410\u043b\u0431\u0443\u043c\u0438 \u043a\u043e\u0438 \u043d\u0430\u0458\u0447\u0435\u0441\u0442\u043e \u0441\u0435 \u0432\u0440\u0442\u0430\u0442
+home.recent.text = \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438 \u043f\u0440\u0435\u0441\u043b\u0443\u0448\u0430\u043d\u0438 \u0430\u043b\u0431\u0443\u043c\u0438
+home.scan = \u041c\u0443\u0437\u0438\u0447\u043a\u0438\u043e\u0442 \u0444\u043e\u043b\u0434\u0435\u0440 \u0441\u0435 \u043f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430. \u0421\u0438\u0442\u0435 \u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043d\u0435 \u0441\u0435 \u0434\u043e\u0441\u0442\u0430\u043f\u043d\u0438.
+home.listsize = \u041f\u0440\u0438\u043a\u0430\u0436\u0438 {0} \u0430\u043b\u0431\u0443\u043c\u0438
+home.albums = \u0410\u043b\u0431\u0443\u043c\u0438 {0} - {1}
+home.playcount = \u0421\u0432\u0438\u0440\u0435\u043b {0} \u043f\u0430\u0442\u0438
+home.lastplayed = \u0421\u0432\u0438\u0440\u0435\u043b {0}
+home.created = \u0418\u0437\u043c\u0435\u043d\u0435\u0442 {0}
+
+# more.jsp
+more.title = \u041f\u043e\u0432\u0435\u045c\u0435
+more.random.title = \u0420\u0430\u043d\u0434\u043e\u043c \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0430
+more.random.text = \u041d\u0430\u043f\u0440\u0430\u0432\u0438 \u0440\u0430\u043d\u0434\u043e\u043c \u043b\u0438\u0441\u0442\u0430 \u0441\u043e
+more.random.songs = {0} \u043f\u0435\u0441\u043d\u0438
+more.random.ok = \u0412\u043e \u0440\u0435\u0434
+more.mobile.title = \u041c\u043e\u0431\u0438\u043b\u0435\u043d \u0442\u0435\u043b\u0435\u0444\u043e\u043d
+more.mobile.text = <p>\u041c\u043e\u0436\u0435\u0448 \u0434\u0430 \u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0430\u0448 \u0421\u0443\u0431\u0441\u043e\u043d\u0438\u043a \u043f\u0440\u0435\u043a\u0443 \u043c\u043e\u0431\u0438\u043b\u0435\u043d \u0442\u0435\u043b\u0435\u0444\u043e\u043d \u0438\u043b\u0438 PDA \u043a\u043e\u0458 \u043f\u043e\u0434\u0434\u0440\u0436\u0443\u0432\u0430 WAP. <br> \
+ \u0415\u0434\u043d\u043e\u0441\u0442\u0430\u0432\u043d\u043e \u0441\u043b\u0435\u0434\u0438 \u0433\u043e \u043b\u0438\u043d\u043a\u043e\u0442 \u043f\u0440\u0435\u043a\u0443 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043e\u0442/PDA <b>http://yourhostname/wap</b></p> \
+ <p>\u041e\u0432\u0430\u0430 \u043e\u043f\u0446\u0438\u0458\u0430 \u0431\u0430\u0440\u0430 \u0432\u0430\u0448\u0438\u043e\u0442 \u0421\u0443\u0431\u0441\u043e\u043d\u0438\u043a \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u0430\u043f\u0435\u043d \u043f\u0440\u0435\u043a\u0443 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442! </p>
+more.podcast.title = Podcast
+more.podcast.text = <p>\u0421\u043d\u0438\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0438 \u0441\u0435 \u0434\u043e\u0441\u0442\u0430\u043f\u043d\u0438 \u043a\u0430\u043a\u043e Podcast.<br>\
+ \u0418\u0441\u043a\u043e\u0440\u0438\u0441\u0442\u0438 \u0433\u043e \u043b\u0438\u043d\u043a\u043e\u0442 \u0437\u0430 Podcast: <b>http://yourhostname/podcast</b>, \
+ \u0438\u043b\u0438 <b><a href="podcast.view?suffix=.rss">\u043a\u043b\u0438\u043a\u043d\u0438 \u0442\u0443\u043a\u0430</a>.</b></p>
+more.upload.title = \u0414\u043e\u0434\u0430\u0434\u0438 \u043f\u043e\u0434\u0430\u0442\u043e\u0446\u0438
+more.upload.source = \u0421\u0435\u043b\u0435\u043a\u0442\u0438\u0440\u0430\u0458 \u043f\u043e\u0434\u0430\u0442\u043e\u043a
+more.upload.target = \u0412\u0447\u0438\u0442\u0430\u0458 \u0432\u043e
+more.upload.browse = \u0418\u0437\u0431\u0435\u0440\u0438
+more.upload.ok = \u0412\u043d\u0435\u0441\u0438
+more.upload.unzip = \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0441\u043a\u0438 \u043e\u0442\u043f\u0430\u043a\u0443\u0432\u0430\u0458 zip-\u043f\u043e\u0434\u0430\u0442\u043e\u043a.
+
+# upload.jsp
+upload.title = \u0412\u0447\u0438\u0442\u0443\u0432\u0430\u043c \u043f\u043e\u0434\u0430\u0442\u043e\u043a
+upload.success = \u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0432\u0447\u0438\u0442\u0430\u043d\u043e <b>{0}</b>
+upload.empty = \u041d\u0435\u043c\u0430 \u043f\u043e\u0434\u0430\u0442\u043e\u0446\u0438 \u0437\u0430 \u0432\u0447\u0438\u0442\u0443\u0432\u0430\u045a\u0435.
+upload.failed = \u0412\u0447\u0438\u0442\u0443\u0432\u0430\u045a\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: <br><b>"{0}"</b>
+upload.unzipped = \u041e\u0442\u043f\u0430\u043a\u0443\u0432\u0430\u043d\u043e {0}
+
+# help.jsp
+help.title = \u0417\u0430 {0}
+help.upgrade = <b>\u0411\u0435\u043b\u0435\u0448\u043a\u0430!</b> \u041d\u043e\u0432\u0430 \u0432\u0435\u0440\u0437\u0438\u0458\u0430 \u0435 \u0434\u043e\u0441\u0442\u0430\u043f\u043d\u0430. \u0421\u0438\u043c\u043d\u0438 \u0433\u043e {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+help.version.title = \u0412\u0435\u0440\u0437\u0438\u0458\u0430
+help.builddate.title = \u0414\u0430\u0442\u0430 \u043d\u0430 \u0438\u0437\u0434\u0430\u0432\u0430\u045a\u0435
+help.license.title = \u041b\u0438\u0446\u0435\u043d\u0446\u0430
+help.license.text = {0} \u0435 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u0435\u043d \u0441\u043e\u0444\u0442\u0432\u0435\u0440 \u043a\u043e\u0458 \u0441\u0435 \u0434\u0438\u0441\u0442\u0440\u0438\u0431\u0443\u0438\u0440\u0430 \u043f\u0440\u0435\u043a\u0443 <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>.
+help.homepage.title = \u0414\u043e\u043c\u0430\u0448\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0430
+help.forum.title = \u0424\u043e\u0440\u0443\u043c
+help.contact.title = \u041a\u043e\u043d\u0442\u0430\u043a\u0442
+help.contact.text = {0} \u0435 \u0438\u0437\u0440\u0430\u0431\u043e\u0442\u0435\u043d \u0438 \u0433\u043e \u043e\u0434\u0434\u0436\u0443\u0432\u0430 Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u0431\u0438\u043b\u043e \u043a\u0430\u043a\u0432\u0438 \u043f\u0440\u0430\u0448\u0430\u045a\u0430, \u0437\u0430\u0431\u0435\u043b\u0435\u0448\u043a\u0438 \u0438\u043b\u0438 \u0441\u0443\u0433\u0435\u0441\u0442\u0438\u0438 \u0437\u0430 \u043f\u043e\u0434\u043e\u0431\u0440\u0443\u0432\u0430\u045a\u0430, \u0432\u0435 \u043c\u043e\u043b\u0438\u043c\u0435 \u043f\u043e\u0441\u0435\u0442\u0435\u0442\u0435 \u043d\u0435 \u043d\u0430 \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic \u0424\u043e\u0440\u0443\u043c</a>.
+help.donate = {0} \u0435 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u0435\u043d, \u043d\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0434\u043e\u043f\u0440\u0438\u043d\u0435\u0441\u0435\u0442\u0435 \u0441\u043e \u0434\u043e\u043d\u0430\u0446\u0438\u0438 \u043e\u0434 \u043a\u043e\u0438 25 % \u043a\u0435 \u0431\u0438\u0434\u0430\u0442 \u043d\u0430\u043c\u0435\u043d\u0435\u0442\u0438 \u0437\u0430 \u0434\u043e\u0431\u0440\u043e\u0442\u0432\u043e\u0440\u043d\u0438 \u0446\u0435\u043b\u0438.
+help.log = \u041b\u043e\u0433
+
+# settingsHeader.jsp
+settingsheader.title = \u041f\u043e\u0434\u0435\u0441\u0443\u0432\u0430\u045a\u0430
+settingsheader.musicFolder = \u041c\u0443\u0437\u0438\u0447\u043a\u0438 \u0444\u043e\u043b\u0434\u0435\u0440\u0438
+settingsheader.internetRadio = \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0422\u0412/\u0420\u0430\u0434\u0438\u043e
+settingsheader.player = \u0421\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u0438
+settingsheader.user = \u041a\u043e\u0440\u0438\u0441\u043d\u0438\u0446\u0438
+settingsheader.search = \u0418\u043d\u0434\u0435\u043a\u0441 \u0437\u0430 \u043f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430\u045a\u0435
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \u0424\u043e\u043b\u0434\u0435\u0440 \u0441\u043e \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0438
+generalsettings.musicmask = \u041c\u0443\u0437\u0438\u0447\u043a\u0438 \u043c\u0430\u0441\u043a\u0438
+generalsettings.coverartmask = \u041c\u0430\u0441\u043a\u0438 \u043d\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0438
+generalsettings.index = \u0418\u043d\u0434\u0435\u043a\u0441
+generalsettings.ignoredarticles = \u0427\u043b\u0435\u043d\u043e\u0432\u0438 \u0437\u0430 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u045a\u0435
+generalsettings.welcomemessage = \u041f\u043e\u0440\u0430\u043a\u0430 \u0437\u0430 \u0434\u043e\u0431\u0440\u0435\u0434\u043e\u0458\u0434\u0435
+generalsettings.language = \u0408\u0430\u0437\u0438\u043a
+
+# advancedSettings.jsp
+advancedsettings.coverartlimit = \u041b\u0438\u043c\u0438\u0442 \u043d\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0438 <br><div class="detail">(0 = \u041d\u0435\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043e)</div>
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \u0424\u043e\u043b\u0434\u0435\u0440
+musicfoldersettings.name = \u0418\u043c\u0435
+musicfoldersettings.enabled = \u0410\u043a\u0442\u0438\u0432\u0435\u043d
+musicfoldersettings.nopath = \u041d\u0435 \u0435 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d \u0444\u043e\u043b\u0434\u0435\u0440.
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradiosettings.homepageurl = \u0414\u043e\u043c\u0430\u0448\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0430
+internetradiosettings.name = \u0418\u043c\u0435
+internetradiosettings.enabled = \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0458
+internetradiosettings.nourl = \u041d\u0435 \u0435 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u043e URL.
+internetradiosettings.noname = \u041d\u0435 \u0435 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u043e \u0438\u043c\u0435.
+
+# playerSettings.jsp
+playersettings.type = \u0422\u0438\u043f
+playersettings.lastseen = \u041f\u043e\u0441\u043b\u0435\u0434\u0435\u043d \u043f\u0430\u0442 \u0432\u0438\u0434\u0435\u043d
+playersettings.title = \u041f\u043e\u0434\u0435\u0441\u0443\u0432\u0430\u045a\u0430 \u0437\u0430
+playersettings.name = \u0418\u043c\u0435 \u043d\u0430 \u043f\u043b\u0435\u0435\u0440\u043e\u0442
+playersettings.coverartsize = \u0413\u043e\u043b\u0435\u043c\u0438\u043d\u0430 \u043d\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0430\u0442\u0430(Cover Art)
+playersettings.maxbitrate = \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0438\u0442\u0440\u0435\u0458\u0442
+playersettings.nolame = <em>Notice:</em> LAME \u043d\u0435 \u0435 \u0438\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u043e.<br>\u041a\u043b\u0438\u043a\u043d\u0438 \u043d\u0430 \u043f\u043e\u043c\u043e\u0448 \u0437\u0430 \u043f\u043e\u0432\u0435\u045c\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.
+playersettings.dynamicip = \u041f\u043b\u0435\u0435\u0440\u043e\u0442 \u0438\u043c\u0430 \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430
+playersettings.autocontrol = \u041a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0430\u0458 \u0433\u043e \u043f\u043b\u0435\u0435\u0440\u043e\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0441\u043a\u0438
+playersettings.ok = \u0421\u043d\u0438\u043c\u0438
+playersettings.forget = \u0418\u0437\u0431\u0440\u0438\u0448\u0438 \u043f\u043b\u0435\u0435\u0440
+playersettings.clone = \u041a\u043b\u043e\u043d\u0438\u0440\u0430\u0458 \u043f\u043b\u0435\u0435\u0440
+
+# userSettings.jsp
+usersettings.admin = \u041a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 \u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440
+usersettings.download = \u041d\u0430 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 \u043c\u0443 \u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u0441\u0438\u043c\u043d\u0443\u0432\u0430 \u043f\u043e\u0434\u0430\u0442\u043e\u0446\u0438.
+usersettings.upload = \u041d\u0430 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 \u043c\u0443 \u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u0434\u043e\u0434\u0430\u0432\u0430\u045a\u0435 \u043d\u0430 \u043f\u043e\u0434\u0430\u0442\u043e\u0446\u0438.
+usersettings.playlist= \u041d\u0430 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 \u043c\u0443 \u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u0431\u0440\u0438\u0448\u0435 \u0438 \u0434\u043e\u0434\u0430\u0432\u0430 \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0438.
+usersettings.comment= \u041d\u0430 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 \u043c\u0443 \u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u043a\u0440\u0435\u0438\u0440\u0430, \u0435\u0434\u0438\u0442\u0438\u0440\u0430 \u043a\u043e\u043c\u0435\u043d\u0442\u0430\u0440\u0438 \u0438 \u0440\u0430\u043d\u0433\u0438\u0440\u0430
+usersettings.username = \u041a\u043e\u0440\u0438\u0441\u043d\u0438\u0447\u043a\u043e \u0438\u043c\u0435
+usersettings.password = \u041b\u043e\u0437\u0438\u043d\u043a\u0430
+usersettings.newpassword = \u041d\u043e\u0432\u0430 \u043b\u043e\u0437\u0438\u043d\u043a\u0430
+usersettings.confirmpassword = \u041f\u043e\u0442\u0432\u0440\u0434\u0438 \u043b\u043e\u0437\u0438\u043d\u043a\u0430
+usersettings.delete = \u0411\u0440\u0438\u0448\u0438 \u0433\u043e \u043e\u0432\u043e\u0458 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a
+usersettings.nousername = \u041d\u0435\u0434\u043e\u0441\u0442\u0430\u0441\u0443\u0432\u0430 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u0447\u043a\u043e \u0438\u043c\u0435.
+usersettings.nopassword = \u041f\u043e\u0442\u0440\u0435\u0431\u043d\u0430 \u0435 \u043b\u043e\u0437\u0438\u043d\u043a\u0430.
+usersettings.wrongpassword = \u041b\u043e\u0437\u0438\u043d\u043a\u0430\u0442\u0430 \u043d\u0435 \u0441\u0435 \u0441\u043e\u0432\u043f\u0430\u0453\u0430.
+usersettings.ok = \u041b\u043e\u0437\u0438\u043d\u043a\u0430\u0430\u0442 \u043d\u0430 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 {0} \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0430.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = \u041d\u0438\u043a\u043e\u0433\u0430\u0448
+musicfoldersettings.interval.one = \u0421\u0435\u043a\u043e\u0458 \u0434\u0435\u043d
+musicfoldersettings.interval.many = \u0421\u0435\u043a\u043e\u0458 {0} \u0434\u0435\u043d
+musicfoldersettings.hour = \u0432\u043e {0}:00
+
+# main.jsp
+main.up = \u0413\u043e\u0440\u0435
+main.playall = \u0421\u0432\u0438\u0440\u0438 \u0441\u0435
+main.addall = \u0414\u043e\u0434\u0430\u0434\u0438 \u0441\u0435
+main.playcount = \u0421\u0432\u0438\u0440\u0435\u043b {0} \u043f\u0430\u0442\u0438.
+main.lastplayed = \u041f\u043e\u0441\u043b\u0435\u0434\u0435\u043d \u043f\u0430\u0442 \u0441\u0432\u0438\u0440\u0435\u043b \u043d\u0430 {0}.
+main.comment = \u041a\u043e\u043c\u0435\u043d\u0442\u0430\u0440
+
+# rating.jsp
+rating.rating = \u0420\u0435\u0458\u0442\u0438\u043d\u0433
+
+# allmusic.jsp
+allmusic.text = \u041f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430\u043c \u0430\u043b\u0431\u0443\u043c <em>{0}</em> \u043d\u0430 allmusic.com - \u0412\u0435 \u043c\u043e\u043b\u043e\u043c\u0430\u043c \u043f\u0440\u0438\u0447\u0435\u043a\u0430\u0458\u0442\u0435.
+
+# changeCoverArt.jsp
+changecoverart.title = \u0421\u043c\u0435\u043d\u0438 \u0438\u0437\u0433\u043b\u0435\u0434 \u043d\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0430 (Cover Art)
+changecoverart.address = \u0412\u043c\u0435\u0442\u043d\u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u0437\u0430 \u043a\u043e\u0432\u0435\u0440\u0442 \u0430\u0440\u0442
+changecoverart.artist = \u0410\u0440\u0442\u0438\u0441\u0442
+changecoverart.album = \u0410\u043b\u0431\u0443\u043c
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = \u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u043f\u0440\u043e\u043c\u0435\u043d\u0430 \u043d\u0430 \u043a\u043e\u0432\u0435\u0440\u0442 \u0430\u0440\u0442 art:<br><b>"{0}"</b>
+
+# helpPopup.jsp
+helppopup.title = {0} \u043f\u043e\u043c\u043e\u0448
+helppopup.cover.title = \u0413\u043e\u043b\u0435\u043c\u0438\u043d\u0430 \u043d\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0430\u0442\u0430
+helppopup.cover.text = <p>\u0422\u0438 \u043e\u0432\u043e\u0437\u043c\u043e\u0436\u0443\u0432\u0430 \u0434\u0430 \u0458\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0448 \u0433\u043e\u043b\u0435\u043c\u0438\u043d\u0430\u0442\u0430 \u043d\u0430 \u043f\u0440\u0438\u043a\u0430\u0436\u0430\u043d\u0430\u0442\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0430\u0442\u0430 \u0438 \u043e\u043f\u0446\u0438\u0458\u0430 \u0434\u0430 \u0441\u0435 \u0438\u0441\u043a\u043b\u0443\u0447\u0438.</p>
+helppopup.transcode.title = \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d bitrate
+helppopup.transcode.text = <p>If you have constrained bandwidth, you may set an upper limit for the bitrate of the music streams. \
+ For instance, if your original mp3 files are encoded using 256 Kbps (kilobits per second), setting max bitrate \
+ to 128 will make {0} automatically resample the music from 256 to 128 Kbps.</p> \
+ <p>This option requires that LAME is installed. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ is an open source mp3 encoder. You can <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">download it here</a>. Please make sure that the directory \
+ in which you install LAME is present in the PATH variable.</p> \
+ <p>Currently, resampling music to lower bitrates is only supported for mp3 files using constant bitrate encoding. \
+ (This is, however, the most common encoding).</p>
+helppopup.playlistfolder.title = \u0424\u043e\u043b\u0434\u0435\u0440 \u0441\u043e \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0438
+helppopup.playlistfolder.text = <p>\u0421\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0458 \u0444\u043e\u043b\u0434\u0435\u0440 \u043a\u0430\u0434\u0435 \u043a\u0435 \u0441\u0435 \u0447\u0443\u0432\u0430\u0430\u0442 \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0438\u0442\u0435.</p>
+helppopup.musicmask.title = \u043c\u0443\u0437\u0438\u0447\u043a\u0430 \u043c\u0430\u0441\u043a\u0430
+helppopup.musicmask.text = <p>\u0421\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0458 \u0442\u0438\u043f\u043e\u0432\u0438 \u043d\u0430 \u0435\u043a\u0441\u0442\u0435\u043d\u0437\u0438\u0438 \u043a\u043e\u0438 \u0441\u0430\u043a\u0430\u0448 \u0434\u0430 \u0441\u0435 \u043f\u0440\u0435\u043f\u043e\u0437\u043d\u0430\u0430\u0442 \u043a\u0430\u043a\u043e \u043c\u0443\u0437\u0438\u043a\u0430 \u0438\u043b\u0438 \u0432\u0438\u0434\u0435\u043e \u043f\u0440\u0438 \u043f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430\u045a\u0435.</p>
+helppopup.coverartmask.title = \u041c\u0430\u0441\u043a\u0430 \u043d\u0430 \u043e\u043f\u0430\u043a\u043e\u0432\u043a\u0430\u0442\u0430
+helppopup.coverartmask.text = <p>\u0422\u0438 \u043e\u0432\u043e\u0437\u043c\u043e\u0436\u0443\u0432\u0430 \u0434\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0448 \u0442\u0438\u043f \u043d\u0430 \u0444\u0430\u0458\u043b \u043a\u043e\u0438 \u043a\u0435 \u0431\u0438\u0434\u0435 \u043f\u0440\u0435\u043f\u043e\u0437\u043d\u0430\u0435\u043d \u043a\u0430\u043a\u043e \u043a\u043e\u0432\u0435\u0440\u0442 \u0430\u0440\u0442 \u043f\u0440\u0438 \u043f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430\u045a\u0435 \u043d\u0430 \u043c\u0443\u0437\u0438\u0447\u043a\u0438\u0442\u0435 \u0444\u043e\u043b\u0434\u0435\u0440\u0438.</p>
+helppopup.index.title = \u0418\u043d\u0434\u0435\u043a\u0441
+helppopup.index.text = <p>Let's you specify how the index (located at the top of the screen) should look like. Files and directories \
+ directly in the root music folder can be easily accessed using this index.</p> \
+ <p>The specification is a space-separated list of index entries. Normally, each entry is just a single character, \
+ but you may also specify multiple characters. For instance, the entry <em>The</em> will link to all files and \
+ folders starting with "The".</p> \
+ <p>You may also create an entry using a group of index characters in paranthesis. For instance, the entry \
+ <em>A-E(ABCDE)</em> will display as <em>A-E</em> and link to all files and folders starting with either \
+ A, B, C, D or E. This may be useful for grouping less-frequently used characters (such and X, Y and Z), or \
+ for grouping accented characters (such as A, \u00c0 and \u00c1)</p> \
+ <p>Files and folders that are not covered by an index entry will be placed under the index entry "#".</p>
+helppopup.ignoredarticles.title = \u0427\u043b\u0435\u043d\u043e\u0432\u0438 \u0437\u0430 \u0438\u0433\u043d\u043e\u043e\u0440\u0438\u0440\u0430\u045a\u0435
+helppopup.ignoredarticles.text = <p>\u0422\u0438 \u043e\u0432\u043e\u0437\u043c\u043e\u0436\u0443\u0432\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u045a\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430 \u043e\u0434 \u0447\u043b\u0435\u043d\u043e\u0432\u0438 (\u041a\u0430\u043a\u043e "The") \u043a\u043e\u0438 \u043a\u0435 \u0441\u0435 \u0438\u0433\u043d\u0438\u043e\u0440\u0438\u0440\u0430\u0430\u0442 \u043f\u0440\u0438 \u043a\u0440\u0435\u0438\u0440\u0430\u045a\u0435 \u043d\u0430 \u0438\u043d\u0434\u0435\u043a\u0441\u0438\u0442\u0435.</p>
+helppopup.language.title = \u0408\u0430\u0437\u0438\u043a
+helppopup.language.text = <p>\u0422\u0438 \u043e\u0432\u043e\u0437\u043c\u043e\u0436\u0443\u0432\u0430 \u0434\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0448 \u0458\u0430\u0437\u0438\u043a.</p>
+helppopup.welcomemessage.title = \u041f\u043e\u0440\u0430\u043a\u0430 \u0437\u0430 \u0434\u043e\u0431\u0440\u0435\u0434\u043e\u0458\u0434\u0435
+helppopup.welcomemessage.text = <p>\u041f\u043e\u0440\u0430\u043a\u0430 \u043a\u043e\u0458\u0430 \u0441\u0435 \u043f\u0440\u0438\u043a\u0430\u0436\u0443\u0432\u0430 \u043d\u0430 \u043f\u043e\u0447\u0435\u0442\u043d\u0430\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430.</p>
+helppopup.coverartlimit.title = \u041b\u0438\u043c\u0438\u0442 \u043d\u0430 \u043a\u043e\u0432\u0435\u0440\u0442 \u0430\u0440\u0442.
+helppopup.coverartlimit.text = <p>\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u0435\u043d \u0431\u0440\u043e\u0458 \u043d\u0430 \u043a\u043e\u0432\u0435\u0440\u0442 \u0430\u0440\u0442 \u043a\u043e\u0438 \u0441\u0435 \u043f\u0440\u0438\u043a\u0430\u0436\u0443\u0432\u0430 \u043d\u0430 \u0435\u0434\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430.</p>
+helppopup.playername.title = \u0418\u043c\u0435 \u043d\u0430 \u043f\u043b\u0435\u0435\u0440\u043e\u0442
+helppopup.playername.text = <p>\u0422\u0438 \u043e\u0432\u043e\u0437\u043c\u043e\u0436\u0443\u0432\u0430 \u0434\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0448 \u043b\u0435\u0441\u043d\u043e \u0437\u0430 \u043f\u043e\u043c\u043d\u0435\u045a\u0435 \u0438\u043c\u0435 \u043d\u0430 \u043f\u043b\u0435\u0435\u0440\u043e\u0442, \u043a\u0430\u043a\u043e "\u0440\u0430\u0431\u043e\u0442\u0430 \u0438\u043b\u0438 \u0434\u043d\u0435\u0432\u043d\u0430 \u0441\u043e\u0431\u0430".</p>
+helppopup.autocontrol.title = \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0430\u0458 \u0433\u043e \u043f\u043b\u0435\u0435\u0440\u043e\u0442 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0441\u043a\u0438
+helppopup.autocontrol.text = <p>\u0421\u043e \u043e\u0432\u0430\u0430 \u043e\u043f\u0446\u0438\u0458\u0430, {0} \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0441\u043a\u0438 \u043a\u0435 \u0433\u043e \u0441\u0442\u0430\u0440\u0442\u0443\u0432\u0430 \u043f\u043b\u0435\u0435\u0440\u043e\u0442 \u043a\u043e\u0433\u0430 \u043a\u0435 \u043a\u043b\u0438\u043a\u043d\u0435\u0448 \u043d\u0430 "\u0421\u0432\u0438\u0440\u0438"\
+ \u0432\u043e \u043f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0430\u0442\u0430. \u0412\u043e \u0441\u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e \u0441\u0430\u043c\u0438\u043e\u0442 \u0442\u0440\u0435\u0431\u0430 \u0434\u0430 \u0438\u0437\u0432\u0440\u0448\u0438\u0448 \u043a\u043e\u043d\u0435\u043a\u0446\u0438\u0458\u0430.</p>
+helppopup.dynamicip.title = \u0414\u0438\u043d\u0430\u043c\u0438\u0447\u043a\u0430 \u0418\u041f \u0430\u0434\u0440\u0435\u0441\u0430
+helppopup.dynamicip.text = <p>\u0418\u0441\u043a\u043b\u0443\u0447\u0438 \u0458\u0430 \u043e\u0432\u0430\u0430 \u043e\u043f\u0446\u0438\u0458\u0430 \u0434\u043e\u043a\u043e\u043b\u043a\u0443 \u043a\u043e\u0440\u0438\u0441\u043d\u0438\u043a\u043e\u0442 \u043a\u043e\u0440\u0438\u0441\u0442\u0438 \u0441\u0442\u0430\u0442\u0438\u0447\u043a\u0430 \u0418\u041f \u0430\u0434\u0440\u0435\u0441\u0430.</p>
+
+
+# wap/index.jsp
+wap.index.missing = \u041d\u0435 \u0441\u0435 \u043f\u0440\u043e\u043d\u0430\u0458\u0434\u0435\u043d\u0438 \u043f\u0435\u0441\u043d\u0438
+wap.index.playlist = \u041f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0430
+wap.index.search = \u0411\u0430\u0440\u0430\u0458
+wap.index.settings = \u041f\u043e\u0434\u0435\u0441\u0443\u0432\u0430\u045a\u0430
+
+# wap/browse.jsp
+wap.browse.playone = \u0421\u0432\u0438\u0440\u0438 \u043f\u0435\u0441\u043d\u0430
+wap.browse.playall = \u0421\u0432\u0438\u0440\u0438 \u0441\u0435
+wap.browse.addone = \u0414\u043e\u0434\u0430\u0434\u0438 \u043f\u0435\u0441\u043d\u0430
+wap.browse.addall = \u0414\u043e\u0434\u0430\u0434\u0438 \u0441\u0435
+
+# wap/playlist.jsp
+wap.playlist.title = \u041f\u043b\u0435\u0458\u043b\u0438\u0441\u0442\u0430
+wap.playlist.noplayer = \u041d\u0435\u043c\u0430 \u043f\u043e\u0432\u0440\u0437\u0430\u043d \u043f\u043b\u0435\u0435\u0440
+wap.playlist.clear = \u0418\u0441\u0447\u0438\u0441\u0442\u0438
+wap.playlist.load = \u0412\u0447\u0438\u0442\u0430\u0458
+
+# wap/search.jsp
+wap.search.title = \u0411\u0430\u0440\u0430\u0458
+
+# wap/searchResult.jsp
+wap.searchresult.index = \u0418\u043d\u0434\u0435\u043a\u0441\u043e\u0442 \u0437\u0430 \u043f\u0440\u0435\u0431\u0430\u0440\u0443\u0432\u0430\u045a\u0435 \u0441\u0435 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430. \u041e\u0431\u0438\u0434\u0435\u0442\u0435 \u0441\u0435 \u043f\u043e\u0434\u043e\u0446\u043d\u0430.
+
+# wap/settings.jsp
+wap.settings.selectplayer = \u0418\u0437\u0431\u0435\u0440\u0438 \u043f\u043b\u0435\u0435\u0440
+wap.settings.allplayers = \u0421\u0438\u0442\u0435
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_nl.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_nl.properties
new file mode 100644
index 00000000..0ee8b3b3
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_nl.properties
@@ -0,0 +1,759 @@
+#
+# Dutch localization.
+# Author: Sindre Mehus
+# Author: Ronald Knot (rokno at planet.nl)
+# Corrected : Sander van der Grind / Jeremy terpstra
+# New configlines : Jeremy terpstra
+# New configlines : Muiz
+# 09-March-2012 Updated: Toolman
+
+common.home = Home
+common.back = Terug
+common.help = Help
+common.play = Speel af
+common.add = Voeg toe
+common.download = Download
+common.close = Sluiten
+common.refresh = Ververs
+common.next = Volgende
+common.previous = Vorige
+common.more = Meer
+common.ok = OK
+common.cancel = Annuleer
+common.save = Opslaan
+common.create = Maak
+common.delete = Verwijder
+common.unknown = (Onbekend)
+common.default = (Standaard)
+
+# login.jsp
+login.username = Gebruikersnaam
+login.password = Wachtwoord
+login.login = Inloggen
+login.remember = Onthoud mij.
+login.logout = Je bent uitgelogd.
+login.error = Gebruikersnaam of wachtwoord onbekend..
+login.insecure = {0} is niet beveiligd. Log in met gebruikersnaam en<br>wachtwoord "admin", of klik <a href="login.view?gebruiker=admin&amp;wachtwoord=admin">hier</a>. en verander het wachtwoord onmiddelijk.
+login.recover = Wachtwoord vergeten?
+
+# recover.jsp
+recover.title = Wachtwoord vergeten?
+recover.text = Vul jouw <b>gebruikersnaam</b> of <b>email adres</b> hieronder in om het wachtwoord te herstellen, .
+recover.username = Gebruikersnaam of e-mailadres
+recover.send = Mail mij het nieuwe wachtwoord.
+recover.success = Je wachtwoord was opnieuw ingesteld en gemaild naar {0}.
+recover.error.usernotfound = Sorry, gebruiker is niet gevonden.
+recover.error.noemail = Sorry, er is geen e-mailadres bekend voor deze gebruiker.
+recover.error.sendfailed = E-mail versturen is mislukt, probeer het later nog eens..
+
+# accessDenied.jsp
+accessDenied.title = Toegang geweigerd.
+accessDenied.text = Je bent niet bevoegd om dit te doen.
+
+# top.jsp
+top.home = Home
+top.now_playing = Speelt nu
+top.settings = Instellingen
+top.status = Status
+top.podcast = Podcast
+top.more = Meer
+top.help = Over...
+top.search = Zoeken
+top.upgrade = <b>Note!</b> Er is een nieuwe versie beschikbaar.<br>Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+top.missing = Geen mediamappen gevonden. Verander de Instellingen.
+top.logout = Uitloggen {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artiesten<br>\
+ {1}&nbsp;albums<br>\
+ {2}&nbsp;songs<br>\
+ {3}<br>\
+ {4}&nbsp;uren
+left.shortcut = Snelkoppeling
+left.radio = Internet TV/radio
+left.allfolders = Alle mappen.
+
+# playlist.jsp
+playlist.stop = Stop
+playlist.start = Speel af.
+playlist.confirmclear = Speelllijst leegmaken?
+playlist.clear = Maak leeg
+playlist.shuffle = Willekeurige volgorde
+playlist.repeat_on = Herhalen is aan
+playlist.repeat_off = Herhalen is uit
+playlist.undo = Ongedaan maken.
+playlist.settings = Instellingen
+playlist.more = Meer acties...
+playlist.more.playlist = Speellijst
+playlist.more.sortbytrack = Sorteer op nummer
+playlist.more.sortbyartist = Sorteer op artiest
+playlist.more.sortbyalbum = Sorteer op album
+playlist.more.selection = Geselecteerde songs
+playlist.more.selectall = Selecteer alles
+playlist.more.selectnone = Selectie opheffen
+playlist.getflash = Download Flash afspeler
+playlist.load = Laad
+playlist.save = Sla op
+playlist.append = Voeg toe aan speellijst
+playlist.remove = Verwijder
+playlist.up = Omhoog
+playlist.down = Omlaag
+playlist.empty = Speellijst is leeg
+
+# videoPlayer.jsp
+videoPlayer.getflash = Installeer Flash afspeler.
+videoPlayer.popout = Open in een nieuw venster.
+
+# status.jsp
+status.title = Status
+status.type = Type
+status.stream = Stream
+status.download = Download
+status.upload = Upload
+status.player = Afspeler
+status.user = Gebruiker
+status.current = Huidig bestand
+status.transmitted = Uitgezonden
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Zoeken
+search.query = Artiest, album of songtitel
+search.search = Zoeken
+search.index = De zoekindex wordt nu gemaakt. Probeer het later opnieuw.
+search.hits.none = Geen overeenkomsten gevonden..
+search.hits.more = Meer
+search.hits.artists = Artiesten
+search.hits.albums = Albums
+search.hits.songs = Songs
+
+# gettingStarted.jsp
+gettingStarted.title = Snelgids
+gettingStarted.text = <p>Welkom bij Subsonic!. De configuratie van Subsonic is eenvoudig, loop de volgende basisstappen even door.<br> \
+ Klik op de "Home" knop in de werkbalk hierboven om naar dit scherm terug te keren.</p> \
+ <p>Raadpleeg de <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>"Getting started"</b></a> gids voor meer informatie.</p>
+gettingStarted.step1.title = Verander administrator wachtwoord.
+gettingStarted.step1.text = Beveilig de server door het standaardwachtwoord van het administrator account te wijzigen. \
+ Tevens kun je hier nieuwe gebruikersaccounts met verschillende rechten aanmaken.
+gettingStarted.step2.title = Stel de mediamappen in.
+gettingStarted.step2.text = Vertel Subsonic waar jouw muziek en videos staan.
+gettingStarted.step3.title = Configureer netwerk Instellingen.
+gettingStarted.step3.text = Enkele nuttige instellingen als je jouw muziek via internet wilt beluisteren, \
+ of wilt delen met familie en vrienden. Registreer meteen je persoonlijke <b><em>jouwname</em>.subsonic.org</b> \
+ adres.
+gettingStarted.hide = Dit niet meer laten zien.
+gettingStarted.hidealert = Ga naar instellingen > Algemeen, om dit scherm weer te tonen.
+
+# home.jsp
+home.random.title = Willekeurig
+home.newest.title = Recentelijk toegevoegd
+home.highest.title = Hoogst gewaardeerd
+home.frequent.title = Meest afgespeeld
+home.recent.title = Recentelijk afgespeeld
+home.users.title = Gebruikers
+home.random.text = Willekeurige albums
+home.renew.text = Vernieuw albums
+home.newest.text = Recentelijk toegevoegde albums
+home.highest.text = Hoogst gewaardeerde albums
+home.frequent.text = Meest afgespeelde albums
+home.recent.text = Recentelijk afgespeelde albums
+home.users.text = Gebruikersstatistieken
+home.scan = De mediamappen worden nu gescanned. Sommige functies zijn niet beschikbaar.
+home.listsize = {0} albums per pagina
+home.albums = Albums {0} - {1}
+home.playcount = Afgespeeld {0} songs
+home.lastplayed = Afgespeeld {0}
+home.created = Gemaakt {0}
+home.chart.total = Totaal (MB)
+home.chart.stream = Gestreamed (MB)
+home.chart.download = Gedownload (MB)
+home.chart.upload = Ge-upload (MB)
+
+# more.jsp
+more.title = Meer
+more.random.title = Willekeurige speellijst
+more.random.text = Maak willekeurige speellijst met
+more.random.songs = {0} songs
+more.random.auto = Speel meer willekeurige liedjes als het einde van de speelijst is bereikt.
+more.random.ok = OK
+more.random.genre = Van genre
+more.random.anygenre = Elke
+more.random.year = en jaar
+more.random.anyyear = Elk
+more.random.folder = In map
+more.random.anyfolder = Elke
+more.apps.title = Subsonic Apps
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic apps</a> zijn beschikbaar voor <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone</b> en <b>AIR</b>.</p>
+more.mobile.title = Mobiele telefoon
+more.mobile.text = <p>Je kunt {0} gebruiken met elke WAP-mobiele telefoon of PDA.<br> \
+ Gebruik gewoon de volgende URL op jouw telefoon: <b>http://jouwhostname/wap</b></p> \
+ <p>Uiteraard moet je server dan wel vanaf het Internet bereikbaar zijn.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Opgeslagen speellijsten zijn beschikbaar als Podcasts.<br>\
+ Gebruik de volgende URL in jouw Podcast ontvanger: <b>http://jouwhostname/podcast</b>, \
+ of <b><a href="podcast.view?suffix=.rss">Klik hier</a>.</b></p>
+more.upload.title = Bestand Uploaden
+more.upload.source = Selecteer bestand
+more.upload.target = Uploaden naar
+more.upload.browse = Kies
+more.upload.ok = Upload
+more.upload.unzip = Pak zip-bestand automatisch uit.
+more.upload.progress = % compleet. Wachten a.u.b...
+
+# upload.jsp
+upload.title = Bezig met uploaden van bestand
+upload.success = Succesvol <b>{0}</b> ge-upload.
+upload.empty = Er zijn geen bestanden om te uploaden.
+upload.failed = Uploaden is mislukt met foutmelding:<br><b>"{0}"</b>
+upload.unzipped = Uitgepakt {0}
+
+# help.jsp
+help.title = Over {0}
+help.upgrade = <b>Let op!</b> Er is een nieuwe versie beschikbaar. Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">hier</a>.
+help.version.title = Versie
+help.builddate.title = Compileer datum
+help.server.title = Server
+help.license.title = Gebruiksvoorwaarden
+help.license.text = {0} is vrije software die wordt gedistribueerd onder de <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source licentie. \
+ {0} maakt gebruik van <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">gelicenseerde bibliotheken van derden</a>. {0} is <em>geen</em> \
+ middel voor de illegale distributie van auteursrechtelijk beschermd materiaal. Hou rekening met de voor jouw land geldende wetten en bepalingen!
+help.homepage.title = Homepage
+help.forum.title = Forum
+help.shop.title = Merchandise
+help.contact.title = Contact
+help.contact.text = {0} is ontwikkeld en wordt onderhouden door Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Als je vragen, commentaar of suggesties hebt voor verbetering bezoek dan \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+help.donate = {0} is gratis, maar je kan bijdragen aan het project door een <b><a href="donate.view?">donatie</a></b>.
+help.log = Log
+help.logfile = Het volledige logbestand is opgeslagen in {0}.
+
+# settingsheader.jsp
+settingsheader.title = Instellingen
+settingsheader.general = Algemeen
+settingsheader.advanced = Gevorderd
+settingsheader.personal = Persoonlijk
+settingsheader.musicFolder = Mediamappen
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Afspelers
+settingsheader.share = Gedeelde media
+settingsheader.network = Netwerk
+settingsheader.transcoding = Omzetting
+settingsheader.user = Gebruikers
+settingsheader.search = Zoeken
+settingsheader.coverArt = Albumhoezen
+settingsheader.wachtwoord = Wachtwoord
+
+# generalsettings.jsp
+generalsettings.speellijstfolder = Speellijstmap
+generalsettings.musicmask = Muziekbestanden
+generalsettings.videomask = Videobestanden
+generalsettings.coverartmask = Albumhoezen
+generalsettings.index = Index
+generalsettings.ignoredarticles = Te negeren items
+generalsettings.shortcuts = Snelkoppelingen
+generalsettings.showgettingstarted = Toon "Snelgids" bij het starten
+generalsettings.welcometitle = Welkom titel
+generalsettings.welcomesubtitle = Welkom ondertitel
+generalsettings.welcomemessage = Welkomstbericht
+generalsettings.loginmessage = Inlogbericht
+generalsettings.language = Standaard taal
+generalsettings.theme = Standaard thema
+
+# advancedsettings.jsp
+advancedsettings.downsamplecommand = Commando voor conversie
+advancedsettings.coverartlimit = Albumhoes limiet<br><div class="detail">(0 = Ongelimiteerd)</div>
+advancedsettings.downloadlimit = Download limiet (Kbps)<br><div class="detail">(0 = Ongelimiteerd)</div>
+advancedsettings.uploadlimit = Upload limiet (Kbps)<br><div class="detail">(0 = Ongelimiteerd)</div>
+advancedsettings.streamport = Non-SSL stream poort<br><div class="detail">(0 = Uitgeschakeld)</div>
+advancedsettings.ldapenabled = Activeer LDAP authentificatie
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP zoekfilter
+advancedsettings.ldapmanagerdn = LDAP manager DN<br><div class="detail">(Optioneel)</div>
+advancedsettings.ldapmanagerpassword = Wachtwoord
+advancedsettings.ldapautoshadowing = Maak automatisch gebruikers aan in {0}
+
+# personalsettings.jsp
+personalsettings.title = Persoonlijke instellingen voor {0}
+personalsettings.language = Taal
+personalsettings.theme = Thema
+personalsettings.display = Scherm
+personalsettings.browse = Bladeren
+personalsettings.speellijst = Speellijst
+personalsettings.tracknumber = Track #
+personalsettings.artist = Artiest
+personalsettings.album = Album
+personalsettings.genre = Genre
+personalsettings.year = Jaar
+personalsettings.bitrate = Bitrate
+personalsettings.duration = Speelduur
+personalsettings.format = Formaat
+personalsettings.filesize = Bestandsgrootte
+personalsettings.captioncutoff = Opschrift afsnijden
+personalsettings.partymode = Party mode
+personalsettings.shownowplaying = Toon wat anderen spelen
+personalsettings.nowplayingallowed = Toon anderen wat ik speel
+personalsettings.showchat = Toon chat berichten
+personalsettings.finalversionnotification = Informeer me over nieuwe versies
+personalsettings.betaversionnotification = Informeer me over nieuwe Beta-versies
+personalsettings.lastfmenabled = Registreer wat ik afspeel op <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm gebruikersnaam
+personalsettings.lastfmpassword = Last.fm wachtwoord
+personalsettings.avatar.title = Persoonlijke afbeelding
+personalsettings.avatar.none = Geen afbeelding
+personalsettings.avatar.custom = Aangepaste afbeelding
+personalsettings.avatar.changecustom = Verander afbeelding
+personalsettings.avatar.upload = Upload
+personalsettings.avatar.courtesy = Ikonen met dank aan: <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-len.com/" target="_blank">Icons-Len</a>, en \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Verander afbeelding
+avataruploadresult.success = Afbeelding succesvol ge-upload "{0}".
+avataruploadresult.failure = Upload van afbeelding is mislukt. Zie <a href="help.view?">log</a> voor details.
+
+# passwordSettings.jsp
+passwordsettings.title = Verander wachtwoord voor {0}
+
+# musicFolderSettings,jsp
+musicfoldersettings.path = Map
+musicfoldersettings.name = Naam
+musicfoldersettings.enabled = Ingeschakeld
+musicfoldersettings.add = Voeg mediamap toe
+musicfoldersettings.nopath = Specificeer aan map.
+musicfoldersettings.notfound = Map niet gevonden
+musicfoldersettings.scan = Scan mediamappen
+musicfoldersettings.interval.never = Nooit
+musicfoldersettings.interval.one = Elke dag
+musicfoldersettings.interval.many = Elke {0} dagen
+musicfoldersettings.hour = om {0}:00
+musicfoldersettings.nowscanning = de mediamappen worden nu gescanned. Dit kan enige minten duren afhankelijk van \
+ de grootte van de mediabibliotheek.
+musicfoldersettings.scannow = Scan de mediamappen nu.
+musicfoldersettings.fastcache = Snelle toegangsmode.
+musicfoldersettings,fastcache.description = Gebruik deze optie om schijfgebruik te minimaliseren, bijvoorbeeld als de mediabestanden op een netwerkschijf staan. \
+ NB: Veranderingen aan bestanden worden alleen zichtbaar als de mediamappen zijn gescanned. (Zie hierboven).
+
+musicfoldersettings.organizebyfolderstructure = Organiseer via mappenstructuur.
+musicfoldersettings.organizebyfolderstructure.description = Gebruik deze optie om door jouw mediabibliotheek te bladeren via de mappenstructuur in plaats van artiest/album info in ID3 tags.
+
+# networkSettings.jsp
+networksettings.text = Gebruik de instellingen hieronder om te bepalen hoe jouw Subsonic-server over het Internet bereikbaar is .<br> \
+ Als er problemen zijn, raadpleeg dan de <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Getting started</b></a>gids.
+networksettings.portforwardingenabled = Configureer automatisch je router om inkomende verbindingen naar Subsonic toe te staan. (UPnP of NAT-PMP poort forwarding).
+networksettings.portforwardinghelp = Als jouw router niet automatisch kan worden geconfigureerd, kun je dit handmatig instellen. \
+ Volg de instructies op <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Je dient poort {0} door te sturen naar de computer waar de Subsonic server actief is.
+networksettings.urlredirectionenabled = Krijg toegang tot je server over het Internet met een gemakkelijk te onthouden adres.
+networksettings.status = Status:
+networksettings.trialexpired = De probeerperiode is vervallen op {0}. <b><a href="donate.view?">Doneer</a></b> alstublieft om deze functie permanent actief te maken.
+networksettings.trialnotexpired = Deze functie is beschikbaar tot {0}. Daarna moet je <b><a href="donate.view?">doneren</a></b> om de functie te kunnen blijven gebruiken.
+
+# transcodingInstellingen.jsp
+transcodingsettings.name = Naam
+transcodingsettings.sourceformat = Converteer van
+transcodingsettings.targetformat = Converteer naar
+transcodingsettings.step1 = Stap 1
+transcodingsettings.step2 = Stap 2
+transcodingsettings.step3 = Stap 3
+transcodingsettings.add = Voeg conversie toe
+transcodingsettings.defaultactive = Activeer deze conversie voor alle bestaande en nieuwe afspelers.
+transcodingsettings.recommended = Aanbevolen configuratie
+transcodingsettings.noname = Geef een naam op.
+transcodingsettings.nosourceformat = Specificeer het te converteren formaat.
+transcodingsettings.notargetformat = Specificeer het formaat waarnaar geconverteerd moet worden.
+transcodingsettings.nostep1 = Geef tenminste \u00e9\u00e9n conversiestap op.
+transcodingsettings.info = <p class="detail">(%s = het te converteren bestand, %b = Max bitrate van de afspeler, %t = Titel, %a = Artiest, %l = Album)</p> \
+ <p>Conversie is het proces waarin \u00e9\u00e9n mediaformaat wordt omgezet naar een ander formaat. De conversie van {1}''s \
+ zet media om die anders niet gestreamd kan worden. De conversie gebeurt tijdens het afspelen en neemt geen schrijfruimte in beslag. <p/> \
+ <p>De eigenlijke conversie gebeurt door software van derden die ge\u00efnstalleerd moeten worden in {0}. \
+ Je kunt je eigen converter (Transcoder) toevoegen mits dat die aan de volgende voorwaarden voldoet: \
+ <ul> \
+ <li>De software moet vanaf de commandoregel bediend kunnen worden.</li> \
+ <li>Het moet mogelijk zijn om de uitvoer naar stdout te sturen..</li> \
+ <li>Als er gebruik wordt gemaakt van stap 2 en 3 dan moet het mogelijk zijn om de invoer te sturen naar stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Merk op dat conversies per afspeler geactiveerd worden via <b>Instellingen &gt; afspelers</b>.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradioSettings.homepageurl = Homepage
+internetradioSettings.name = Naam
+internetradioSettings.enabled = Geactiveerd
+internetradioSettings.add = Voeg Internet TV/radio toe
+internetradioSettings.nourl = Specificeer een URL.
+internetradioSettings.noname = Specificeer een naam.
+
+# podcastSettings.jsp
+podcastsettings.update = Controleer op nieuwe uitzendingen
+podcastsettings.keep = Bewaar
+podcastsettings.keep.all = Alle uitzendingen
+podcastsettings.keep.one = Meest recente aflevering
+podcastsettings.keep.many = Laatste {0} uitzendingen
+podcastsettings.download = Wanneer nieuwe uitzendingen beschikbaar zijn
+podcastsettings.download.all = Download alles
+podcastsettings.download.one = Download de meest recente aflevering.
+podcastsettings.download.many = Download de laatste {0} uitzendingen
+podcastsettings.download.none = Download niets
+podcastsettings.interval.manually = Handmatig
+podcastsettings.interval.hourly = Elk uur
+podcastsettings.interval.daily = Elke dag
+podcastsettings.interval.weekly = Elke week
+podcastsettings.folder = Sla podcasts op in:
+
+# playerSettings.jsp
+playersettings.noplayers = Geen afspelers gevonden.
+playersettings.type = Type
+playersettings.lastseen = Laatst bekeken
+playersettings.title = Selecteer afspeler
+playersettings.technology.web.title = Web afspeler
+playersettings.technology.external.title = Externe afspeler
+playersettings.technology.external_with_playlist.title = Externe afspeler met speellijst
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Speel muziek direct af in de web browser met de ge\u00efntegreerde Flash afspeler.
+playersettings.technology.external.text = Speel muziek met je favoriete afspeler, zoals WinAmp of Windows Media Player.
+playersettings.technology.external_with_playlist.text = Hetzelfde als hierboven, maar de speellijst wordt beheerd door de afspeler, en niet \
+ door de Subsonic server. In deze mode, is overslaan van liedjes mogelijk.
+playersettings.technology.jukebox.text = Speel muziek direct af via de geluidskaart van de Subsonic server. (Alleen voor geauthoriseerde gebruikers).
+playersettings.name = Afspeler naam
+playersettings.coverartsize = Grootte albumhoes
+playersettings.maxbitrate = Max bitsnelheid
+playersettings.coverart.off = Uit
+playersettings.coverart.small = Klein
+playersettings.coverart.medium = Middel
+playersettings.coverart.large = Groot
+playersettings.nolame = <em>LET OP:</em> LAME schijnt niet ge\u00efnstalleerd te zijn.<br>Klik de Help knop voor meer informatie.
+playersettings.autocontrol = Bedien afspeler automatisch
+playersettings.dynamicip = Afspeler heeft een dynamisch IP adres
+playersettings.transcodings = Actieve conversie
+playersettings.ok = Opslaan
+playersettings.forget = Verwijder afspeler
+playersettings.clone = Kloon afspeler
+
+# shareSettings.jsp
+sharesettings.name = Naam
+sharesettings.owner = Gedeeld door
+sharesettings.description = Beschrijving
+sharesettings.visits = Bezoeken
+sharesettings.lastvisited = Laatst bezocht
+sharesettings.expires = Verloopt op
+sharesettings.files = Gedeelde bestanden
+sharesettings.expirein = Verloopt na
+sharesettings.expirein.week = 1w
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1j
+sharesettings.expirein.never = nooit
+
+# usersettings.jsp
+usersettings.title = Selecteer gebruiker
+usersettings.newuser = Nieuwe gebruiker
+usersettings.admin = Gebruiker is administrator
+usersettings.settings = Gebruiker mag instellingen en wachtwoord veranderen
+usersettings.stream = Gebruiker mag bestanden afspelen
+usersettings.jukebox = Gebruiker mag bestanden afspelen in jukebox modus
+usersettings.download = Gebruiker mag bestanden downloaden.
+usersettings.upload = Gebruiker mag bestanden uploaden
+usersettings.share = Gebruiker mag bestanden delen met iedereen.
+usersettings.playlist = Gebruiker mag speellijsten maken en verwijderen.
+usersettings.coverart = Gebruiker mag albumhoezen en tags wijzigen.
+usersettings.comment= Gebruiker mag commentaar en beoordelingen maken en bewerken.
+usersettings.podcast= Gebruiker mag Podcasts beheren.
+usersettings.username = Gebruikersnaam
+usersettings.email = E-mail
+usersettings.changepassword = Verander wachtwoord
+usersettings.password = Wachtwoord
+usersettings.newpassword = Nieuw wachtwoord
+usersettings.confirmpassword = Bevestig wachtwoord
+usersettings.delete = Verwijder deze gebruiker
+usersettings.ldap = Authentificeer gebruiker in LDAP
+usersettings.nousername = Gebruikersnaam niet opgegeven.
+usersettings.noemail= Ongeldig e-mailadres.
+usersettings.useralreadyexists = Gebruiker bestaat al.
+usersettings.nopassword = Wachtwoord is verplicht.
+usersettings.wrongpassword = Wachtwoorden kwamen niet overeen.
+usersettings.ldapdisabled = LDAP authentificatie is niet ingeschakeld. Zie gevorderde instellingen.
+usersettings.passwordnotsupportedforldap = Kan het wachtwoord voor LDAP-Geauthentificeerde gebruikers niet instellen of wijzigen.
+usersettings.ok = Wachtwoord succesvol gewijzigd voor gebruiker {0}.
+
+# searchSettings.jsp
+searchsettings.auto = Update zoekindex automatisch
+searchsettings.manual = Update zoekindex nu.
+searchsettings.interval.never = Nooit
+searchsettings.interval.one = Elke dag
+searchsettings.interval.many = Elke {0} dagen
+searchsettings.hour = om {0}:00
+searchsettings.text = De zoekindex wordt nu gemaakt. Dit kan enige tijd duren, afhankelijk van de grootte \
+ van je mediabibliotheek.<br>Je kunt {0} blijven gebruiken terwijl de zoekindex wordt gemaakt.
+
+# main.jsp
+main.up = Omhoog
+main.playall = Speel alles
+main.playrandom = Speel willekeurig
+main.addall = Voeg alles toe
+main.tags = Bewerk tags
+main.playcount = {0} keer afgespeeld.
+main.lastplayed = Laatst gespeeld {0}.
+main.comment = Commentaar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>New paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Named link</td></tr>\
+ </table>
+main.sharealbum = Delen
+main.more = Meer acties...
+main.more.selection = Geselecteerde songs
+main.more.share = Deel
+main.donate = <a href="{0}" style="text-decoration:underline">Doneer</a> to {1}!<br>(en verwijder deze advertentie)
+main.nowplaying = Speelt nu
+main.lyrics = Teksten
+main.minutesago = minuten geleden
+main.chat = Chat berichten
+main.scanning = Bezig met scannen bestanden:
+main.message = Schrijf een bericht.
+main.clearchat = Verwijder berichten
+
+# rating.jsp
+rating.rating = Waardering
+rating.clearrating = Verwijder waardering
+
+# coverArt.jsp
+coverart.change = Verander
+coverart.zoom = Zoom in
+
+# allmusic.jsp
+allmuziek.text = Bezig met zoeken naar album <em>{0}</em> op allmusic.com - Even geduld alstublieft.
+
+# changeCoverArt.jsp
+changecoverart.title = Verander albumhoes
+changecoverart.adres = Geef url voor afbeelding
+changecoverart.artist = Artiest
+changecoverart.album = Album
+changecoverart.search = Google Afbeeldingen Zoeken
+changecoverart.wait = Even geduld...
+changecoverart.success = Afbeelding succesvol gedownload.
+changecoverart.error = Kon afbeelding niet downloaden.
+changecoverart.noimagesfound = Geen afbeeldingen gevonden.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Kon albumhoes voor:<br><b>"{0}"</b> niet veranderen.
+
+# editTags.jsp
+edittags.title = Bewerk tags
+edittags.file = Bestand
+edittags.track = Nummer
+edittags.songtitle = Titel
+edittags.artist = Artiest
+edittags.album = Album
+edittags.year = Jaar
+edittags.genre = Genre
+edittags.status = Status
+edittags.suggest = Suggestie
+edittags.reset = Reset
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Instellen
+edittags.working = Bezig...
+edittags.updated = Bijgewerkt
+edittags.skipped = Overgeslagen
+edittags.error = Fout!
+
+# share.jsp
+share.title = Delen
+share.warning = <h2>Attentie!</h2><p> Blijf eerlijk &ndash;Overtreed geen wetten door auteursrechtelijk beschermd werk te delen.</p>
+share.facebook = Deel op Facebook
+share.twitter = Deel op Twitter
+share.googleplus = Deel op Google+
+share.link = of deel dit door iemand de volgende link te sturen: <a href="{0}" target="_blank">{0}</a>
+share.disabled = Om jouw muziek te kunnen delen moet je eerst je eigen <em>subsonic.org</em> adres registreren.<br> \
+ Ga naar <a href="networksettings.view"><b>Instellingen &gt; Network</b></a> (Administrator rechten vereist!).
+share.manage = Beheer gedeelde media.
+
+# donate.jsp
+donate.title = Doneer
+donate.invalidlicense = Ongeldige licentie sleutel.
+donate.amount = Doneer {0}
+
+donate.textbefore = <p>Bedankt voor uw donatie aan het {0} project! \
+ Donateurs krijgen toegang tot:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> voor Android, iPhone en Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> voor PlayBook, Roku, Mac, Chrome en meer*.</li> \
+ <li>Video streaming.</li> \
+ <li>Je persoonlijke server adres: <em>jouwnaam</em>.subsonic.org (Zie <a href="networksettings.view">Instellingen &gt; Netwerk</a>).</li> \
+ <li>Deel jouw media op Facebook, Twitter, Google+.</li> \
+ <li>Geen advertenties in de web interface.</li> \
+ <li>Andere aanvullingen die later nog uitgebracht zullen worden.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Sommige apps worden verkocht door andere ontwikkelaars.</p>\
+ <p>Als donateur ontvang je een licentiesleutel die geldig is voor persoonlijk ,niet-commercieel gebruik van deze \
+ en alle toekomstige releases van {0}. Neem, voor commercieel gebruik , <a href="mailto:subsonic_donation@activeobjects.no">contact</a> met ons op.</p> \
+ <p> Onze voorkeur gaat uit naar een donatie van <b>&euro;20</b>, maar je kunt elk bedrag geven dat je wilt.</p>
+donate.textafter = <p>Klik een knop om naar Paypal te gaan waar je met creditkaart kunt betalen of \
+ met jouw PayPal account (als je dat hebt). Je ontvangt de licentiesleutel binnen een paar minuten per e-mail..</p> \
+ <p>Mocht je nog vragen hebben, stuur dan een e-mail naar: \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Dit exemplaar van{2} is gelicenseerd aan {0} op {1}. Dank je voor de ondersteuning!
+donate.register = Na dat je de licentiesleutel hebt ontvangen, kun je die hieronder invoeren.
+donate.resend = Heb je al een licentie gekocht maar ben je de licentiesleutel kwijt? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Mail mijn sleutel opnieuw.</a>.
+donate.register.email = E-mail
+donate.register.license = Licentie
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast ontvanger
+podcastreceiver.expandall = Toon uitzendingen
+podcastreceiver.collapseall = Verberg uitzendingen
+podcastreceiver.status.new = Nieuw
+podcastreceiver.status.downloading = Bezig met downloaden
+podcastreceiver.status.completed = Voltooid
+podcastreceiver.status.error = Fout
+podcastreceiver.status.deleted = Verwijderd
+podcastreceiver.status.skipped = Overgeslagen
+podcastreceiver.downloadselected= Download geselecteerde
+podcastreceiver.deleteselected= Verwijder geselecteerde
+podcastreceiver.confirmdelete= Geselecteerde Podcasts echt verwijderen?
+podcastreceiver.check = Controleer op nieuwe uitzendingen
+podcastreceiver.refresh = Ververs pagina.
+podcastreceiver.settings = Podcast Instellingen
+podcastreceiver.subscribe = Abonneer op Podcast
+
+# lyrics.jsp
+lyrics.title = Teksten
+lyrics.artist = Artiest
+lyrics.song = Song
+lyrics.search = Zoeken
+lyrics.wait = Zoek naar teksten, even wachten...
+lyrics.courtesy = (Teksten van <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Geen teksten gevonden.
+
+# helpPopup.jsp
+helppopup.title = {0} Help
+helppopup.cover.title = Albumhoes afmetingen
+helppopup.cover.text = <p>Hiermee kun je de grootte van de albumhoezen bepalen en het tonen daarvan uitschakelen.</p>
+helppopup.transcode.title = Max bitrate
+helppopup.transcode.text = <p>Als je een beperkte bandbreedte hebt kun je hier en maximale limiet instellen voor de bitrate voor afgespeelde muziek. \
+ Voorbeeld: Als jouw originele mp3-bestanden een bitrate hebben van 256 Kbps (kilobits per second), zal {0} bij een \
+ ingestelde max bitrate van 128 automatisch de muziek resamplen van 256 to naar 128 Kbps.</p> \
+ <p>Hiervoor moet wel LAME zijn ge\u00efnstalleerd. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ is een open source mp3 encoder. Je kunt dit programma <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"> hier downloaden</a>. \
+ Iinstalleer Lame in de SUBSONIC_HOME/transcode map.</p>
+helppopup.playlistfolder.title = Speellijstmap
+helppopup.playlistfolder.text = <p>Geef hier op in welke map jouw speellijsten staan.</p>
+helppopup.musicmask.title = Muziekbestanden
+helppopup.musicmask.text = <p>Geef hier aan welke bestandstypen als muziekbestanden moeten worden gezien.</p>
+helppopup.videomask.title = Videobestanden
+helppopup.videomask.text = <p>Geef hier aan welke bestandstypen als videobestanden moeten worden gezien.</p>
+helppopup.coverartmask.title = Albumhoesbestanden
+helppopup.coverartmask.text = <p>Geef hier aan welke bestandstypen als afbeeldingsbestanden moeten worden gezien voor albumhoezen.</p>
+helppopup.downsamplecommand.title = Downsample commando
+helppopup.downsamplecommand.text = <p>Geef hier het commando op om muziek van een hogere bitsnelheid om te zetten naar een lagere bitsnelheid.</p>\
+ <p>(%s = Het bestand dat moet worden omgezet, %b = Max bitsnelheid van de afspeler, %t = Titel, %a = Artiest, %l = Album)</p>
+helppopup.index.title = Index
+helppopup.index.text = <p>Specificeer hier hoe de index (links op het scherm) moet worden getoond. Bestanden en mappen die in de mediamap staan \
+ zijn via deze index snel en gemakkelijk toegankelijk.</p> \
+ <p>De specificatie is een spatie-gescheiden lijst van indexvermeldingen. Normaal gsproken is elke vermeldingen maar \u00e9\u00e9n karakter, \
+ maar je mag ook meerdere karakters opgeven. Voorbeeld: de vermelding <em>de</em> zal linken naar alle bestanden en mappen \
+ die beginnen met "de".</p> \
+ <p>Je kunt ook een vermelding maken van een groep index-karakters tussen haakjes. Voorbeeld: de vermelding: \
+ <em>A-E(ABCDE)</em> zal worden getoond als <em>A-E</em> en linken naar alle bestanden en mappen die met \
+ A, B, C, D of E beginnen. Dit kan nuttig zijn bij het groeperen van minder frequent gebruikte letters als X, Y en Z, of \
+ voor het groeperen van accentletters als A, \u00c0 en \u00c1) of cijfers.</p> \
+ <p>Bestanden en mappen die niet in een indexvermelding worden benoemd worden geplaatst onder "#".</p>
+helppopup.ignoredarticles.title = Te negeren items
+helppopup.ignoredarticles.text = <p>Specificeer hier welke woorden (zoals "the") moeten worden genegeerd bij het maken van de index.</p>
+helppopup.shortcuts.title = Snelkoppelingen
+helppopup.shortcuts.text = <p>Dit is een spatie-gescheiden lijst van mappen die je via een snelkoppeling kunt openen. Gebruik aanhalingstekens om woorden te groeperen.</p> \
+ <p>Bijvoorbeeld <em>Nieuw Binnengekomen "Sound tracks"</em></p>
+helppopup.language.title = Taal
+helppopup.language.text = <p>Stel hier in welke taal je gaat gebruiken.</p>
+helppopup.visibility.title = Zichtbaarheid
+helppopup.visibility.text = <p>Selecteer welke details moeten worden getoond voor elk lied, tevens bepaal je hier waar het onderschrift wordt afgebroken.\
+ Dit is het maximale aantal letters die worden getoond om Titel, Artiest en Album te vermelden.</p>
+helppopup.partymode.title = Party mode
+helppopup.partymode.text = <p>Met Party Mode ingeschakeld wordt de gebruikersinterface eenvoudiger en gemakkelijker bedienbaar gemaakt voor onervaren gebruikers. \
+ Hierbij wordt voorkomen dat afspeellijsten worden verprutst.</p>
+helppopup.theme.title = Thema
+helppopup.theme.text = <p>Bepaal hier welk thema je gaat gebruiken. Een thema bepaalt het uiterlijk van {0} voor wat betreft kleuren, lettertypen, afbeeldingen enz.</p>
+helppopup.welcomemessage.title = Welkomstbericht
+helppopup.welcomemessage.text = <p>Het bericht dat op de EERSTE pagina van {0} wordt getoond.</p>
+helppopup.loginmessage.title = Loginbericht
+helppopup.loginmessage.text = <p>Het bericht dat op de INLOGpagina wordt getoond.</p>
+helppopup.coverartlimit.title = Limiet albumhoes
+helppopup.coverartlimit.text = <p>Het maximale aantal albumhoezen dat op een pagina mag worden getoond.</p>
+helppopup.downloadlimit.title = Download limiet
+helppopup.downloadlimit.text = <p>Een bovengrens voor de bandbreedte die zal worden gebruikt voor het downloaden van bestanden.</p>
+helppopup.uploadlimit.title = Upload limiet
+helppopup.uploadlimit.text = <p>Een bovengrens voor de bandbreedte die zal worden gebruikt voor het uploaden van bestanden.</p>
+helppopup.streamport.title = Non-SSL stream poort
+helppopup.streamport.text = <p>Deze optie is alleen relevant als {0} op een server met SSL (HTTPS) wordt uitgevoerd.</p><p>Sommige afspelers \
+ (zoals Winamp) ondersteunen geen streaming over SSL. Specificeer het poortnummer voor normaal http-verkeer (gewoonlijk 80 \
+ of 4040) als je niet over SSL wilt streamen. Let wel dat de streams geen encryptie hebben.</p>
+helppopup.ldap.title = LDAP authentificatie
+helppopup.ldap.text = <p>gebruikers kunnen worden geauthentificeerd door een externe LDAP server (inclusief Windows Active Directory). \
+ Als gebruikers waarvoor LDAP is ingeschakeld inloggen op {0}, wordt de gebruikersnaam en wachtwoord geverifieerd door de externe server, niet door {0} zelf.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>de URL van de LDAP server. Het protocol moet of <em>ldap://</em> of <em>ldaps://</em> zijn.\
+ (voor LDAP over SSL). Zie <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">hier</a> \
+ voor een gedetaileerdere beschrijving.</p>
+helppopup.ldapsearchfilter.title = LDAP zoekfilter
+helppopup.ldapsearchfilter.text = <p>de filter expressie die gebruikt wordt om een gebruiker op te zoeken in LDAP. Dit is een LDAP zoekfilter \
+ (zoals gedefinieerd in <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Het patroon "'{0'}" wordt vervangen door de gebruikersnaam, bijvoorbeeld: \
+ <ul>\
+ <li>(uid='{0'}) - Dit zou zoeken op een overeenkomst op het uid attribuut.</li> \
+ <li>(sAMAccountName='{0'}) - typisch gebruikt voor authentificatie in Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP manager DN
+helppopup.ldapmanagerdn.text = <p>Als de LDAP server geen anonieme verbindingen toestaat moet de DN (<em>Distinguished Name</em>) \
+ en wachtwoord worden vermeld van de LDAP-gebruiker die verbinding mag maken met de LDAP server.</p>
+helppopup.ldapautoshadowing.title = Maak automatisch LDAP gebruikers aan in {0}
+helppopup.ldapautoshadowing.text = <p>Als je voor deze optie kiest, hoef je LDAP-gebruikers niet handmatig in {0} aan te maken. </p> \
+ <p>LET OP! Dit btekent dat ELKE gebruiker met een geldige LDAP-gebruikersnaam en wachtwoord in kan loggen op {0}, \
+ wat je misschien niet wilt hebben.</p>
+helppopup.playername.title = Afspelernaam
+helppopup.playername.text = <p>Hier kun je een gemakkelijk te onthouden naam opgeven voor een afspeler, zoals "Werk" of "Huiskamer".</p>
+helppopup.autocontrol.title = Bedien afspeler automatisch
+helppopup.autocontrol.text = <p>Met deze optie aan gezet, zal {0} automatisch de afspeler starten als je op "Speel" klikt \
+ in de afspeellijst. Anders moet je zelf de afspeler starten en verbinden.</p>
+helppopup.dynamicip.title = Dynamisch IP adres
+helppopup.dynamicip.text = <p>Schakel deze optie uit als de afspeler een statisch IP adres heeft.</p>
+
+# wap/index.jsp
+wap.index.missing = Geen muziek gevonden.
+wap.index.playlist = Speellijst
+wap.index.search = Zoeken
+wap.index.settings = Instellingen
+
+# wap/browse.jsp
+wap.browse.playone = Speel song
+wap.browse.playall = Speel alles
+wap.browse.addone = Voeg song toe
+wap.browse.addall = Voeg alles toe
+wap.browse.downloadone = Download song
+wap.browse.downloadall = Download alles
+
+# wap/playlist.jsp
+wap.playlist.title = Speellijst
+wap.playlist.noplayer = Geen afspeler verbonden
+wap.playlist.clear = Maak leeg
+wap.playlist.load = Laad
+wap.playlist.random = Willekeurig
+wap.playlist.play = Afspelen op telefoon.
+
+# wap/search.jsp
+wap.search.title = Zoeken
+
+# wap/searchResult.jsp
+wap.searchresult.index = De zoekindex wordt nu gemaakt. Probeer het later nog eens...
+
+# wap/Instellingen.jsp
+wap.Instellingen.selectplayer = Selecteer afspeler
+wap.Instellingen.allplayers = Alle
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_no.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_no.properties
new file mode 100644
index 00000000..87736fe9
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_no.properties
@@ -0,0 +1,640 @@
+#
+# Norwegian localization.
+# Author: Sindre Mehus
+# Spell check: Tommy Karlsen
+#
+
+common.home = Hjem
+common.back = Tilbake
+common.help = Hjelp
+common.play = Spill
+common.add = Legg til
+common.download = Last ned
+common.close = Lukk
+common.refresh = Oppdater
+common.next = Neste
+common.previous = Forrige
+common.more = Mer
+common.ok = OK
+common.cancel = Avbryt
+common.save = Lagre
+common.create = Opprett
+common.delete = Slett
+common.unknown = (Ukjent)
+common.default = (Standard)
+
+# login.jsp
+login.username = Brukernavn
+login.password = Passord
+login.login = Logg inn
+login.remember = Husk meg
+login.logout = Du er n\u00e5 logget ut.
+login.error = Galt brukernavn eller passord.
+login.insecure = {0} er ikke sikret. Logg inn med brukernavn og<br>passord "admin", og bytt passord umiddelbart.
+
+# accessDenied.jsp
+accessDenied.title = Ingen tilgang
+accessDenied.text = Beklager, du er ikke autorisert til \u00e5 utf\u00f8re denne oppgaven.
+
+# top.jsp
+top.home = Hjem
+top.now_playing = Spilles&nbsp;n\u00e5
+top.settings = Innstillinger
+top.status = Status
+top.podcast = Podcast
+top.more = Mer
+top.help = Om
+top.search = S\u00f8k
+top.upgrade = <b>NB!</b> Det finnes en oppdatert versjon.<br>Last ned {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">her</a>.
+top.missing = Fant ingen musikk-mapper. Endre innstillingene.
+top.logout = Logg ut {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artister<br>\
+ {1}&nbsp;album<br>\
+ {2}&nbsp;sanger<br>\
+ {3} (&#126; {4} timer)
+left.shortcut = Snarveier
+left.radio = Internett-TV/Radio
+left.allfolders = Alle
+
+# playlist.jsp
+playlist.stop = Stopp
+playlist.start = Start
+playlist.confirmclear = Vil du virkelig t\u00f8mme spillelisten?
+playlist.clear = T\u00f8m
+playlist.shuffle = Stokk om
+playlist.repeat_on = Listen repeteres
+playlist.repeat_off = Listen repeteres ikke
+playlist.undo = Angre
+playlist.settings = Innstillinger
+playlist.more = Flere valg...
+playlist.more.playlist = Spilleliste
+playlist.more.sortbytrack = Sorter p\u00e5 sang
+playlist.more.sortbyartist = Sorter p\u00e5 artist
+playlist.more.sortbyalbum = Sorter p\u00e5 album
+playlist.more.selection = Valgte sanger
+playlist.more.selectall = Velg alle
+playlist.more.selectnone = Velg ingen
+playlist.getflash = Hent Flash-spiller
+playlist.load = \u00c5pne
+playlist.save = Lagre
+playlist.append = Legg til spilleliste
+playlist.remove = Slett
+playlist.up = Opp
+playlist.down = Ned
+playlist.empty = Spillelisten er tom
+
+# status.jsp
+status.title = Status
+status.type = Type
+status.stream = Avspilling
+status.download = Nedlasting
+status.upload = Opplasting
+status.player = Spiller
+status.user = Bruker
+status.current = Fil
+status.transmitted = Overf\u00f8rt
+status.bitrate = Hastighet (Kbps)
+
+# search.jsp
+search.title = S\u00f8k
+search.search = S\u00f8k
+search.index = S\u00f8keindeksen er i ferd med \u00e5 bli opprettet. Pr\u00f8v p\u00e5 nytt senere.
+search.hits.none = Ingen treff.
+search.hits.more = Mer
+
+# home.jsp
+home.random.title = Tilfeldig
+home.newest.title = Ny
+home.highest.title = H\u00f8yest rangert
+home.frequent.title = Mest spilt
+home.recent.title = Sist spilt
+home.users.title = Brukere
+home.random.text = Tilfeldige album
+home.newest.text = Nyeste album
+home.highest.text = H\u00f8yest rangerte album
+home.frequent.text = Mest spilte album
+home.recent.text = Sist spilte album
+home.users.text = Brukerstatistikk
+home.scan = Leter etter musikk. Noen funksjoner er forel\u00f8pig ikke tilgjengelige.
+home.listsize = {0} album per side
+home.albums = Album {0} - {1}
+home.playcount = Spilt {0} sanger
+home.lastplayed = Spilt {0}
+home.created = Lagt til {0}
+home.chart.total = Totalt (MB)
+home.chart.stream = Avspilt (MB)
+home.chart.download = Nedlastet (MB)
+home.chart.upload = Opplastet (MB)
+
+# more.jsp
+more.title = Mer
+more.random.title = Tilfeldig spilleliste
+more.random.text = Lag tilfeldig spilleliste med
+more.random.songs = {0} sanger
+more.random.auto = Spill flere tilfeldige sanger n\u00e5r spillelisten er ferdig
+more.random.ok = OK
+more.random.genre = fra sjanger
+more.random.anygenre = Alle
+more.random.year = og \u00e5r
+more.random.anyyear = Alle
+more.random.folder = i mappe
+more.random.anyfolder = Alle
+more.mobile.title = Mobiltelefon
+more.mobile.text = <p>Du kan styre {0} fra en WAP-telefon eller PDA.<br>\
+ Bruk denne adressen: <b>http://dinmaskin/wap</b></p>\
+ <p>Dette krever at {0}-tjeneren din er tilgjengelig p\u00e5 internett.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Lagrede spillelister er tilgjengelig via Podcast.<br>\
+ Bruk denne adressen i din Podcast-mottaker: <b>http://dinmaskin/podcast</b>, \
+ eller <b><a href="podcast.view?suffix=.rss">klikk her</a>.</b></p>
+more.upload.title = Last opp fil
+more.upload.source = Velg fil
+more.upload.target = Lagre i
+more.upload.browse = Velg
+more.upload.ok = OK
+more.upload.unzip = Pakk opp zip-filer automatisk.
+more.upload.progress = % ferdig. Vennligst vent...
+
+# upload.jsp
+upload.title = Laster opp filer
+upload.success = Lastet opp <b>{0}</b>
+upload.empty = Ingen filer funnet.
+upload.failed = Fikk f\u00f8lgende feil ved opplasting av filer:<br><b>"{0}"</b>
+upload.unzipped = Pakket ut {0}
+
+# help.jsp
+help.title = Om {0}
+help.upgrade = <b>NB!</b> Det finnes en oppdatert versjon. Last ned {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">her</a>.
+help.version.title = Versjon
+help.builddate.title = Byggedato
+help.server.title = Server
+help.license.title = Lisens
+help.license.text = {0} er \u00e5pen kildekode og distribueres i henhold til vilk\u00e5rene i <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>.
+help.homepage.title = Hjemmeside
+help.forum.title = Forum
+help.shop.title = Butikk
+help.contact.title = Kontakt
+help.contact.text = {0} er utviklet og vedlikeholdt av Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Bes\u00f8k <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a> hvis du har sp\u00f8rsm\u00e5l, \
+ kommentarer eller forslag til forbedringer.
+help.donate = {0} er gratis, men du kan st\u00f8tte prosjektet ved \u00e5 gi en <b><a href="donate.view?">donasjon</a></b>.
+help.log = Logg
+help.logfile = Hele loggen finnes i {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Innstillinger
+settingsheader.general = Generelt
+settingsheader.advanced = Avansert
+settingsheader.personal = Personlig
+settingsheader.musicFolder = Musikkmapper
+settingsheader.internetRadio = Internett-TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Spillere
+settingsheader.transcoding = Konvertering
+settingsheader.user = Brukere
+settingsheader.search = S\u00f8k
+settingsheader.password = Passord
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Mappe for spillelister
+generalsettings.musicmask = M\u00f8nster for musikk
+generalsettings.coverartmask = M\u00f8nster for bilder
+generalsettings.index = Indeks
+generalsettings.ignoredarticles = Utelatte artikler
+generalsettings.shortcuts = Snarveier
+generalsettings.welcometitle = Velkomst-tittel
+generalsettings.welcomesubtitle = Velkomst-undertittel
+generalsettings.welcomemessage = Velkomstmelding
+generalsettings.language = Spr\u00e5k
+generalsettings.theme = Tema
+generalsettings.loginmessage = Innloggingsmelding
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Downsample-kommando
+advancedsettings.coverartlimit = Maks bildeantall<br><div class="detail">(0 = Ubegrenset)</div>
+advancedsettings.downloadlimit = Maks nedlastingsfart (Kbps)<br><div class = "detail">(0 = Ubegrenset)</div>
+advancedsettings.uploadlimit = Maks opplastingsfart (Kbps)<br><div class = "detail">(0 = Ubegrenset)</div>
+advancedsettings.streamport = Ikke-SSL stream port<br><div class="detail">(0 = Avskrudd)</div>
+advancedsettings.ldapenabled = Aktiver LDAP autentisering
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = s\u00f8kefilter for LDAP
+advancedsettings.ldapmanagerdn = DN for LDAP-administrasjon<br><div class="detail">(Valgfritt)</div>
+advancedsettings.ldapmanagerpassword = Passord
+advancedsettings.ldapautoshadowing = Opprett brukere i {0} automatisk
+
+
+# personalSettings.jsp
+personalsettings.title = Personlige innstillinger for {0}
+personalsettings.language = Spr\u00e5k
+personalsettings.theme = Tema
+personalsettings.display = Vis
+personalsettings.browse = Hoved
+personalsettings.playlist = Spilleliste
+personalsettings.tracknumber = Spor
+personalsettings.artist = Artist
+personalsettings.album = Album
+personalsettings.genre = Sjanger
+personalsettings.year = \u00c5r
+personalsettings.bitrate = Bit-rate
+personalsettings.duration = Varighet
+personalsettings.format = Format
+personalsettings.filesize = Filst\u00f8rrelse
+personalsettings.captioncutoff = Maks. lengde
+personalsettings.partymode = Festmodus
+personalsettings.shownowplaying = Vis hva andre spiller
+personalsettings.nowplayingallowed = La andre se hva jeg spiller
+personalsettings.finalversionnotification = Vis melding om nye versjoner
+personalsettings.betaversionnotification = Vis melding om nye beta-versjoner
+personalsettings.lastfmenabled = Registrer hva jeg spiller hos <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmpassword = Passord for Last.fm
+personalsettings.lastfmusername = Brukernavn for Last.fm
+personalsettings.avatar.title = Personlig bilde
+personalsettings.avatar.none = Ingen bilde
+personalsettings.avatar.custom = Egendefinert bilde
+personalsettings.avatar.changecustom = Endre egendefinert bilde
+personalsettings.avatar.upload = Last opp
+personalsettings.avatar.courtesy = Ikoner fra <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, og \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Endre personlig bilde
+avataruploadresult.success = Lastet opp personlig bilde "{0}".
+avataruploadresult.failure = Opplasting av personlig bilde feilet. Se <a href="help.view?">log</a> for detaljer.
+
+
+
+# passwordSettings.jsp
+passwordsettings.title = Bytt passord for {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Mappe
+musicfoldersettings.name = Navn
+musicfoldersettings.enabled = Aktiv
+musicfoldersettings.add = Legg til musikkmappe
+musicfoldersettings.nopath = Vennligst angi mappe
+
+# transcodingSettings.jsp
+transcodingsettings.name = Navn
+transcodingsettings.sourceformat = Konv. fra
+transcodingsettings.targetformat = Konv. til
+transcodingsettings.step1 = Steg 1
+transcodingsettings.step2 = Steg 2
+transcodingsettings.step3 = Steg 3
+transcodingsettings.defaultactive = Standard
+transcodingsettings.enabled = Aktiv
+transcodingsettings.add = Legg til transkoding
+transcodingsettings.noname = Vennligst angi et navn.
+transcodingsettings.nosourceformat = Angi formatet det skal konverteres fra.
+transcodingsettings.notargetformat = Angi formatet det skal konverteres til.
+transcodingsettings.nostep1 = Angi minst ett konverteringssteg.
+transcodingsettings.info = <p class="detail">(%s = Fila som skal konverteres, %b = Maks. b\u00e5ndbredde for spilleren)</p> \
+ <p>Konvertering er \u00e5 oversette fra et mediaformat til et annet. Slik kan {1} spille av media som normalt ikke lar seg streame. \
+ Konverteringen skjer p\u00e5 direkten etter behov, og krever ikke bruk av disk.</p>\
+ <p>Den faktiske konverteringen utf\u00f8res av tredjeparts kommandolinje-program som m\u00e5 installeres i {0}. \
+ En samling av konvertere for Windows \
+ kan lastes ned <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>her</b></a>. Du kan legge til en egen \
+ konverterer hvis den tilfredsstiller f\u00f8lgende krav: \
+ <ul> \
+ <li>Den m\u00e5 ha et kommandolinje-grensesnitt.</li> \
+ <li>Den m\u00e5 kunne skrive resultatet til "stdout".</li> \
+ <li>Hvis den skal brukes i steg 2 eller 3 m\u00e5 den kunne lese data fra "stdin".</li> \
+ </ul> \
+ </p> \
+ <p>Merk at konverterere aktiveres per spiller (fra siden for spiller-innstillinger). Hvis "Standard" er valgt, vil konvertereren \
+ automatisk bli aktivert for nye spillere.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Stream URL
+internetradiosettings.homepageurl = Hjemmeside
+internetradiosettings.name = Navn
+internetradiosettings.enabled = Aktiv
+internetradiosettings.add = Legg til Internett TV/radio
+internetradiosettings.nourl = Vennligst angi URL.
+internetradiosettings.noname = Vennligst angi navn.
+
+# podcastSettings.jsp
+podcastsettings.update = Se etter nye episoder
+podcastsettings.keep = Behold
+podcastsettings.keep.all = Alle episoder
+podcastsettings.keep.one = Nyeste episode
+podcastsettings.keep.many = Siste {0} episoder
+podcastsettings.download = N\u00e5r nye episoder er tilgjengelige
+podcastsettings.download.all = Last ned alle
+podcastsettings.download.one = Last ned nyeste
+podcastsettings.download.many = Last ned siste {0} episoder
+podcastsettings.download.none = Ikke gj\u00f8r noe
+podcastsettings.interval.manually = Manuelt
+podcastsettings.interval.hourly = Hver time
+podcastsettings.interval.daily = Hver dag
+podcastsettings.interval.weekly = Hver uke
+podcastsettings.folder = Lagre Podcaster i
+
+# playerSettings.jsp
+playersettings.noplayers = Ingen spillere registrert.
+playersettings.type = Type
+playersettings.lastseen = Sist sett
+playersettings.title = Velg spiller
+
+playersettings.technology.web.title = Web-spiller
+playersettings.technology.external.title = Ekstern spiller
+playersettings.technology.external_with_playlist.title = Ekstern spiller med spilleliste
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Spill musikk direkte i nettleseren ved \u00e5 bruke den integrerte Flash-spilleren.
+playersettings.technology.external.text = Spill musikk i din favorittmediaspiller, som WinAmp eller Windows Media Player.
+playersettings.technology.external_with_playlist.text = Samme som over, men la spillelisten bli kontrollert av spilleren istedenfor \
+ Subsonic-serveren. I denne modusen er det mulig \u00e5 spole innenfor sanger.
+playersettings.technology.jukebox.text = Spill musikk direkte gjennom lydsystemet til Subsonic-serveren. (Kun autoriserte brukere)
+playersettings.name = Navn p\u00e5 spiller
+playersettings.coverartsize = Bildest\u00f8rrelse
+playersettings.maxbitrate = Maksimal b\u00e5ndbredde
+playersettings.coverart.off = Av
+playersettings.coverart.small = Liten
+playersettings.coverart.medium = Middels
+playersettings.coverart.large = Stor
+playersettings.nolame = <em>NB!</em> LAME ser ikke ut til \u00e5 v\u00e6re installert.<br>Klikk p\u00e5 Hjelp-knappen for mer informasjon.
+playersettings.autocontrol = Kontroller spilleren automatisk
+playersettings.dynamicip = Spilleren har dynamisk IP-addresse
+playersettings.transcodings = Aktive konverterere
+playersettings.ok = Lagre
+playersettings.forget = Slett spilleren
+playersettings.clone = Lag kopi
+
+# userSettings.jsp
+usersettings.title = Velg bruker
+usersettings.newuser = Ny bruker
+usersettings.admin = Bruker er administrator
+usersettings.settings = Bruker har lov til \u00e5 endre innstillinger og passord
+usersettings.stream = Bruker har lov til \u00e5 spille filer
+usersettings.jukebox = Bruker har lov til \u00e5 spille filer i jukebox-modus
+usersettings.download = Bruker har lov til \u00e5 laste ned filer
+usersettings.upload = Bruker har lov til \u00e5 laste opp filer
+usersettings.playlist = Bruker har lov til \u00e5 opprette og slette spillelister
+usersettings.coverart = Bruker har lov til \u00e5 endre bilder og tags
+usersettings.comment = Bruker har lov til \u00e5 gi karakterer og kommentarer
+usersettings.podcast = Bruker har lov til \u00e5 administrere Podcaster
+usersettings.username = Brukernavn
+usersettings.password = Passord
+usersettings.changepassword = Bytt passord
+usersettings.newpassword = Nytt passord
+usersettings.confirmpassword = Bekreft passord
+usersettings.delete = Slett denne brukeren
+usersettings.ldap = Autentiser bruker i LDAP
+usersettings.useralreadyexists = Brukeren finnes allerede.
+usersettings.wrongpassword = Passordene var forskjellige.
+usersettings.ldapdisabled = LDAP-autentisering er ikke aktivert. Se avanserte innstillinger.
+usersettings.passwordnotsupportedforldap = Kan ikke sette eller endre passord for LDAP-autentiserte brukere
+usersettings.nopassword = Manglende passord.
+usersettings.nousername = Manglende brukernavn.
+usersettings.ok = Passordet for bruker {0} er endret.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Aldri
+musicfoldersettings.interval.one = Hver dag
+musicfoldersettings.interval.many = Hver {0}. dag
+musicfoldersettings.hour = kl {0}:00
+
+# main.jsp
+main.up = Opp
+main.playall = Spill alle
+main.playrandom = Spill tilfeldig
+main.addall = Legg til alle
+main.tags = Endre tags
+main.playcount = Spilt {0} ganger.
+main.lastplayed = Sist spilt {0}.
+main.comment = Kommentar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__tekst__</td><td>Fet tekst </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Linjeskift </td></tr>\
+ <tr><td style="padding-right:1em">~~tekst~~</td><td>Kursiv tekst </td><td style="padding-left:3em;padding-right:1em">(blank linje) </td><td>Nytt avsnitt</td></tr>\
+ <tr><td style="padding-right:1em">* tekst </td><td>Liste </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>link </td></tr>\
+ <tr><td style="padding-right:1em">1. tekst </td><td>Nummerert liste </td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Navngitt link</td></tr>\
+ </table>
+main.donate = Gi en <a href="{0}" style="text-decoration:underline">donasjon</a> til {1}!<br>(og fjern denne meldingen)
+main.nowplaying = Spilles n\u00e5
+main.lyrics = Tekst
+main.minutesago = minutter siden
+main.message = Skriv en melding
+
+# rating.jsp
+rating.rating = Karakter
+rating.clearrating = Slett karakter
+
+# coverArt.jsp
+coverart.change = Endre
+coverart.zoom = Forst\u00f8rr
+
+# allmusic.jsp
+allmusic.text = S\u00f8ker etter <em>{0}</em> p\u00e5 allmusic.com - Vennligst vent.
+
+# changeCoverArt.jsp
+changecoverart.title = Bytt bilde
+changecoverart.address = Eller angi adresse til bilde
+changecoverart.artist = Artist
+changecoverart.album = Album
+changecoverart.searchdiscogs = S\u00f8k p\u00e5 Discogs
+changecoverart.wait = Vennligst vent...
+changecoverart.success = Nedlasting av bilde var vellykket
+changecoverart.error = Nedlasting av bilde feilet
+changecoverart.noimagesfound = Ingen bilder funnet
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = En feil oppstod ved bytting av bilde:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Endre tags
+edittags.file = Fil
+edittags.track = Spor
+edittags.songtitle = Sang
+edittags.artist = Artist
+edittags.album = Album
+edittags.year = \u00c5r
+edittags.genre = Sjanger
+edittags.status = Status
+edittags.suggest = Foresl\u00e5
+edittags.reset = Still tilbake
+edittags.suggest.short = F
+edittags.reset.short = S
+edittags.set = Bruk
+edittags.working = Jobber
+edittags.updated = Oppdatert
+edittags.skipped = Ingen endring
+edittags.error = Feil
+
+# donate.jsp
+donate.title = Donasjon
+donate.invalidlicense = Ugyldig lisensn\u00f8kkel.
+donate.amount = Gi {0}
+donate.textbefore = <p>Takk for at du vil st\u00f8tte {0} ved \u00e5 gi en donasjon! \
+ Som donor vil du motta en lisensn\u00f8kkel som gj\u00f8r at du ikke vil bli p\u00e5minnet om \u00e5 gi flere donasjoner. \
+ Lisensn\u00f8kkelen vil v\u00e6re gyldig for denne og alle fremtidige versjoner av {0}.</p> \
+ <p>Foresl\u00e5tt bel\u00f8p er <b>&euro;20</b>, men du kan gi s\u00e5 mye eller lite du vil. \
+ Merk at lisensn\u00f8kkelen vil bli sendt til epost-adressen du angir, s\u00e5 pass p\u00e5 \u00e5 angi en gyldig \
+ adresse n\u00e5r du registrerer donasjonen hos PayPal.</p>
+donate.textafter = <p>Trykk p\u00e5 en av knappene for \u00e5 g\u00e5 til PayPal hvor du kan betale med kredittkort, eller ved \u00e5 \
+ bruke din PayPal konto hvis du har. Du vil motta lisensn\u00f8kkelen p\u00e5 epost s\u00e5 fort donasjonen er behandlet.</p> \
+ <p>Eventuelle sp\u00f8rsm\u00e5l kan sendes til \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Denne kopien av {2} ble lisensiert {1} til {0}. Takk for din st\u00f8tte!
+donate.register = Etter at du har mottatt lisensn\u00f8kkelen kan du registere den under.
+donate.register.email = Epost
+donate.register.license = Lisens
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast-mottaker
+podcastreceiver.expandall = Vis episoder
+podcastreceiver.collapseall = Skjul episoder
+podcastreceiver.status.new = Ny
+podcastreceiver.status.downloading = Laster ned
+podcastreceiver.status.completed = Ferdig
+podcastreceiver.status.error = Feil
+podcastreceiver.status.deleted = Slettet
+podcastreceiver.status.skipped = Skippet
+podcastreceiver.downloadselected = Last ned valgte
+podcastreceiver.deleteselected = Slett valgte
+podcastreceiver.confirmdelete = Vil du virkelig slette valgte Podcaster?
+podcastreceiver.check = Se etter nye episoder
+podcastreceiver.refresh = Oppdater side
+podcastreceiver.settings = Innstillinger
+podcastreceiver.subscribe = Abonnere p\u00e5 Podcast
+
+# lyrics.jsp
+lyrics.title = Sangtekst
+lyrics.artist = Artist
+lyrics.song = Sang
+lyrics.search = S\u00f8k
+lyrics.wait = S\u00f8ker etter sangtekst, vennligst vent...
+lyrics.courtesy = (Tekst fra <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Fant ingen tekst.
+
+# helpPopup.jsp
+helppopup.title = {0} - Hjelp
+helppopup.cover.title = Bildest\u00f8rrelse
+helppopup.cover.text = <p>Angir hvor store bildene med platecover skal v\u00e6re. Du kan ogs\u00e5 skru av bildevisning.</p>
+helppopup.transcode.title = Maksimal b\u00e5ndbredde
+helppopup.transcode.text = <p>Du kan sette en \u00f8vre grense for b\u00e5ndbredden som skal brukes for denne spilleren. Dette kan v\u00e6re aktuelt \
+ dersom du har begrenset b\u00e5ndbredde p\u00e5 nettverket ditt.</p> \
+ <p>Dersom du for eksempel har en mp3-fil som er kodet med 256 Kbps (kilobits per sekund), og setter \
+ maksimal b\u00e5ndbredde til 128, vil {0} automatisk re-kode mp3-str\u00f8mmen til 128 Kbps.</p> \
+ <p>Dette valget krever av LAME er installert. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ er en gratis mp3-koder av h\u00f8y-kvalitet, som kan lastes ned <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">her</a>.\
+ Du m\u00e5 installere LAME i SUBSONIC_HOME/transcode, eller i en mappe som er inkludert i PATH-variabelen.</p>
+helppopup.playlistfolder.title = Mappe for spillelister
+helppopup.playlistfolder.text = <p>Her angir du hvilken mappe spillelistene er plassert i.</p>
+helppopup.musicmask.title = M\u00f8nster for musikk
+helppopup.musicmask.text = <p>Her angir du hvilke filtyper som skal gjenkjennes som musikk.</p>
+helppopup.coverartmask.title = M\u00f8nster for bilder
+helppopup.coverartmask.text = <p>Her angir du hvilke filtyper som skal gjenkjennes som bilder av platecover.</p>
+helppopup.downsamplecommand.title = Downsample-kommando
+helppopup.downsamplecommand.text = <p>Her angir du kommandoen som skal kj\u00f8res for \u00e5 downsample til lavere b\u00e5ndbredde.</p>\
+ <p>(%s = Fila som skal konverteres, %b = Maks. b\u00e5ndbredde for spilleren)</p>
+helppopup.index.title = Indeks
+helppopup.index.text = <p>Her angir du hvordan indeksen \u00f8verst p\u00e5 siden skal se ut. Denne indeksen gj\u00f8r deg i stand til \u00e5 \
+ raskt hoppe til mapper som ligger p\u00e5 \u00f8verste niv\u00e5 i musikk-mappen din.</p> \
+ <p>Spesifikasjonen er en mellomromseparert liste av indekselementer. Vanligvis er hvert element en enkelt \
+ bokstav, men du kan ogs\u00e5 angi flere bokstaver. For eksempel vil elementet <em>The</em> lenke til alle mapper \
+ som starter med "The".</p> \
+ <p>Du kan ogs\u00e5 lage et element best\u00e5ende av flere bokstaver i parentes. For eksempel vil elementet \
+ <em>A-E(ABCDE)</em> vises som <em>A-E</em>, og lenke til alle mapper som starter med enten A, B, C, D eller E. \
+ Dette kan v\u00e6re nyttig for \u00e5 gruppere sjeldent brukte bokstaver (f.eks X, Y, Z, \u00c6, \u00d8, \u00c5), eller for \u00e5 gruppere \
+ bokstaver med aksenter (f.eks A, \u00c0 and \u00c1)</p> \
+ <p>Filer og mapper som ikke dekkes av noen indekselementer blir plassert under elementet "#".</p>
+helppopup.ignoredarticles.title = Utelatte artikler
+helppopup.ignoredarticles.text = <p>Her kan du angi en liste av artikler (for eksempel "The") som skal utelates n\u00e5r indeksen lages.</p>
+helppopup.language.title = Spr\u00e5k
+helppopup.language.text = <p>Her kan du velge hvilket spr\u00e5k {0} skal bruke.</p>
+helppopup.visibility.title = Vis
+helppopup.visibility.text = <p>Velg hvilke detaljer som skal vises for hver sang. Du kan ogs\u00e5 angi en grense ("Maks. lengde") for hvor \
+ mange bokstaver som skal vises for tittel, album og artist.</p>
+helppopup.partymode.title = Festmodus
+helppopup.partymode.text = <p>N\u00e5r festmodus er aktivert er brukergrensesnittet forenklet og enklere \u00e5 bruke for mindre erfarne brukere. \
+ Tilfeldig \u00f8deleggelse av aktiv spilleliste blir for eksempel unng\u00e5tt.</p>
+helppopup.welcomemessage.title = Velkomstmelding
+helppopup.welcomemessage.text = <p>Her kan du angi meldingen som skal vises p\u00e5 hjemmesiden til {0}.</p>
+helppopup.coverartlimit.title = Maks bildeantall
+helppopup.coverartlimit.text = <p>Her kan du angi det maksimale antall bilder som skal vises p\u00e5 en side.</p>
+helppopup.shortcuts.title = Snarveier
+helppopup.shortcuts.text = <p>Her kan du angi en liste av mapper (p\u00e5 toppniv\u00e5) som du vil at {0} skal vise snarveier til. \
+ Bruk anf\u00f8rselstegn for \u00e5 gruppere ord, for eksempel:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.loginmessage.title = Innloggingsmelding
+helppopup.loginmessage.text = <p>Meldingen som vises p\u00e5 innloggingssiden.</p>
+helppopup.theme.title = Utseende
+helppopup.theme.text = <p>Her kan du velge utseende til {0}. Forskjellige utseende har forskjellige farger, skrfttyper, bilder osv.</p>
+helppopup.downloadlimit.title = Maks nedlastingsfart
+helppopup.downloadlimit.text = <p>Her kan du sette en \u00f8vre grense for hvor mye b\u00e5ndbredde som kan brukes for \u00e5 laste ned filer.</p>
+helppopup.uploadlimit.text = <p>Her kan du sette en \u00f8vre grense for hvor mye b\u00e5ndbredde som kan brukes for \u00e5 laste opp filer.</p>
+helppopup.uploadlimit.title = Maks opplastingsfart
+
+helppopup.streamport.title = Ikke-SSL stream port
+helppopup.streamport.text = <p>Dette valget er bare relevant dersom du kj\u00f8rer {0} p\u00e5 en server med SSL (HTTPS).</p><p>Noen spillere \
+ (for eksempel Winamp), st\u00f8tter ikke avspilling over SSL. Angi portnummeret for vanlig http (vanligvis 80 \
+ eller 4040) hvis du ikke vil at avspillingen skal foreg\u00e5 over SSL. Merk at avspillingsstr\u00f8mmen da ikke \
+ vil bli kryptert.</p>
+helppopup.ldap.title = LDAP-autentisering
+helppopup.ldap.text = <p>Brukere kan autentiseres mot en ekstern LDAP-server (f.eks Windows Active Directory). \
+ N\u00e5r LDAP-aktiverte brukere logger p\u00e5 {0} blir brukernavnet og passordet verifisert mot den eksterne \
+ serveren, og ikke av {0} selv.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>URL-en til LDAP-serveren. Protokollen m\u00e5 v\u00e6re enten <em>ldap://</em> eller <em>ldaps://</em> \
+ (for LDAP over SSL). Se <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">her</a> \
+ for en mer detaljert beskrivelse.</p>
+helppopup.ldapsearchfilter.title = S\u00f8kefilter for LDAP
+helppopup.ldapsearchfilter.text = <p>Filteruttrykk brukt i brukers\u00f8k. Dette er et LDAP-s\u00f8kefilter \
+ (som definert i <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Filterutrykket "'{0'}" blir erstattet med brukernavn, for eksempel: \
+ <ul>\
+ <li>(uid='{0'}) - dette vil s\u00f8ke etter et brukernavn som treffer p\u00e5 uid-attributtet.</li> \
+ <li>(sAMAccountName='{0'}) - typisk brukt for autentisering i Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = Administrator-DN for LDAP
+helppopup.ldapmanagerdn.text = <p>Dersom LDAP-serveren ikke st\u00f8tter anonym binding m\u00e5 du spesifisere DN \
+ (<em>Distinguished Name</em>) og passord for LDAP-brukeren som skal brukes ved binding.</p>
+helppopup.ldapautoshadowing.title = Automatisk opprett LDAP-brukere i {0}
+helppopup.ldapautoshadowing.text = <p>Med dette valget aktivt trenger ikke brukere i LDAP \u00e5 bli manuelt opprettet i {0} f\u00f8r innlogging.</p> \
+ <p>MERK! Dette betyr at enhver bruker med et gyldig LDAP-brukernavn og -passord kan logge inn i {0}, \
+ som ikke n\u00f8dvendigvis er det du vil.</p>
+helppopup.playername.title = Navn p\u00e5 spiller
+helppopup.playername.text = <p>Her kan du angi et navn p\u00e5 spilleren som er lett \u00e5 huske, for eksempel "Arbeid" eller "Stue".</p>
+helppopup.autocontrol.title = Kontroller spilleren automatisk
+helppopup.autocontrol.text = <p>Angir om {0} skal starte spilleren automatisk n\u00e5r du trykker "Start" i spillelista. Hvis ikke \
+ dette valget er skrudd p\u00e5, m\u00e5 du selv starte spilleren og koble den til {0}.</p>
+helppopup.dynamicip.title = Dynamisk IP-adresse
+helppopup.dynamicip.text = <p>Skru av dette valget hvis spilleren bruker en fast (statisk) IP-adresse.</p>
+
+# wap/index.jsp
+wap.index.missing = Ingen musikk funnet
+wap.index.playlist = Spilleliste
+wap.index.search = S\u00f8k
+wap.index.settings = Innstillinger
+
+# wap/browse.jsp
+wap.browse.playone = Spill sang
+wap.browse.playall = Spill alle
+wap.browse.addone = Legg til sang
+wap.browse.addall = Legg til alle
+wap.browse.downloadone = Last ned sang
+wap.browse.downloadall = Last ned alle
+
+# wap/playlist.jsp
+wap.playlist.title = Spilleliste
+wap.playlist.noplayer = Ingen spiller tilkoblet
+wap.playlist.clear = T\u00f8m
+wap.playlist.load = \u00c5pne
+wap.playlist.random = Tilfeldig
+wap.playlist.play = Spill p\u00e5 telefon
+
+# wap/search.jsp
+wap.search.title = S\u00f8k
+
+# wap/searchResult.jsp
+wap.searchresult.index = S\u00f8keindeksen er i ferd med \u00e5 bli opprettet. Pr\u00f8v p\u00e5 nytt senere.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Velg spiller
+wap.settings.allplayers = Alle
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pl.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pl.properties
new file mode 100644
index 00000000..9e749dc0
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pl.properties
@@ -0,0 +1,729 @@
+#
+# Polish localization.
+# Author: Micha\u0142 Kotas (miko@mikofoto.net)
+# Version: 2.0.0
+# Last update: 12-10-2011
+
+common.home = G\u0142\u00f3wna
+common.back = Cofnij
+common.help = Pomoc
+common.play = Odtw\u00f3rz
+common.add = Dodaj
+common.download = Pobierz
+common.close = Zamknij
+common.refresh = Od\u015bwie\u017c
+common.next = Nast\u0119pny
+common.previous = Poprzedni
+common.more = Wi\u0119cej
+common.ok = OK
+common.cancel = Anuluj
+common.save = Zapisz
+common.create = Utw\u00f3rz
+common.delete = Usu\u0144
+common.unknown = (Nieznany)
+common.default = (Standardowy)
+
+# login.jsp
+login.username = U\u017cytkownik
+login.password = Has\u0142o
+login.login = Zaloguj
+login.remember = Pami\u0119taj mnie
+login.logout = Obecnie jeste\u015b wylogowany
+login.error = Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o.
+login.insecure = konto {0} jest niezabezpieczone. Prosz\u0119 zalogowa\u0107 si\u0119 jako u\u017cytkownik "admin" <br>has\u0142o "admin", lub klikn\u0105\u0107 <a href="login.view?user=admin&amp;password=admin">tutaj</a>, aby zmieni\u0107 has\u0142o natychmiast.
+
+# accessDenied.jsp
+accessDenied.title = Odmowa dost\u0119pu
+accessDenied.text = Przepraszamy, nie masz uprawnie\u0144 do wykoanania tej czynno\u015bci.
+
+# top.jsp
+top.home = G\u0142\u00f3wna
+top.now_playing = Odtwarzane
+top.settings = Ustawienia
+top.status = Status
+top.podcast = Podcasty
+top.more = Wi\u0119cej
+top.help = Pomoc
+top.search = Szukaj
+top.upgrade = <b>Uwaga!</b> Dost\u0119pna jest nowa wersja.<br>Pobierz {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">tutaj</a>.
+top.missing = Brak folder\u00f3w medi\u00f3w. Prosz\u0119 zmieni\u0107 ustawienia.
+top.logout = Wyloguj {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;wykonawc\u00f3w<br>\
+ {1}&nbsp;album\u00f3w<br>\
+ {2}&nbsp;utwor\u00f3w<br>\
+ {3} (&#126; {4} godzin)
+left.shortcut = Skr\u00f3ty
+left.radio = Internet TV/radio
+left.allfolders = Wszystkie foldery
+
+# playlist.jsp
+playlist.stop = Zatrzymaj
+playlist.start = Odtwarzaj
+playlist.confirmclear = Na pewno wyczy\u015bci\u0107 playlist\u0119?
+playlist.clear = Wyczy\u015b\u0107
+playlist.shuffle = Losowo
+playlist.repeat_on = Powtarzanie w\u0142.
+playlist.repeat_off = Powtarzanie wy\u0142.
+playlist.undo = Cofnij
+playlist.settings = Ustawienia
+playlist.more = Wi\u0119cej...
+playlist.more.playlist = Playlista
+playlist.more.sortbytrack = Sortuj wg utwor\u00f3w
+playlist.more.sortbyartist = Sortuj wg wykonawc\u00f3w
+playlist.more.sortbyalbum = Sortuj wg album\u00f3w
+playlist.more.selection = Wybrane utwory
+playlist.more.selectall = Zaznacz wszystkie
+playlist.more.selectnone = Odznacz wszystkie
+playlist.getflash = Pobierz Flash Player
+playlist.load = Wczytaj
+playlist.save = Zapisz
+playlist.append = Dodaj do playlisty
+playlist.remove = Usu\u0144
+playlist.up = W g\u00f3r\u0119
+playlist.down = W d\u00f3\u0142
+playlist.empty = Playlista jest pusta
+
+# videoPlayer.jsp
+videoPlayer.getflash = Prosz\u0119 zainstalowa\u0107 Flash Player
+videoPlayer.popout = Otw\u00f3rz w nowym oknie
+
+# status.jsp
+status.title = Status
+status.type = Typ
+status.stream = Strumie\u0144
+status.download = Download
+status.upload = Upload
+status.player = Odtwarzacz
+status.user = U\u017cytkownik
+status.current = Bie\u017c\u0105cy plik
+status.transmitted = Wys\u0142ane
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Szukaj
+search.query = Wykonawca, album lub tytu\u0142 utworu
+search.search = Szukaj
+search.index = Indeks wyszukiwania jest aktualnie tworzony. Prosz\u0119 spr\u00f3bowa\u0107 po\u017aniej.
+search.hits.none = Brak wynik\u00f3w spe\u0142niaj\u0105cych kryteria
+search.hits.more = Wi\u0119cej
+search.hits.artists = Wykonawcy
+search.hits.albums = Albumy
+search.hits.songs = Utwory
+
+# gettingStarted.jsp
+gettingStarted.title = Pierwsze kroki
+gettingStarted.text = <p>Witamy w Subsonic! Konfiguracja zajmie tylko chwil\u0119, wystarczy wykona\u0107 poni\u017csze czynno\u015bci.<br> \
+ Kliknij przycisk "G\u0142\u00f3wna" na pasku narz\u0119dzi powy\u017cej, aby wr\u00f3ci\u0107 do tego ekranu.</p> \
+ <p>Wi\u0119cej informacji mo\u017cna znale\u017a\u0107 w <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Instrukcji Obs\u0142ugi</b></a>.</p>
+gettingStarted.step1.title = Zmie\u0144 has\u0142o administratora.
+gettingStarted.step1.text = Zabezpiecz sw\u00f3j serwer zmieniaj\u0105c domyslne has\u0142o administratora. \
+ Mo\u017cesz tak\u017ce stworzy\u0107 nowe konta u\u017cytkownik\u00f3w oraz okre\u015bli\u0107 uprawnienia.
+gettingStarted.step2.title = Ustaw foldery medi\u00f3w.
+gettingStarted.step2.text = Okre\u015bl lokalizacje plikow muzycznych i film\u00f3w
+gettingStarted.step3.title = Skonfiguruj sie\u0107.
+gettingStarted.step3.text = Kilka przydatnych opcji pozwalaj\u0105cych s\u0142ucha\u0107 muzyki zdalnie przez Internet, \
+ lub podzieli\u0107 si\u0119 ni\u0105 z rodzin\u0105 i przyjaci\u00f3\u0142mi. Stw\u00f3rz w\u0142asny adres <b><em>twojadres</em>.subsonic.org</b>
+
+gettingStarted.hide = Nie pakazuj ponownie
+gettingStarted.hidealert = Aby pokaza\u0107 ten ekran ponownie, przejd\u017a do Ustawienia &gt; Og\u00f3lne.
+
+# home.jsp
+home.random.title = Losowe
+home.newest.title = Najnowsze
+home.highest.title = Najwy\u017cej oceniane
+home.frequent.title = Najcz\u0119\u015bciej odtwarzane
+home.recent.title = Ostatnio odtwarzane
+home.users.title = U\u017cytkownicy
+home.random.text = Losowe albumy
+home.newest.text = Ostatnio dodane lub zmodyfikowane albumy
+home.highest.text = Najwy\u017cej ocenione albumy
+home.frequent.text = Najcz\u0119\u015bciej odtwarzane albumy
+home.recent.text = Ostatnio odtwarzane albumy
+home.users.text = Statystyki u\u017cytkownik\u00f3w
+home.scan = Folder medi\u00f3w jest obecnie skanowany. Wszystkie funkcje nie s\u0105 jeszcze dost\u0119pne.
+home.listsize = {0} album\u00f3w na stron\u0119
+home.albums = Albumy {0} - {1}
+home.playcount = Played {0} songs
+home.lastplayed = Played {0}
+home.created = Modified {0}
+home.chart.total = \u0141\u0105cznie (MB)
+home.chart.stream = Przes\u0142anych (MB)
+home.chart.download = Pobranych (MB)
+home.chart.upload = Wys\u0142anych (MB)
+
+# more.jsp
+more.title = Wi\u0119cej
+more.random.title = Losowa playlista
+more.random.text = Utw\u00f3rz playlist\u0119 z
+more.random.songs = {0} utwor\u00f3w
+more.random.auto = Odtwarzaj kolejne losowane utwory po osi\u0105cni\u0119ciu ko\u0144ca playlisty.
+more.random.ok = OK
+more.random.genre = gatuntu
+more.random.anygenre = Dowolny
+more.random.year = z roku
+more.random.anyyear = Dowolny
+more.random.folder = w folderze
+more.random.anyfolder = Dowolny
+more.apps.title = Subsonic Apps
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Aplikacje Subsonic</a> s\u0105 dost\u0119pna dla systemu <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone</b> oraz <b>AIR</b>.</p>
+more.mobile.title = Telefon kom\u00f3rkowy
+more.mobile.text = <p>Mo\u017cesz kontrolowa\u0107 {0} przy pomocy dowolnego telefonu lub PDA wyposa\u017conego w WAP.<br> \
+ Wystarczy uruchomi\u0107 nast\u0119puj\u0105cy adres URL z poziomu telefonu: <b>http://yourhostname/wap</b></p> \
+ <p>Wymagane jest aby tw\u00f3j serwer by\u0142 dost\u0119pny w Internecie</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Zapisane playlisty s\u0105 dost\u0119pne jako Podscasty<br>\
+ U\u017cyj nast\u0119puj\u0105cego adresu URL aby doda\u0107 Podcasty do twojego czytnika: <b>http://yourhostname/podcast</b>, \
+ lub <b><a href="podcast.view?suffix=.rss">kliknij tutaj</a>.</b></p>
+more.upload.title = Prze\u015blij plik
+more.upload.source = Wybierz plik
+more.upload.target = Prze\u015blij do
+more.upload.browse = Wybierz
+more.upload.ok = Prze\u015blij
+more.upload.unzip = Automatycznie rozpakuj pliki zip.
+more.upload.progress = uko\u0144czono %. Prosz\u0119 czeka\u0107...
+
+# upload.jsp
+upload.title = Przesy\u0142anie pliku
+upload.success = Pomy\u015blnie przes\u0142ano <b>{0}</b>
+upload.empty = Brak plik\u00f3w do przes\u0142ania.
+upload.failed = Przesy\u0142anie nie powiod\u0142o si\u0119 z powodu b\u0142\u0119du:<br><b>"{0}"</b>
+upload.unzipped = Rozpakowano {0}
+
+# help.jsp
+help.title = O {0}
+help.upgrade = <b>Uwaga!</b> Dost\u0119pna jest nowa wersja. Pobierz {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">tutaj</a>.
+help.version.title = Wersja
+help.builddate.title = Data wydania
+help.server.title = Serwer
+help.license.title = Licencja
+help.license.text = {0} jest darmowym oprogramowaniem rozpowszechnianym na licencji typu open-source - <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>. \
+ {0} wykorzystuje <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licencjonowane biblioteki zewn\u0119trznych dostawc\u00f3w</a>.
+
+help.homepage.title = Strona domowa
+help.forum.title = Forum
+help.shop.title = Sklep
+help.contact.title = Kontakt
+help.contact.text = {0} jest rozwijany i utrzymywany przez Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Je\u015bli masz pytania, uwagi lub sugestie, zapraszam na \
+ <a href="http://forum.subsonic.org" target="_blank">forum Subsonic</a>.
+help.donate = {0} jest darmowym oprogramowaniem, jednak mo\u017cesz si\u0119 przyczyni\u0107 do jego rozwoju poprzez <b><a href="donate.view?">darowizn\u0119</a></b>.
+help.log = Log
+help.logfile = Pe\u0142ny log mo\u017cna odnale\u017a\u0107 w {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Ustawienia
+settingsheader.general = Og\u00f3lne
+settingsheader.advanced = Zaawansowane
+settingsheader.personal = Osobiste
+settingsheader.musicFolder = Foldery medi\u00f3w
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcasty
+settingsheader.player = Odtwarzacze
+settingsheader.share = Media wsp\u00f3\u0142dzielone
+settingsheader.network = Sie\u0107
+settingsheader.transcoding = Transkodowanie
+settingsheader.user = U\u017cytkownicy
+settingsheader.search = Szukaj
+settingsheader.coverArt = Cover art
+settingsheader.password = Has\u0142o
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Folder playlist
+generalsettings.musicmask = Maska plik\u00f3w muzycznych
+generalsettings.videomask = Maska plik\u00f3w wideo
+generalsettings.coverartmask = Maska ok\u0142adek
+generalsettings.index = Indeks
+generalsettings.ignoredarticles = Ignorowane wyra\u017cenia
+generalsettings.shortcuts = Skr\u00f3ty
+generalsettings.showgettingstarted = Poka\u017c "Pierwsze kroki" na stronie g\u0142\u00f3wnej
+generalsettings.welcometitle = Tytu\u0142 powitania
+generalsettings.welcomesubtitle = Podtytu\u0142 powitania
+generalsettings.welcomemessage = Wiadomo\u015b\u0107 powitalna
+generalsettings.loginmessage = Wiadomo\u015b\u0107 przy logowaniu
+generalsettings.language = Standardowy j\u0119zyk
+generalsettings.theme = Standardowy motyw
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Komenda downsamplingu
+advancedsettings.coverartlimit = Limit ok\u0142adek<br><div class="detail">(0 = Brak limitu)</div>
+advancedsettings.downloadlimit = Limit pobierania (Kbps)<br><div class="detail">(0 = Brak limitu)</div>
+advancedsettings.uploadlimit = Limit wysy\u0142ania (Kbps)<br><div class="detail">(0 = Brak limitu)</div>
+advancedsettings.streamport = Port strumieniowania Nie-SSL <br><div class="detail">(0 = Wy\u0142\u0105czony)</div>
+advancedsettings.ldapenabled = W\u0142\u0105cz uwierzytelnianie LDAP
+advancedsettings.ldapurl = Adres URL LDAP
+advancedsettings.ldapsearchfilter = filtr szukania LDAP
+advancedsettings.ldapmanagerdn = Menad\u017cer DN LDAP <br><div class="detail">(Opcjonalne)</div>
+advancedsettings.ldapmanagerpassword = Has\u0142o
+advancedsettings.ldapautoshadowing = Automatycznie tworzenie u\u017cytwkonik\u00f3w w {0}
+
+# personalSettings.jsp
+personalsettings.title = Ustawienia osobiste dla {0}
+personalsettings.language = J\u0119zyk
+personalsettings.theme = Motyw
+personalsettings.display = Wy\u015bwietl
+personalsettings.browse = Przegl\u0105danie
+personalsettings.playlist = Playlista
+personalsettings.tracknumber = Nr. utworu
+personalsettings.artist = Wykonawca
+personalsettings.album = Album
+personalsettings.genre = Gatunek
+personalsettings.year = Rok
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Czas trwania
+personalsettings.format = Format
+personalsettings.filesize = Rozmiar pliku
+personalsettings.captioncutoff = Skr\u00f3cenie podpisu
+personalsettings.partymode = Tryb imprezy
+personalsettings.shownowplaying = Poka\u017c co odtwarzaj\u0105 inni
+personalsettings.nowplayingallowed = Pozw\u00f3l innym widzie\u0107 co odtwarzam
+personalsettings.showchat = Poka\u017c wiadomo\u015bci chat
+personalsettings.finalversionnotification = Powiadom mnie o nowej wersji
+personalsettings.betaversionnotification = Powiadom mnie o nowej beta wersji
+personalsettings.lastfmenabled = Rejestuj co odtwarzam na <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = U\u017cytkownik Last.fm
+personalsettings.lastfmpassword = Has\u0142o Last.fm
+personalsettings.avatar.title = M\u00f3j obrazek
+personalsettings.avatar.none = Bez obrazka
+personalsettings.avatar.custom = W\u0142asny obrazek
+personalsettings.avatar.changecustom = Zmie\u0144 w\u0142asny obrazek
+personalsettings.avatar.upload = Wczytaj
+personalsettings.avatar.courtesy = Ikony dost\u0119pne dzi\u0119ki uprzejmo\u015bci <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, oraz \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Zmien osobisty obrazek
+avataruploadresult.success = Osobisty obrazek"{0}" pomy\u015blnie wczytany.
+avataruploadresult.failure = B\u0142\u0105d podczas wczytywania osobistego obrazka. Dla szczeg\u00f3\u0142\u00f3w zobacz <a href="help.view?">log</a>.
+
+# passwordSettings.jsp
+passwordsettings.title = Zmie\u0144 has\u0142o dla u\u017cytkownika{0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Folder
+musicfoldersettings.name = Nazwa
+musicfoldersettings.enabled = Aktywny
+musicfoldersettings.add = Dodaj folder medi\u00f3w
+musicfoldersettings.nopath = Prosz\u0119 okre\u015bli\u0107 folder.
+
+# networkSettings.jsp
+networksettings.text = U\u017cyj poni\u017cszych ustawie\u0144, aby kontrolowa\u0107 jak server Subsonic jest dost\u0119pne poprzez Internet.<br> \
+ Je\u015bli masz problemy, zajrzyj do <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Instrukcji Obs\u0142ugi</b></a>.
+networksettings.portforwardingenabled = Skonfiguruj router automatyczne, aby zezwoli\u0107 na po\u0142\u0105czenia przychodz\u0105ce do Subsonic (przekazywanie port\u00f3w UPnP lub NAT-PMP).
+networksettings.portforwardinghelp = Je\u015bli router nie mo\u017ce zostac skonfigurowanie automatycznie, mo\u017cna ustawi\u0107 go r\u0119cznie. \
+ Post\u0119puj zgodnie z instrukcj\u0105 na <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Musisz przekierowa\u0107 port {0} do komputera na kt\u00f3rym znajduje si\u0119 serwer Subsonic
+networksettings.urlredirectionenabled = Uzyskaj dost\u0119p do serwera przez Internet poprzez \u0142atwy do zapami\u0119tania adres.
+networksettings.status = Status:
+networksettings.trialexpired = Okres pr\u00f3bny up\u0142yn\u0105\u0142 w dniu {0}. Prosimy o <b><a href="donate.view?">wsparcie</a></b> aby w\u0142\u0105czy\u0107 t\u0119 opcj\u0119 na sta\u0142e.
+networksettings.trialnotexpired = Opcja jest dost\u0119pna do {0}. Po tym okresie prosimy o <b><a href="donate.view?">wsparcie</a></b> aby u\u017cywa\u0107 jej na sta\u0142e.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nazwa
+transcodingsettings.sourceformat = Konwertuj z
+transcodingsettings.targetformat = Konwertuj do
+transcodingsettings.step1 = Krok 1
+transcodingsettings.step2 = Krok 2
+transcodingsettings.step3 = Krok 3
+transcodingsettings.defaultactive = Standadrowe
+transcodingsettings.enabled = Aktywny
+transcodingsettings.add = Dodaj transkodowanie
+transcodingsettings.recommended = Zalecana konfiguracja
+transcodingsettings.noname = Prosz\u0119 okre\u015bli\u0107 nazw\u0119
+transcodingsettings.nosourceformat = Prosz\u0119 okre\u015bli\u0107 format \u017ar\u00f3d\u0142owy
+transcodingsettings.notargetformat = Prosz\u0119 okre\u015bli\u0107 format docelowy
+transcodingsettings.nostep1 = Prosz\u0119 okre\u015bli\u0107 przynajmniej jeden krok transkodowania
+transcodingsettings.info = <p class="detail">(%s = Plik kt\u00f3ry b\u0119dzie transkodowany, %b = Maksymalny bitrate dla odtwarzacza)</p> \
+ <p>Transkodowanie jest procesem konwerowania danego formatu medi\u00f3w na inny. Transkodowanie {1} \
+ umo\u017cliwia strumieniowanie medi\u00f3w, kt\u00f3re standardowo nie posiadaj\u0105 takiej mo\u017cliwo\u015bci. Proces transkodowania odbywa si\u0119 w locie i nie wymaga \
+ dodatkowej przestrzeni dyskowej.<p/> \
+ <p>Obecnie transkodowanie realizowane jest z wykorzystaniem program\u00f3w lini polece\u0144 dostawc\u00f3w zewn\u0119trznych. Programy te musz\u0105 by\u0107 zainstalowane w {0}. \
+ Pakiet transkoder\u00f3w dla Windows \
+ jest dost\u0119pny <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>tutaj</b></a>. Mo\u017cesz tak\u017ce doda\u0107 w\u0142asny transkoder, pod warunkiem i\u017c \
+ spe\u0142nia on warunki: \
+ <ul> \
+ <li>Posiada interfejs lini polece\u0144</li> \
+ <li>Posiada mo\u017cliwo\u015b\u0107 przekazania wynik\u00f3w na standradowe wyj\u015bcie (stdout).</li> \
+ <li>Przy wykorzystaniu w kroku 2 lub 3, musi posiada\u0107 mo\u017cliwo\u015b\u0107 pobrania wynik\u00f3w ze standardowego wej\u015bcia (stdin).</li> \
+ </ul> \
+ </p> \
+ <p> Transkodery s\u0105 aktywowane z poziomu ustawie\u0144 poszczeg\u00f3lnych odtwarzaczy. W przypadku wybrania pola "Domy\u015blnie", transkoder \
+ aktywowany jest automatycznie dla nowo definiowanych odtwarzaczy.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL Strumienia
+internetradiosettings.homepageurl = Strona domowa
+internetradiosettings.name = Nazwa
+internetradiosettings.enabled = Aktywny
+internetradiosettings.add = Dodaj TV Internetow\u0105/radio
+internetradiosettings.nourl = Prosz\u0119 okre\u015bli\u0107 adres URL.
+internetradiosettings.noname = Prosz\u0119 okre\u015bli\u0107 nazw\u0119
+
+# podcastSettings.jsp
+podcastsettings.update = Sprawd\u017a czy s\u0105 dost\u0119pne nowe odcinki
+podcastsettings.keep = Zachowaj
+podcastsettings.keep.all = Wszystkie odcinki
+podcastsettings.keep.one = Najnowsze odcinki
+podcastsettings.keep.many = Ostatnie {0} odcink\u00f3w
+podcastsettings.download = Gdy nowe odcinki s\u0105 dost\u0119pne
+podcastsettings.download.all = Pobierz wszystkie
+podcastsettings.download.one = Pobierz najnowsze
+podcastsettings.download.many = Pobierz ostatnie {0} odcink\u00f3w
+podcastsettings.download.none = Pomi\u0144
+podcastsettings.interval.manually = R\u0119cznie
+podcastsettings.interval.hourly = Co godzin\u0119
+podcastsettings.interval.daily = Codziennie
+podcastsettings.interval.weekly = Co tydzie\u0144
+podcastsettings.folder = Zapisz Podcast
+
+# playerSettings.jsp
+playersettings.noplayers = Nie odnaleziono odtwarzacza
+playersettings.type = Typ
+playersettings.lastseen = Ostatnio u\u017cywany
+playersettings.title = Wybierz odtwarzacz
+
+playersettings.technology.web.title = Odtwarzacz Web
+playersettings.technology.external.title = Odtwarzacz zewn\u0119trzny
+playersettings.technology.external_with_playlist.title = Odtwarzacz zewn\u0119trzny z playlist\u0105
+playersettings.technology.jukebox.title = Szafa graj\u0105ca
+playersettings.technology.web.text = Odtwarzaj muzyk\u0119 bezpo\u015brednio w przegl\u0105darce za pomoc\u0105 odtwarzacza Flash.
+playersettings.technology.external.text = Odtwarzaj muzyk\u0119 w ulubionym programie, takim jak WinAmp czy Windows Media Player.
+playersettings.technology.external_with_playlist.text = Podobnie jak wy\u017cej, jednak playlista zarz\u0105dzana jest przez odtwarzacz \
+ a nie serwer Subsonic. W tym trybie mo\u017cliwe jest pomijanie utwor\u00f3w.
+playersettings.technology.jukebox.text = Odtwarzaj muzyk\u0119 bezpo\u015brednio na urz\u0105dzeniu audio serwera gdzie zainstalowany jest Subsonic. (jedynie zarejestowanie u\u017cytkownicy).
+playersettings.name = Nazwa odtwarzacza
+playersettings.coverartsize = Rozmiar ok\u0142adek
+playersettings.maxbitrate = Maksymalny bitrate
+playersettings.coverart.off = Wy\u0142\u0105cz
+playersettings.coverart.small = Ma\u0142y
+playersettings.coverart.medium = \u015aredni
+playersettings.coverart.large = Du\u017cy
+playersettings.nolame = <em>Notice:</em> LAME prawdopodobnie nie jest zainstalowany.<br>Kliknij przycisk pomoc aby uzyska\u0107 wi\u0119cej informacji.
+playersettings.autocontrol = Automatycznie kontroluj odtwarzanie
+playersettings.dynamicip = Odtwarzaj\u0105cy posiada dynamiczny adres IP
+playersettings.transcodings = Aktywuj transkodowanie
+playersettings.ok = Zapisz
+playersettings.forget = Usu\u0144 odtwarzacz
+playersettings.clone = Klonuj odtwarzacz
+
+# shareSettings.jsp
+sharesettings.name = Nazwa
+sharesettings.owner = Udost\u0119pnione przez
+sharesettings.description = Opis
+sharesettings.visits = Wizyt
+sharesettings.lastvisited = Ostatnia wizyta
+sharesettings.expires = Wygasa
+sharesettings.files = Udost\u0119pnione pliki
+sharesettings.expirein = Wygasa w
+sharesettings.expirein.week = 1t
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1r
+sharesettings.expirein.never = Nigdy
+
+# userSettings.jsp
+usersettings.title = Wybierz u\u017cytkownika
+usersettings.newuser = Nowy u\u017cytkownik
+usersettings.admin = U\u017cytkownik jest administratorem
+usersettings.settings = U\u017cytownik mo\u017ce zmienia\u0107 ustawienia oraz has\u0142o
+usersettings.stream = U\u017cytkownik mo\u017ce odtwarza\u0107 pliki
+usersettings.jukebox = U\u017cytkownik mo\u017ce odtwarza\u0107 pliki w trybie szafy graj\u0105cej
+usersettings.download = U\u017cytkownik mo\u017ce pobiera\u0107 pliki
+usersettings.upload = U\u017cytkownik mo\u017ce przesy\u0142a\u0107 pliki na serwer
+usersettings.share = U\u017cytkownik ma prawo do udost\u0119pniania plik\u00f3w ka\u017cdemu
+usersettings.playlist= U\u017cytkownik mo\u017ce tworzy\u0107 oraz usuwa\u0107 playlisty
+usersettings.coverart = U\u017cytkownik mo\u017ce zmienia\u0107 ok\u0142adki oraz tagi
+usersettings.comment= U\u017cytkownik mo\u017ce dodawa\u0107, edytowa\u0107 komentarze i oceny
+usersettings.podcast= U\u017cytkownik mo\u017ce zarz\u0105dza\u0107 podcastami
+usersettings.username = Nazwa u\u017cytwkownika
+usersettings.email = Adres email
+usersettings.changepassword = Zmie\u0144 has\u0142o
+usersettings.password = Has\u0142o
+usersettings.newpassword = Nowe has\u0142o
+usersettings.confirmpassword = Potwierd\u017a has\u0142o
+usersettings.delete = Usu\u0144 tego u\u017cytwkonika
+usersettings.ldap = Uwierzytelniaj u\u017cywtkownika za pomoc\u0105 LDAP
+usersettings.nousername = Brak nazwy u\u017cytwkonika.
+usersettings.noemail= Nieprawid\u0142owy adres email.
+usersettings.useralreadyexists = U\u017cytkownik ju\u017c istnieje.
+usersettings.nopassword = Has\u0142o jest wymagane.
+usersettings.wrongpassword = Has\u0142a nie s\u0105 zgodne.
+usersettings.ldapdisabled = Uwierzytelnianie LDAP jest nieaktywne. Prosz\u0119 sprawdzi\u0107 ustawienia zaawansowane.
+usersettings.passwordnotsupportedforldap = Nie mo\u017cna ustawi\u0107 lub zmieni\u0107 has\u0142a u\u017cytkownika dla uwierzytelniania LDAP
+usersettings.ok = Pomy\u015blnie zmieniono has\u0142o dla u\u017cytkownika {0}.
+
+# searchSettings.jsp
+searchsettings.auto = Aktualizuj indeks wyszukiwania automatycznie
+searchsettings.manual = Aktualizuj indeks wyszukiwania.
+searchsettings.interval.never = Nigdy
+searchsettings.interval.one = Codziennie
+searchsettings.interval.many = Co {0} dni
+searchsettings.hour = o {0}:00
+searchsettings.text = Indek wyszukiwania jest obecnie tworzony. Proces mo\u017ce potrwa\u0107 kilka minut, w zale\u017cno\u015bci \
+ od wielko\u015bci twojej biblioteki medi\u00f3w.<br>Mo\u017cesz kontynuowa\u0107 wyszukiwanie z {0} podczas tworzenia indeksu.
+
+# main.jsp
+main.up = Do g\u00f3ry
+main.playall = Odtwarzaj wszystkie
+main.playrandom = Odtwarzaj losowo
+main.addall = Dodaj wszystkie
+main.tags = Edytuj tagi
+main.playcount = Odtwarzane {0} razy.
+main.lastplayed = Ostatnio odtwarzane {0}.
+main.comment = Komentarze
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__tekst__</td><td>Wyt\u0142uszczenie </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Nowa linia</td></tr>\
+ <tr><td style="padding-right:1em">~~tekst~~</td><td>Kursywa </td><td style="padding-left:3em;padding-right:1em">(pusta linia) </td><td>Nowy akapit</td></tr>\
+ <tr><td style="padding-right:1em">* tekst </td><td>Punkowanie element\u00f3w </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. tekst </td><td>Numerowanie element\u00f3w</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Link nazwany</td></tr>\
+ </table>
+main.sharealbum = Udost\u0119pnij
+main.more = Wi\u0119cej...
+main.more.selection = Wybrane utwory
+main.more.share = Udost\u0119pnij
+main.donate = <a href="{0}" style="text-decoration:underline">Wesprzyj</a> {1}!<br>(reklama zostanie usuni\u0119ta)
+main.nowplaying = Teraz odtwarzane
+main.lyrics = S\u0142owa
+main.minutesago = minut temu
+main.chat = Wiadomo\u015bci Chat
+main.message = Napisz wiadomo\u015b\u0107
+main.clearchat = Wyczy\u015b\u0107 wiadomo\u015bci
+
+# rating.jsp
+rating.rating = Ranking
+rating.clearrating = Wyczy\u015b\u0107 ranking
+
+# coverArt.jsp
+coverart.change = Zmie\u0144
+coverart.zoom = Powi\u0119ksz
+
+# allmusic.jsp
+allmusic.text = Szukanie albumu <em>{0}</em> w serwisie allmusic.com - Prosz\u0119 czeka\u0107.
+
+# changeCoverArt.jsp
+changecoverart.title = Zmie\u0144 ok\u0142adk\u0119
+changecoverart.address = Lub wpisz adres obrazka
+changecoverart.artist = Wykonawca
+changecoverart.album = Album
+changecoverart.searchdiscogs = Szukaj w Discogs
+changecoverart.wait = Prosz\u0119 czeka\u0107...
+changecoverart.success = Obraz zosta\u0142 pomy\u015blnie pobrany.
+changecoverart.error = B\u0142\u0105d podczas pobierania obrazu
+changecoverart.noimagesfound = Nie odnaleziono obrazu.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = B\u0142\u0105d podczas zmiany ok\u0142adki:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Edytuj tagi
+edittags.file = Plik
+edittags.track = Nr. utworu
+edittags.songtitle = Tyty\u0142
+edittags.artist = Wykonawca
+edittags.album = Album
+edittags.year = Rok
+edittags.genre = Gatunek
+edittags.status = Status
+edittags.suggest = Podpowied\u017a
+edittags.reset = Resetuj
+edittags.suggest.short = P
+edittags.reset.short = R
+edittags.set = Ustaw
+edittags.working = W toku
+edittags.updated = Zaktualizowano
+edittags.skipped = Pomini\u0119to
+edittags.error = B\u0142\u0105d
+
+# share.jsp
+share.title = Udost\u0119pnij
+share.warning = <h2>WA\u017bNA UWAGA!</h2><p>Graj fair - Nie udost\u0119pniaj materia\u0142\u00f3w chronionych prawem autorskim w spos\u00f3b naruszaj\u0105cy prawo.</p>
+share.facebook = Udost\u0119pnij na Facebooku
+share.twitter = Udost\u0119pnij na Twitterze
+share.link = Lub Udost\u0119pnij komu\u015b, wysy\u0142aj\u0105c link: <a href="{0}" target="_blank">{0}</a>
+share.disabled = Aby wsp\u00f3\u0142dzieli\u0107 muzyke z innymi u\u017cytkownikami, musisz najpierw zarejestrowa\u0107 sw\u00f3j adres <em>subsonic.org</em>.<br> \
+ Przejd\u017a do <a href="networkSettings.view"><b>Ustawienia &gt; Sie\u0107</b></a> (administrative rights required).
+share.manage = Zarz\u0105dzaj udost\u0119pnionymi pliki
+
+# donate.jsp
+donate.title = Darowizna
+donate.invalidlicense = Nieprawid\u0142owy klucz licencji.
+donate.amount = Podaruj {0}
+donate.textbefore = <p>Dzi\u0119kujemy za zainteresowanie darmowizn\u0105 na rzecz wsparcia projektu {0}! \
+ Jako darczy\u0144ca uzyskasz dost\u0119p do funkcji, takich jak:</p> \
+ <ul> \
+ <li>Streaming muzyki dla <a href="http://subsonic.org/pages/apps.jsp" target="blank">Android, iPhone oraz Windows Phone</a>.</li> \
+ <li>Streaming wideo.</li> \
+ <li>Tw\u00f3j osobisty adres serwera: <em>twojanazwa</em>.subsonic.org (see <a href="networkSettings.view">Ustawienia &gt; Sie\u0107</a>).</li> \
+ <li>Brak reklam.</li> \
+ <li>The <a href="http://subsonic.org/pages/apps.jsp" target="blank">SubAir</a> rich desktop application.</li> \
+ <li>Inne funkcje kt\u00f3re zostan\u0105 wydane w przysz\u0142o\u015bci.</li> \
+ </ul> \
+ <p> \
+ Otrzymana Licencja jest aktywna dla obecnego \
+ wydania {0}, oraz nast\u0119pnych.</p> \
+ <p>Sugerowana kwota darowizny to <b>&euro;20</b>, jednak mo\u017cesz podarowa\u0107 tyle, ile uwa\u017casz za stosowane:</p>
+donate.textafter = <p>Kilkaj\u0105c na przycisk zostaniesz przekierowany na PayPal, gdzie b\u0119dziesz m\u00f3g\u0142 zap\u0142aci\u0107 kart\u0105 kredytow\u0105 \
+ lub przy pomocy konta PayPal (je\u015bli takowe posiadasz). Po dokonaniu wp\u0142aty otrzymasz klucz licencyjny na adres emial.</p> \
+ <p>Je\u015bli masz pytania, zach\u0119cam do wys\u0142ania ich na adres emial \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Ta kopia {2} zosta\u0142a zarejestrowana dla {0} w {1}. Dzi\u0119kujemy za wsparcie!
+donate.register = Po otrzymaniu klucza licencyjnego nale\u017cy zarejestrowa\u0107 go poni\u017cej.
+donate.register.email = Email
+donate.register.license = Licencja
+
+# podcastReceiver.jsp
+podcastreceiver.title = Odbiornik Podcast\u00f3w
+podcastreceiver.expandall = Poka\u017c odcinki
+podcastreceiver.collapseall = Ukryj odcinki
+podcastreceiver.status.new = Nowe
+podcastreceiver.status.downloading = Pobieranie
+podcastreceiver.status.completed = Uko\u0144czono
+podcastreceiver.status.error = B\u0142\u0105d
+podcastreceiver.status.deleted = Usu\u0144
+podcastreceiver.status.skipped = Pomini\u0119te
+podcastreceiver.downloadselected= Pobierz wybrane
+podcastreceiver.deleteselected= Usu\u0144 wybrane
+podcastreceiver.confirmdelete= Na pewno usu\u0144\u0105\u0107 wybrany Podcast?
+podcastreceiver.check = Sprawdy czy s\u0105 dost\u0119pne nowe odcinki
+podcastreceiver.refresh = Od\u015bwie\u017c stron\u0119
+podcastreceiver.settings = Ustawienia podcast\u00f3w
+podcastreceiver.subscribe = Subskrybuj Podcast
+
+# lyrics.jsp
+lyrics.title = Tekst uwtoru
+lyrics.artist = Wykonawca
+lyrics.song = Utw\u00f3r
+lyrics.search = Szukaj
+lyrics.wait = Szukanie tekstu, prosz\u0119 czeka\u0107...
+lyrics.courtesy = (Tekst dzi\u0119ki <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Nie odnaleziono tekstu.
+
+# helpPopup.jsp
+helppopup.title = {0} Pomoc
+helppopup.cover.title = Rozmiar ok\u0142adki
+helppopup.cover.text = <p>Umo\u017cliwia okre\u015blenie wielko\u015bci wy\u015bwietlanej ok\u0142adki albumu, lub ca\u0142kowitego jej wy\u0142\u0105czenia.</p>
+helppopup.transcode.title = Maksymalny bitrate
+helppopup.transcode.text = <p>W przypadku ograniczonej przepustowo\u015bci pasma, mo\u017cesz ustawi\u0107 g\u00f3rny limit szybko\u015bci strumieniowania muzyki. \
+ Przyk\u0142adowo - je\u015bli orginalny plik mp3 posiada szybko\u015b\u0107 transmisji bit\u00f3w 256Kbps (kilobit\u00f3w na sekond\u0119), ustawienie maksymalnego birtate \
+ na 128 spowoduje, \u017ce {0} automatycznie przekonwertuje muzyk\u0119 z 256 na 128 Kbps.</p> \
+ <p>Ta opcja wymaga zainstalowania LAME. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ jest otwartym koderem mp3. Mo\u017cna go <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">pobra\u0107 tutaj</a>. \
+ Kodek powinien by\u0107 umieszczony w katalogu SUBSONIC_HOME/transcode, lub w katalogu obecnym w \u015bcie\u017cce zmiennej systemowej PATH</p>
+helppopup.playlistfolder.title = Folder playlist
+helppopup.playlistfolder.text = <p>Umo\u017cliwia okre\u015blenie folderu, w kt\u00f3rym b\u0119d\u0105 umieszczone pliki playlist.</p>
+helppopup.musicmask.title = Maska plik\u00f3w medi\u00f3w
+helppopup.musicmask.text = <p>Umo\u017cliwia okre\u015blenie, kt\u00f3re typy plik\u00f3w maj\u0105 by\u0107 rozpoznawane jako muzyka, podczas przegl\u0105dania folder\u00f3w medi\u00f3w.</p>
+helppopup.videomask.title = Maska plik\u00f3w wideo
+helppopup.videomask.text = <p>Umo\u017cliwia okre\u015blenie, kt\u00f3re typy plik\u00f3w maj\u0105 by\u0107 rozpoznawane jako wideo, podczas przegl\u0105dania folder\u00f3w medi\u00f3w.</p>
+helppopup.coverartmask.title = Maska ok\u0142adek
+helppopup.coverartmask.text = <p>Umo\u017cliwia okre\u015blenie, kt\u00f3re typy plik\u00f3w maj\u0105 byc rozpoznawane jako obrazy ok\u0142adek, podczas przegl\u0105dania folder\u00f3w medi\u00f3w.</p>
+helppopup.downsamplecommand.title = Komenta downsamplingu
+helppopup.downsamplecommand.text = <p>Umo\u017cliwia okre\u015blenie komendy podczas pr\u00f3bkowania do ni\u017cszej szybko\u015bci transmisji bit\u00f3w (bitrate)</p>\
+ <p>(%s = plik, kt\u00f3ry b\u0119dzie pr\u00f3bkowany, %b = Maksymalny birtate odtwarzacza)</p>
+helppopup.index.title = Indeks
+helppopup.index.text = <p>Umo\u017cliwia okre\u015blenie jak b\u0119dzie wygl\u0105da\u0142 (widoczny u g\u00f3ry ekranu) indeks. Dzi\u0119ki indeksowi \
+ pliki i foldery umieszczone bezpo\u015brednio w g\u0142\u00f3wnym katalogu medi\u00f3w, s\u0105 szybko i \u0142atwo dost\u0119pne.</p> \
+ <p>Indeks jest list\u0105 oddzielonych spacj\u0105 element\u00f3w. Standardowo poszczeg\u00f3lne elementy s\u0105 pojedy\u0144czymi znakami, \
+ jednak istnieje mo\u017cliwo\u015b\u0107 okre\u015blenia element\u00f3w wieloznakowych. Przyk\u0142adowo - wpis <em>The</em> b\u0119dzie kierowa\u0142 do plik\u00f3w \
+ oraz folder\u00f3w rozpoczynaj\u0105cych si\u0119 od "The".</p> \
+ <p>Mo\u017cna tak\u017ce okre\u015bli\u0107 grupy element\u00f3w sk\u0142adaj\u0105ce si\u0119 z wielu znak\u00f3w w nawiasach. Przyk\u0142adowo wpis \
+ <em>A-E(ABCDE)</em> zostanie wy\u015bwietlony jako <em>A-E</em> i b\u0119dzie prowadzi\u0142 do plik\u00f3w i folder\u00f3w rozpoczynaj\u0105cych si\u0119 od \
+ A, B, C, D lub E. Opcja ta mo\u017ce by\u0107 u\u017cyteczna dla grupowania rzadziej wyst\u0119puj\u0105cych element\u00f3w (takich jak X, Y oraz Z), lub \
+ do kategoryzowania znak\u00f3w diakrytycznych (takich jak A, \u00c0 oraz \u00c1)</p> \
+ <p>Pliki i folery, kt\u00f3rych nie obejmuje indeks, zostan\u0105 umieszczone w spisie pod znakiem "#".</p>
+helppopup.ignoredarticles.title = Ignorowane przedimki
+helppopup.ignoredarticles.text = <p>Umo\u017cliwia okre\u015blenie listy przedimk\u00f3w (takich jak "The"), kt\u00f3re b\u0119d\u0105 ignorowane podczas tworzenia indeksu.</p>
+helppopup.shortcuts.title = Skr\u00f3ty
+helppopup.shortcuts.text = <p>Lista oddzialonych spacjami folder\u00f3w do kt\u00f3rych maj\u0105 zosta\u0107 utworzone skr\u00f3ty. U\u017cyj cudzys\u0142owiu aby grupowa\u0107 wyrazy, przyk\u0142adowo:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = J\u0119zyk
+helppopup.language.text = <p>Umo\u017cliwia wyb\u00f3r j\u0119zyka kt\u00f3ry b\u0119dzie u\u017cywany.</p>
+helppopup.visibility.title = Widoczno\u015b\u0107
+helppopup.visibility.text = <p>Wybierz szceg\u00f3\u0142y, kt\u00f3re b\u0119d\u0105 wy\u015bwietlane dla poszczeg\u00f3lnych utwor\u00f3w, oraz liczb\u0119 wy\u015bwietlanych znak\u00f3w (czyli maksymalna \
+ liczba znak\u00f3w tytu\u0142u, nazwy albumu i wykonawcy, kt\u00f3re b\u0119d\u0105 wy\u015bwietlane).</p>
+helppopup.partymode.title = Tryb imprezy
+helppopup.partymode.text = <p>Podczas aktywnego trybu imprezy interfejs jest uproszczony, dzi\u0119ki czemu mniej do\u015bwiadczeni u\u017cytkownicy mog\u0105 \u0142atwo obs\u0142ugiwa\u0107 program. \
+ W szczeg\u00f3lno\u015bci, przypadkowe zniszczenie playlisty jest niemo\u017cliwe.</p>
+helppopup.theme.title = Motyw
+helppopup.theme.text = <p>Umo\u017cliwia wybranie motywu. Motyw okre\u015bla wygl\u0105d {0} pod wzgl\u0119dem kolor\u00f3w, czcionek, obraz\u00f3w itp.</p>
+helppopup.welcomemessage.title = Wiadomo\u015b\u0107 powitalna
+helppopup.welcomemessage.text = <p>Wiadomo\u015b\u0107, kt\u00f3ra b\u0119dzie wy\u015bwietlana na stronie g\u0142\u00f3wnej.</p>
+helppopup.loginmessage.title = Wiadomo\u015b\u0107 logowania
+helppopup.loginmessage.text = <p>Wiadomo\u015b\u0107, kt\u00f3ra b\u0119dzie wy\u015bwietlana na stronie logowania.</p>
+helppopup.coverartlimit.title = Limit ok\u0142adek
+helppopup.coverartlimit.text = <p>Maksymalna liczba ok\u0142adek, kt\u00f3re b\u0119d\u0105 wy\u015bwietlane na pojedy\u0144czej stronie.</p>
+helppopup.downloadlimit.title = Limit pobierania
+helppopup.downloadlimit.text = <p>Warto\u015b\u0107 graniczna okre\u015blaj\u0105ca jaka cz\u0119\u015b\u0107 pasma ma by\u0107 dost\u0119pna podczas pobierania plik\u00f3w z serwera.</p>
+helppopup.uploadlimit.title = Limit wysy\u0142ania
+helppopup.uploadlimit.text = <p>Warto\u015b\u0107 graniczna okre\u015blaj\u0105ca jaka cz\u0119\u015b\u0107 pasma ma by\u0107 dost\u0119pna podczas wysy\u0142ania plik\u00f3w na serwer.</p>
+helppopup.streamport.title = Port strumieniowania Nie-SSL
+helppopup.streamport.text = <p>Ta opcja jest pomocna jedynie gdy u\u017cywasz {0} zainstalowanego na serwerze z obs\u0142ug\u0105 SSL (HTTPS).</p><p>Niekt\u00f3re odtwarzacze \
+ (takie jak WinAmp) nie umo\u017cliwiaj\u0105 transmisji strumieniowej na porcie SSL. Okre\u015bl numer zwyk\u0142\u0119go portu HTTP (najcz\u0119sciej 80 \
+ lub 4040) je\u015bli nie chcesz przesy\u0142a\u0107 strumienia z wykorzystaniem protoko\u0142u SSL. Uwaga! Dane przesy\u0142\u0105ne na wybranym porcie b\u0119d\u0105 nie zaszyfrowane.</p>
+helppopup.ldap.title = Uwierzytelnianie LDAP
+helppopup.ldap.text = <p>U\u017cytkownicy mog\u0105 by\u0107 uwierzytelniani z wykorzystaniem zewn\u0119trznego serwera LDAP (w\u0142\u0105czaj\u0105c w to Windows Active Directory). \
+ Kiedy u\u017cytkownicy z aktywnym uwierzytelnianiem LDAP loguj\u0105 si\u0119 do {0}, nazwa u\u017cytwkonika i has\u0142o spradzane jest przez zewn\u0119trzny serwer LDAP, a nie przez {0}.</p>
+helppopup.ldapurl.title = URL LDAP
+helppopup.ldapurl.text = <p>Adres URL serwera LDAP. Wykorzystany musi by\u0107 jeden z protoko\u0142\u00f3w <em>ldap://</em> lub <em>ldaps://</em> \
+ (dla LDAP po SSL). <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">Tutaj</a> \
+ mo\u017cna odnale\u017a\u0107 bardziej szczeg\u00f3\u0142owy opis.</p>
+helppopup.ldapsearchfilter.title = Filtr wyszukiwania LDAP
+helppopup.ldapsearchfilter.text = <p>Wyra\u017cenie u\u017cywane do wyszukania u\u017cytkownika. Jest to filtr wyszukiwania LDAP \
+ (zdefiniowany w <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Symbol "'{0'}" b\u0119dzie zast\u0105piony nazw\u0105 u\u017cytkownika, przyk\u0142adowo: \
+ <ul>\
+ <li>(uid='{0'}) - wyra\u017cenie b\u0119dzie wyszukiwa\u0142o u\u017cytkownika odpowiadaj\u0105cego atrybutowi uid.</li> \
+ <li>(sAMAccountName='{0'}) - wyra\u017cenie zwykle wykorzystywane do uwierzytelniania w Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = Menad\u017cer DN LDAP
+helppopup.ldapmanagerdn.text = <p>Je\u015bli serwer LDAP nie obs\u0142uguje anonimowego bindowania, nale\u017cy okre\u015bli\u0107 DN \
+ (<em>Distinguished Name</em>) oraz has\u0142o u\u017cytkownika LDAP, kt\u00f3ry b\u0119dzie u\u017cywany podczas bindowania.</p>
+helppopup.ldapautoshadowing.title = Automatycznie utw\u00f3rz u\u017cytkownika LDAP w {0}
+helppopup.ldapautoshadowing.text = <p>Kiedy ta opcja jest zaznaczona u\u017cytkownik LDAP nie musi by\u0107 r\u0119cznie utworzony w {0} przed logowaniem.</p> \
+ <p>UWAGA! Oznacza to, i\u017c ka\u017cdy aktywny u\u017cytkownik LDAP mo\u017ce zalogowa\u0107 si\u0119 do {0}, \
+ co mo\u017c\u0119 by\u0107 niepo\u017c\u0105dane.</p>
+helppopup.playername.title = Nazwa odtwarzacza
+helppopup.playername.text = <p>Umo\u017cliwia okre\u015blenie \u0142atwej do zapami\u0119tania nazwy odtwarzacza, przyk\u0142adowo "Praca" lub "Salon".</p>
+helppopup.autocontrol.title = Automatycznie kontroluj odtwarzanie
+helppopup.autocontrol.text = <p>Po zaznaczeniu tej opcji, {0} automatycznie rozpocznie odtwarzanie po klikni\u0119ciu "Odtwarzaj" \
+ na playliscie.</p>
+helppopup.dynamicip.title = Dynamiczny adres IP
+helppopup.dynamicip.text = <p>Opcja powinna by\u0107 odznaczona, je\u015bli podczas odtwarzania wykorzystywany jest statyczny adres IP.</p>
+
+# wap/index.jsp
+wap.index.missing = Nie odnaleziono muzyki
+wap.index.playlist = Playlista
+wap.index.search = Szukaj
+wap.index.settings = Ustawienia
+
+# wap/browse.jsp
+wap.browse.playone = Odtwarzaj utw\u00f3r
+wap.browse.playall = Odtwarzaj wszystkie
+wap.browse.addone = Dodaj utw\u00f3r
+wap.browse.addall = Dodaj wszystkie
+wap.browse.downloadone = Pobierz utw\u00f3r
+wap.browse.downloadall = Pobierz wszystkie
+
+# wap/playlist.jsp
+wap.playlist.title = Playlista
+wap.playlist.noplayer = Brak po\u0142\u0105czonego odtwarzacza
+wap.playlist.clear = Wyczy\u015b\u0107
+wap.playlist.load = Wczytaj
+wap.playlist.random = Losowo
+wap.playlist.play = Odtwarzaj na telefonie
+
+# wap/search.jsp
+wap.search.title = Szukaj
+
+# wap/searchResult.jsp
+wap.searchresult.index = Indeks wyszukiwania jest aktualnie tworzony. Prosz\u0119 spr\u00f3bowa\u0107 po\u017aniej.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Wybierz odtwarzacz
+wap.settings.allplayers = Wszystkie
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pt.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pt.properties
new file mode 100644
index 00000000..675d9f77
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_pt.properties
@@ -0,0 +1,686 @@
+#
+# Portuguese localization.
+# Author: Miguel Fonseca
+#
+
+common.home = Início
+common.back = Voltar
+common.help = Ajuda
+common.play = Reproduzir
+common.add = Adicionar
+common.download = Descarregar
+common.close = Fechar
+common.refresh = Actualizar
+common.next = Seguinte
+common.previous = Anterior
+common.more = Mais
+common.ok = OK
+common.cancel = Cancelar
+common.save = Guardar
+common.create = Criar
+common.delete = Apagar
+common.unknown = (Desconhecido)
+common.default = (Predefinido)
+
+# login.jsp
+login.username = Utilizador
+login.password = Senha
+login.login = Iniciar sessão
+login.remember = Manter sessão iniciada
+login.logout = Está agora desligado.
+login.error = Utilizador ou senha errada.
+login.insecure = {0} não está seguro. Por favor inicie sessão com o utilizador e<br>senha "admin", ou carregue <a href="login.view?user=admin&amp;password=admin">aqui</a>. Depois mude a senha imediatamente.
+
+# accessDenied.jsp
+accessDenied.title = Acesso negado
+accessDenied.text = Desculpe, não está autorizado a executar essa operação.
+
+# top.jsp
+top.home = Início
+top.now_playing = A tocar
+top.settings = Configurações
+top.status = Estado
+top.podcast = Podcast
+top.more = Mais
+top.help = Acerca
+top.search = Pesquisar
+top.upgrade = <b>Aviso!</b> Uma nova versão está disponível.<br>Descarregar {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">aqui</a>.
+top.missing = Não se encontra nenhuma pasta. Por favor mude as configurações.
+top.logout = Terminar sessão {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;Artistas<br>\
+ {1}&nbsp;Albuns<br>\
+ {2}&nbsp;Músicas<br>\
+ {3} (&#126; {4} horas)
+left.shortcut = Atalhos
+left.radio = Internet TV/radio
+left.allfolders = Todas as pastas
+
+# playlist.jsp
+playlist.stop = Parar
+playlist.start = Reproduzir
+playlist.confirmclear = Quer mesmo limpar a lista?
+playlist.clear = Limpar
+playlist.shuffle = Modo aleatório
+playlist.repeat_on = Repetir está ligado
+playlist.repeat_off = Repetir está desligado
+playlist.undo = Desfazer
+playlist.settings = Configurações
+playlist.more = Mais acções...
+playlist.more.playlist = Lista
+playlist.more.sortbytrack = Ordenar por pista
+playlist.more.sortbyartist = Ordenar por artista
+playlist.more.sortbyalbum = Ordenar por album
+playlist.more.selection = Músicas seleccionadas
+playlist.more.selectall = Selecionar todas
+playlist.more.selectnone = Selecionar nenhuma
+playlist.getflash = Obtenha o Flash player
+playlist.load = Abrir
+playlist.save = Guardar
+playlist.append = Adicionar à lista
+playlist.remove = Apagar
+playlist.up = Voltar
+playlist.down = Baixo
+playlist.empty = Lista vazia
+
+# status.jsp
+status.title = Estado
+status.type = Tipo
+status.stream = Stream
+status.download = Descarregar
+status.upload = Carregar
+status.player = Leitor
+status.user = Utilizador
+status.current = Música a tocar
+status.transmitted = Transmitido
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = Pesquisar
+search.query = Artista, album ou música
+search.search = Pesquisar
+search.index = O índice de pesquisa está sendo criado. Por favor tente daqui a pouco.
+search.hits.none = Não foram encontradas ocorrências.
+search.hits.more = Mais
+search.hits.artists = Artistas
+search.hits.albums = Albuns
+search.hits.songs = Músicas
+
+# gettingStarted.jsp
+gettingStarted.title = Primeiros passos
+gettingStarted.text = <p>Bem-vindo ao Subsonic! Vamos prepará-lo num instante, basta seguir os seguintes passos básicos.<br> \
+ Clicar no botão "Início" na barra acima para voltar a este ecran.</p> \
+ <p>Para mais informações, por favor consulte o<a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Guia</b></a> Primeiros passos.</p>
+gettingStarted.step1.title = Mudar senha do administrador.
+gettingStarted.step1.text = Proteja o seu servidor, alterando a senha padrão para a conta de administrador. \
+ Pode criar novas contas de utilizador com privilégios diferentes.
+gettingStarted.step2.title = Criar pastas de música.
+gettingStarted.step2.text = Mostrar ao Subsonic onde tem a sua música.
+gettingStarted.step3.title = Configurações de rede.
+gettingStarted.step3.text = Algumas definições úteis se você quiser desfrutar da sua música remotamente através da Internet, \
+ ou compartilhá-la com a família e amigos. Obtenha o seu endereço com nome de<b><em>utilizador</em>.subsonic.org</b>.
+gettingStarted.hide = Não mostrar mais
+gettingStarted.hidealert = Para mostar este ecran outra vez, vá a Configurações > Geral.
+
+# home.jsp
+home.random.title = Aleatório
+home.newest.title = Novos Albuns
+home.highest.title = Melhor Classificados
+home.frequent.title = Mais Ouvidos
+home.recent.title = Mais Recentes
+home.users.title = Utilizadores
+home.random.text = Albuns aleatórios
+home.newest.text = Albuns adicionados recentemente
+home.highest.text = Albuns melhor classificados
+home.frequent.text = Albuns mais ouvidos
+home.recent.text = Albuns reproduzidos recentemente
+home.users.text = Estatísticas de utilizador
+home.scan = A pasta música está a ser analisada. Não estão disponíveis todas as caracteristicas.
+home.listsize = {0} Albuns por página
+home.albums = Albuns {0} - {1}
+home.playcount = Reproduzidos {0} vezes
+home.lastplayed = Reproduzidos {0}
+home.created = Modificados {0}
+home.chart.total = Total (MB)
+home.chart.stream = Enviado (MB)
+home.chart.download = Descarregados (MB)
+home.chart.upload = Carregados (MB)
+
+# more.jsp
+more.title = Mais
+more.random.title = Lista aleatória
+more.random.text = Criar lista aleatória com
+more.random.songs = {0} músicas
+more.random.auto = Reproduzir mais músicas aleatórias quando chegar ao fim da lista.
+more.random.ok = OK
+more.random.genre = do género
+more.random.anygenre = Qualquer
+more.random.year = e ano
+more.random.anyyear = Qualquer
+more.random.folder = na pasta
+more.random.anyfolder = Qualquer
+more.apps.title = Aplicações Subsonic
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Aplicações Subsonic</a> estão disponíveis para <b>iPhone</b>, \
+ <b>Android</b> e <b>AIR</b>.</p>
+more.mobile.title = Telemóvel
+more.mobile.text = <p>Pode controlar o {0} com qualquer telemóvel que tenha WAP activado ou num PDA.<br> \
+ Só tem de visitar este endereço do seu telemóvel: <b>http://oseuendereço/wap</b></p> \
+ <p>Para que isto funcione o servidor tem de estar acessível através da internet.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Listas de reprodução guardadas como Podcasts.<br>\
+ Use o seguinte endereço no seu Podcast: <b>http://oseuendereço/podcast</b>, \
+ ou <b><a href="podcast.view?suffix=.rss">clique aqui</a>.</b></p>
+more.upload.title = Enviar ficheiro
+more.upload.source = Seleccionar ficheiro
+more.upload.target = Enviar para
+more.upload.browse = Escolher
+more.upload.ok = Enviar
+more.upload.unzip = Descomprimir automáticamente o ficheiro zip.
+more.upload.progress = % completo. Por favor aguarde...
+
+# upload.jsp
+upload.title = A enviar ficheiro
+upload.success = Carregamento com sucesso <b>{0}</b>
+upload.empty = Nenhum ficheiro para enviar.
+upload.failed = O carregamento falhou com o seguinte erro:<br><b>"{0}"</b>
+
+upload.unzipped = Descomprimido {0}
+
+# help.jsp
+help.title = Acerca {0}
+help.upgrade = <b>Aviso!</b> Está disponível uma nova versão. Descarregar {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">aqui</a>.
+help.version.title = Versão
+help.builddate.title = Data de compilação
+help.server.title = Servidor
+help.license.title = Licença
+help.license.text = O {0} é um software livre distribuido sobre a licença <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> de código aberto. \
+ O {0} usa uma licença<a href="http://subsonic.org/pages/libraries.jsp" target="_blank"> de bibliotecas de terceiros</a>. Tome nota que o {0} <em>não é</em> \
+ uma ferramenta para a distribuição ilegal de material protegido por direitos de autor. Preste sempre atenção e siga as leis específicas para o seu país.
+help.homepage.title = Página do projecto
+help.forum.title = Forum
+help.shop.title = Merchandise
+help.contact.title = Contacto
+help.contact.text = O {0} é desenvolvido e mantido por Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ Se tiver quaisquer perguntas, comentários ou sugestões para melhorias, por favor visite o Forum do\
+ <a href="http://forum.subsonic.org" target="_blank"> Subsonic </a>.
+help.donate = O {0} é gratuito, mas você pode contribuir com o projeto, dando um <b><a href="donate.view?">donativo</a></b>.
+help.log = Log
+help.logfile = O log completo está guardado em {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Configurações
+settingsheader.general = Geral
+settingsheader.advanced = Avançado
+settingsheader.personal = Pessoal
+settingsheader.musicFolder = Pastas de música
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Leitores
+settingsheader.network = Rede
+settingsheader.transcoding = Transcodificação
+settingsheader.user = Utilizadores
+settingsheader.search = Pesquisa
+settingsheader.coverArt = Capas
+settingsheader.password = Senha
+
+# generalSettings.jsp
+generalsettings.playlistfolder = Pasta de listas
+generalsettings.musicmask = Ficheiros de música
+generalsettings.videomask = Ficheiros de video
+generalsettings.coverartmask = Ficheiros de capas
+generalsettings.index = Índice
+generalsettings.ignoredarticles = Ignorar artigos
+generalsettings.shortcuts = Atalhos
+generalsettings.showgettingstarted = Mostrar "Primeiros passos" no início
+generalsettings.welcometitle = Título de boas vindas
+generalsettings.welcomesubtitle = Subtítulo de boas vindas
+generalsettings.welcomemessage = Mensagem de boas vindas
+generalsettings.loginmessage = Messagem de iníco de sessão
+generalsettings.language = Idioma Padrão
+generalsettings.theme = Tema Padrão
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Comandos de sub-amostragem
+advancedsettings.coverartlimit = Limite de capas<br><div class="detail">(0 = Ilimitado)</div>
+advancedsettings.downloadlimit = Limite de Download (receber)(Kbps)<br><div class="detail">(0 = Ilimitado)</div>
+advancedsettings.uploadlimit = Limite de Upload (enviar) (Kbps)<br><div class="detail">(0 = Ilimitado)</div>
+advancedsettings.streamport = Porta não-SSL<br><div class="detail">(0 = Desactivado)</div>
+advancedsettings.ldapenabled = Activar autenticação LDAP
+advancedsettings.ldapurl = Endereço LDAP
+advancedsettings.ldapsearchfilter = Filtro de pesquisa LDAP
+advancedsettings.ldapmanagerdn = Gestor de LDAP DN<br><div class="detail">(Opcional)</div>
+advancedsettings.ldapmanagerpassword = Senha
+advancedsettings.ldapautoshadowing = Criar automáticamente utilizadores em {0}
+
+# personalSettings.jsp
+personalsettings.title = Configurações pessoais para o utilizador {0}
+personalsettings.language = Idioma
+personalsettings.theme = Tema
+personalsettings.display = Mostrar
+personalsettings.browse = Navegar
+personalsettings.playlist = Lista
+personalsettings.tracknumber = Pista #
+personalsettings.artist = Artista
+personalsettings.album = Album
+personalsettings.genre = Género
+personalsettings.year = Ano
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Duração
+personalsettings.format = Formato
+personalsettings.filesize = Tamanho de ficheiro
+personalsettings.captioncutoff = Caracteres visualizáveis
+personalsettings.partymode = Modo festa
+personalsettings.shownowplaying = Mostra o que os outros estão a ouvir
+personalsettings.nowplayingallowed = Deixar os outros ver o que eu estou a ouvir
+personalsettings.showchat = Mostrar mensagens de chat
+personalsettings.finalversionnotification = Notifique-me sobre novas versões
+personalsettings.betaversionnotification = Notifique-me sobre novas versões beta
+personalsettings.lastfmenabled = Registar o que estou a ouvir no <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Utilizador Last.fm
+personalsettings.lastfmpassword = Senha Last.fm
+personalsettings.avatar.title = Imagem pessoal
+personalsettings.avatar.none = Sem imagem
+personalsettings.avatar.custom = Imagem personalizada
+personalsettings.avatar.changecustom = Mudar imagem personalizada
+personalsettings.avatar.upload = Enviar
+personalsettings.avatar.courtesy = Icones cortezia de <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Mudar imagem personalizada
+avataruploadresult.success = Carregou com sucesso a sua imagem personalizada "{0}".
+avataruploadresult.failure = Falhou o envio da sua imagem personalizada. Veja o <a href="help.view?">log</a> para os detalhes.
+
+# passwordSettings.jsp
+passwordsettings.title = Mudar senha para {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Pasta
+musicfoldersettings.name = Nome
+musicfoldersettings.enabled = Activado
+musicfoldersettings.add = Adicionar pasta de música
+musicfoldersettings.nopath = Por favor especifique a pasta.
+
+# networkSettings.jsp
+networksettings.text = Use as configurações abaixo para controlar a forma de aceder ao servidor do Subsonic através da Internet .<br> \
+ Se tiver problemas, consulte o guia dos <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>primeiros passos</b></a>. (em inglês)
+networksettings.portforwardingenabled = Configurar automáticamente o seu router para permitir ligações de entrada para o Subsonic (usando UPnP ou o encaminhamento de porta NAT-PMP).
+networksettings.portforwardinghelp = Se o seu router não puder ser configurado automáticamente, pode configurá-lo manualmente. \
+ Siga as instruções em <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Deve encaminhar a porta {0} para o computador que está a executar o servidor Subsonic.
+networksettings.urlredirectionenabled = Aceda ao seu servidor na Internet usando um endereço fácil de lembrar.
+networksettings.status = Estado:
+networksettings.trialexpired = O período de avaliação expirou em {0}. Por favor <b><a href="donate.view?">dê um donativo</a></b> para habilitar este recurso de forma permanente.
+networksettings.trialnotexpired = Este recurso está disponível até {0}. Depois disso deve <b><a href="donate.view?">doar</a></b> para o usar permanentemente.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Nome
+transcodingsettings.sourceformat = Converter de
+transcodingsettings.targetformat = Converter para
+transcodingsettings.step1 = Passo 1
+transcodingsettings.step2 = Passo 2
+transcodingsettings.step3 = Passo 3
+transcodingsettings.defaultactive = Padrão
+transcodingsettings.enabled = Activado
+transcodingsettings.add = Adicionar transcodificação
+transcodingsettings.noname = Por favor especifique o nome.
+transcodingsettings.nosourceformat = Por favor, especifique o formato para converter de.
+transcodingsettings.notargetformat = Por favor, especifique o formato para converter para.
+transcodingsettings.nostep1 = Por favor, especificar pelo menos uma etapa de transcodificação.
+transcodingsettings.info = <p class="detail">(%s = O ficheiro a ser transcodificado, %b = Max bitrate do Leitor, %t = Título, %a = Artista, %l = Album)</p> \
+ <p>Transcodificação é o processo de conversão de um formato para outro. A transcodificação no {1} \
+ é o motor que permite o envio em tempo real dos ficheiros. A transcodificação é feita na hora e não \
+ requer o uso do disco.<p/> \
+ <p>A transcodificação é feita por programas de terceiros que devem ser instalados em {0}. \
+ Um pacote de transcodificação para o Windows \
+ está disponível <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>aqui</b></a>. Pode adicionar o seu próprio transcodificador, desde que \
+ preencha os seguintes requisitos: \
+ <ul> \
+ <li>Deve ter uma interface de linha de comando.</li> \
+ <li>Deve ser capaz de enviar a saída para stdout.</li> \
+ <li>Se usado nos passos 2 ou 3, deve ser capaz de ler a entrada de stdin.</li> \
+ </ul> \
+ </p> \
+ <p> Note-se que as transcodificações são activadas numa base por-Leitor na página das configurações do Leitor. Se o padrão estiver activado, a transcodificação \
+ é activada automáticamente para os novos Leitores.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Endereço do Stream
+internetradiosettings.homepageurl = Página principal
+internetradiosettings.name = Nome
+internetradiosettings.enabled = Activado
+internetradiosettings.add = Adicionar Internet TV/radio
+internetradiosettings.nourl = Por favor especifique um endereço.
+internetradiosettings.noname = Por favor especifique um nome.
+
+# podcastSettings.jsp
+podcastsettings.update = Verificar se há novos episódios
+podcastsettings.keep = Manter
+podcastsettings.keep.all = Todos os episódios
+podcastsettings.keep.one = Os episódios mais recentes
+podcastsettings.keep.many = Os últimos {0} episódios
+podcastsettings.download = Quando novos episódios estiverem disponíveis
+podcastsettings.download.all = Descarregar todos
+podcastsettings.download.one = Descarregar o mais recente
+podcastsettings.download.many = Descarregar os últimos {0} episódios
+podcastsettings.download.none = Não fazer nada
+podcastsettings.interval.manually = Manualmente
+podcastsettings.interval.hourly = A cada hora
+podcastsettings.interval.daily = A cada dia
+podcastsettings.interval.weekly = A cada semana
+podcastsettings.folder = Gravar os Podcasts em
+
+# playerSettings.jsp
+playersettings.noplayers = nenhum Leitor encontrado.
+playersettings.type = Tipo
+playersettings.lastseen = Visto pela última vez
+playersettings.title = Selecionar Leitor
+
+playersettings.technology.web.title = Leitor Web
+playersettings.technology.external.title = Leitor Externo
+playersettings.technology.external_with_playlist.title = Leitor Externo com Listas
+playersettings.technology.jukebox.title = Caixa de Música
+playersettings.technology.web.text = Reproduzir música directamente no navegador web usando o Flash player integrado.
+playersettings.technology.external.text = Reproduzir música no seu Leitor favorito, como o Winamp ou o Windows Media Player.
+playersettings.technology.external_with_playlist.text = O mesmo que acima, mas a lista é gerida pelo Leitor, em vez \
+ do servidor Subsonic. Neste modo, o avanço nas músicas é possível.
+playersettings.technology.jukebox.text = Reproduzir músicas directamente no dispositivo de áudio do servidor Subsonic. (Só utilizadores autorizados).
+playersettings.name = Nome do Leitor
+playersettings.coverartsize = Tamanho de capa
+playersettings.maxbitrate = Max bitrate
+playersettings.coverart.off = Desligado
+playersettings.coverart.small = Pequena
+playersettings.coverart.medium = Media
+playersettings.coverart.large = Grande
+playersettings.nolame = <em>Notice:</em> O LAME parece não estar instalado.<br>Clique no botão Ajuda para obter mais informações.
+playersettings.autocontrol = Controlar Leitor Automáticamente
+playersettings.dynamicip = O Leitor tem endereço IP dinâmico
+playersettings.transcodings = Transcodificações activas
+playersettings.ok = Guardar
+playersettings.forget = Apagar Leitor
+playersettings.clone = Duplicar Leitor
+
+# userSettings.jsp
+usersettings.title = Seleccione utilizador
+usersettings.newuser = Novo utilizador
+usersettings.admin = O utilizador é administrador
+usersettings.settings = O utilizador pode alterar as configurações e a senha
+usersettings.stream = O utilizador pode ouvir música
+usersettings.jukebox = O utilizador pode usar o modo caixa de música
+usersettings.download = O utilizador pode descarregar músicas
+usersettings.upload = O utilizador pode enviar músicas
+usersettings.playlist= O utilizador pode criar e apagar listas
+usersettings.coverart = O utilizador pode mudar capas e etiquetas
+usersettings.comment= O utilizador pode criar/editar comentários e classificações
+usersettings.podcast= O utilizador pode administrar Podcasts
+usersettings.username = Utilizador
+usersettings.changepassword = Mudar senha
+usersettings.password = Senha
+usersettings.newpassword = Nova senha
+usersettings.confirmpassword = Confirmar senha
+usersettings.delete = Apagar este utilizador
+usersettings.ldap = Autenticar utilizador com LDAP
+usersettings.nousername = Falta o utilizador.
+usersettings.useralreadyexists = Utilizador já existe.
+usersettings.nopassword = A senha é requerida.
+usersettings.wrongpassword = A senhas não correspondem.
+usersettings.ldapdisabled = Autenticação por LDAP não está activa. Veja as configurações avançadas.
+usersettings.passwordnotsupportedforldap = Não é possível definir ou alterar a senha para os utilizadores autenticados-LDAP.
+usersettings.ok = Senha alterada com sucesso para o utilizador {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = Nunca
+musicfoldersettings.interval.one = A cada dia
+musicfoldersettings.interval.many = A cada {0} dias
+musicfoldersettings.hour = às {0}:00
+
+# main.jsp
+main.up = Voltar
+main.playall = Reproduzir todas
+main.playrandom = Reproduzir aleatóriamente
+main.addall = Adicionar todas
+main.tags = Editar etiquetas
+main.playcount = Ouvida {0} vezes.
+main.lastplayed = Ouvida a última vez em {0}.
+main.comment = Comentar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__Texto__</td><td>negrito </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Quebra de linha</td></tr>\
+ <tr><td style="padding-right:1em">~~texto~~</td><td>Texto itálico </td><td style="padding-left:3em;padding-right:1em">(linha vazia) </td><td>Novo parágrafo</td></tr>\
+ <tr><td style="padding-right:1em">* texto </td><td>Listar item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Endereço</td></tr>\
+ <tr><td style="padding-right:1em">1. texto </td><td>Enumerar item da lista</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Nome de endereço</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Donativo</a> to {1}!<br>(e retire este anúncio)
+main.nowplaying = A tocar agora
+main.lyrics = Letras
+main.minutesago = à minutos
+main.chat = Mensagens de chat
+main.message = Escrever uma mensagem
+main.clearchat = Limpar mensagens
+
+# rating.jsp
+rating.rating = Classificar
+rating.clearrating = Apagar classificação
+
+# coverArt.jsp
+coverart.change = Mudar
+coverart.zoom = Aumentar
+
+# allmusic.jsp
+allmusic.text = Pesquisar por album <em>{0}</em> em allmusic.com - Por favor espere.
+
+# changeCoverArt.jsp
+changecoverart.title = Mudar capa
+changecoverart.address = Ou digite o endereço da imagem
+changecoverart.artist = Artista
+changecoverart.album = Album
+changecoverart.search = Pesquisa de imagem no Google
+changecoverart.wait = Por favor espere...
+changecoverart.success = A imagem foi descarregada com sucesso.
+changecoverart.error = Falhou a descarga da imagem.
+changecoverart.noimagesfound = Imagem não encontrada.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Falha de mudança de capa:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Editar etiquetas
+edittags.file = Ficheiro
+edittags.track = Pista
+edittags.songtitle = Título
+edittags.artist = Artista
+edittags.album = Album
+edittags.year = Ano
+edittags.genre = Género
+edittags.status = Estado
+edittags.suggest = Sugerir
+edittags.reset = Restabelecer
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Aplicar
+edittags.working = A executar
+edittags.updated = Actualizada
+edittags.skipped = Ignorada
+edittags.error = Erro
+
+# donate.jsp
+donate.title = Doar
+donate.invalidlicense = Chave de licença inválida.
+donate.amount = Doar {0}
+
+donate.textbefore = <p>Obrigado por considerar uma doação para apoiar o projecto! \
+ Os doadores têm acesso a funcionalidades tais como:</p> \
+ <ul> \
+ <li>Uso ilimitado de aplicações do Subsonic <a href="http://subsonic.org/pages/apps.jsp" target="blank">para</a> o iPhone, Android e AIR.</li> \
+ <li>Endereço de servidor personalizado: <em>oseunome</em>.subsonic.org (veja em <a href="networkSettings.view">Configurações &gt; Rede</a>).</li> \
+ <li>Sem anúncios no interface web.</li> \
+ <li>Novos recursos a ser lançados .</li> \
+ </ul> \
+ <p> \
+ Como doador receberá uma chave de licença que é válida para esta \
+ e para todas as futuras versões do {0}.</p> \
+ <p>O montante de doação sugerida é <b>&euro;20</b>, mas pode seleccionar qualquer quantidade que queira:</p>
+donate.textafter = <p>Clique no botão para ir para o PayPal, onde pode pagar com cartão de crédito ou usando \
+ a sua conta PayPal (se tiver uma). Receberá a chave de licença por e-mail em poucos minutos.</p> \
+ <p>Se tiver alguma dúvida, envie um e-mail para \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Esta cópia do {2} foi licenciada para {0} em {1}. Obrigado pelo seu apoio!
+donate.register = Após receber a sua chave de licença, por favor, registre-a abaixo.
+donate.register.email = Email
+donate.register.license = Licença
+
+# podcastReceiver.jsp
+podcastreceiver.title = Receptor Podcast
+podcastreceiver.expandall = Mostrar episódios
+podcastreceiver.collapseall = Esconder episódios
+podcastreceiver.status.new = Novo
+podcastreceiver.status.downloading = A descarregar
+podcastreceiver.status.completed = Completo
+podcastreceiver.status.error = Erro
+podcastreceiver.status.deleted = Apagado
+podcastreceiver.status.skipped = Ignorado
+podcastreceiver.downloadselected= Descarregar seleccionado
+podcastreceiver.deleteselected= Apagar seleccionado
+podcastreceiver.confirmdelete= Quer mesmo apagar os Podcasts seleccionados?
+podcastreceiver.check = Verificar se há novos episódios
+podcastreceiver.refresh = Actualizar página
+podcastreceiver.settings = Configurações do Podcast
+podcastreceiver.subscribe = Subscrever Podcast
+
+# lyrics.jsp
+lyrics.title = Letras
+lyrics.artist = Artista
+lyrics.song = Música
+lyrics.search = Pesquisar
+lyrics.wait = A pesquisar a letra, por favor aguarde...
+lyrics.courtesy = (Letras por <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Não foi encontrada nenhuma letra.
+
+# helpPopup.jsp
+helppopup.title = {0} Ajuda
+helppopup.cover.title = Tamanho de capa
+helppopup.cover.text = <p>Permite especificar o tamanho da capa, com a opção de a desligar completamente.</p>
+helppopup.transcode.title = Max bitrate
+helppopup.transcode.text = <p>Se tiver restrições de banda, pode definir um limite superior para o bitrate da música enviada. \
+ Por exemplo, se o seu mp3 for codificado a 256 Kbps (kilobits por segundo), e configurar o max bitrate \
+ para 128 vai o {0} fazer automáticamente a reamostragem da música de 256kbps para 128 Kbps.</p> \
+ <p>Esta opção requer que o LAME esteja instalado. O LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ é um codificador de mp3 de código aberto. Pode <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">descarregá-lo aqui</a>. \
+ Por favor, certifique-se de instalá-lo no directório SUBSONIC_HOME/transcode .</p>
+helppopup.playlistfolder.title = Pasta de listas
+helppopup.playlistfolder.text = <p>Deixa-o especificar onde estão localizadas as suas listas.</p>
+helppopup.musicmask.title = Ficheiros de música
+helppopup.musicmask.text = <p>Deixa-o especificar que tipo de ficheiros são reconhecidos como música.</p>
+helppopup.videomask.title = Ficheiros de video
+helppopup.videomask.text = <p>Deixa-o especificar que tipo de ficheiros são reconhecidos como videos.</p>
+helppopup.coverartmask.title = Ficheiros de capas
+helppopup.coverartmask.text = <p>Deixa-o especificar que tipo de ficheiros são reconhecidos como capas ao navegar pela pasta de músicas.</p>
+helppopup.downsamplecommand.title = Comandos de sub-amostragem
+helppopup.downsamplecommand.text = <p>Deixa-o especificar que comandos executar quando fizer a sub-amostragem para bitrates inferiores.</p>\
+ <p>(%s = O ficheiro a ser sub-amostrado, %b = Max bitrate do Leitor, %t = Título, %a = Artista, %l = Album)</p>
+helppopup.index.title = ìndice
+helppopup.index.text = <p>Deixa-o especificar como o índice (localizado no lado esquerdo da janela) deve parecer. Ficheiros e directorias \
+ directamente na pasta de raiz de música podem ser acedidos usando este índice.</p> \
+ <p>A especificação é uma lista separada por espaços das entradas do índice. Normalmente, cada entrada é apenas um único caracter, \
+ mas pode especificar vários caracteres. Por exemplo, a entrada <em>The(o)</em> vai ligar todos os ficheiros e \
+ pastas começadas por "The".</p> \
+ <p>Pode também criar uma entrada usando um grupo do índice em parênteses. Por exemplo, a entrada \
+ <em>A-E(ABCDE)</em> vai aparecer <em>A-E</em> e ligar a todos os ficheiros e pastas com \
+ A, B, C, D ou E. Isto pode ser útil para o agrupamento de caracteres utilizados com menos frequência (como o X, Y e o Z), ou \
+ para o agrupamento de caracteres acentuados (como o A, \u00c0 e \u00c1)</p> \
+ <p>Ficheiros e pastas que não sejam cobertos pelo índice, serão colocados sobre a entrada do índice "#".</p>
+helppopup.ignoredarticles.title = Artigos para ignorar
+helppopup.ignoredarticles.text = <p>Deixa-o especificar uma lista de artigos (como o "The") que serão ignorados durante a criação do índice.</p>
+helppopup.shortcuts.title = Atalhos
+helppopup.shortcuts.text = <p>Uma lista separada por espaços de pastas de nível superior para criar atalhos para. Use aspas às palavras do grupo, por exemplo:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = Idioma
+helppopup.language.text = <p>Deixa-o especificar o idioma a usar.</p>
+helppopup.visibility.title = Visibilidade
+helppopup.visibility.text = <p>Seleccione quais informações devem ser apresentadas para cada música, bem como a sua legenda. Isto é o máximo \
+ número de caracteres para mostrar o título da música, album e artista.</p>
+helppopup.partymode.title = Modo Festa
+helppopup.partymode.text = <p>Qaundo o modo festa está seleccionado, a interface do utilizador é simplificada e mais fácil de operar por utilizadores com pouca experiência. \
+ Em particular, em acidentes a mexer nas listas de música.</p>
+helppopup.theme.title = Tema
+helppopup.theme.text = <p>Deixa-o especificar o tema a usar. Um tema define a aparência do {0} em termos de cores, fontes, imagens etc.</p>
+helppopup.welcomemessage.title = Mensagem de boas vindas
+helppopup.welcomemessage.text = <p>A mensagem que é exibida na página inicial.</p>
+helppopup.loginmessage.title = Mensagem de início de sessão
+helppopup.loginmessage.text = <p>A mensagem que é exibida na página de início de sessão.</p>
+helppopup.coverartlimit.title = Limite de capas
+helppopup.coverartlimit.text = <p>O número máximo de imagens de capas para exibir em uma única página.</p>
+helppopup.downloadlimit.title = Limite de download
+helppopup.downloadlimit.text = <p>Um limite superior para a largura de banda será usada para descarregar ficheiros.</p>
+helppopup.uploadlimit.title = Limite de upload
+helppopup.uploadlimit.text = <p>Um limite superior para a largura de banda será usada para enviar ficheiros.</p>
+helppopup.streamport.title = Porta não-SSL
+helppopup.streamport.text = <p>Esta opção só é relevante se usar o {0} num servidor com SSL (HTTPS).</p><p>Alguns Leitores \
+ (como o Winamp) não suportam o envio sobre SSL. Especifique o número da porta http (normalmente a 80 \
+ ou a 4040) se não quiser que os envios sejam transmitidos via SSL. Note-se que os envios não não encriptados.</p>
+helppopup.ldap.title = Autenticação por LDAP
+helppopup.ldap.text = <p>Os utilizadores podem ser autenticados por um servidor LDAP externo (incluindo o Windows Active Directory). \
+ Qaundo os utilizadores autenticados por LDAP iniciam sessão no {0}, o utilizador e senha são controlados pelo servidor externo, e não pelo próprio {0}.</p>
+helppopup.ldapurl.title = Endereço LDAP
+helppopup.ldapurl.text = <p>O endereço do servidor LDAP. O protocolo deve ser <em>ldap://</em> ou <em>ldaps://</em> \
+ (para LDAP via SSL). Veja <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">aqui</a> \
+ para uma descrição mais detalhada.</p>
+helppopup.ldapsearchfilter.title = Filtro de pesquisa LDAP
+helppopup.ldapsearchfilter.text = <p>A expressão filtrada usada pelo utilizador na pesquisa. Isto é um filtro de pesquisa LDAP \
+ (como definida em <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ O padrão "'{0'}" é substituido pelo nome do utilizador, por exemplo: \
+ <ul>\
+ <li>(uid='{0'}) - isto procura por um nome de utilizador no atributo uid.</li> \
+ <li>(sAMAccountName='{0'}) - normalmente usado para autenticação no Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = Gestor de LDAP ND
+helppopup.ldapmanagerdn.text = <p>Se o servidor LDAP não suportar vinculação anónima deve especificar o ND \
+ (<em>Nome Distinto</em>) e a senha do utilizador LDAP para usar ao vincular.</p>
+helppopup.ldapautoshadowing.title = Cria automáticamente utilizadores LDAP em {0}
+helppopup.ldapautoshadowing.text = <p>Com esta opção activada,os utilizadores LDAP não têm de ser criados manualmente no {0} antes do início de sessão.</p> \
+ <p>AVISO! Isto significa que qualquer utilizador com um nome de utilizador e senha válidos LDAP pode fazer o início de sessão {0}, \
+ que pode não ser o que quer.</p>
+helppopup.playername.title = Nome do Leitor
+helppopup.playername.text = <p>Deixa-o especificar um nome fácil de lembrar para o seu Leitor, como "Emprego" ou "Sala de estar".</p>
+helppopup.autocontrol.title = Controla automáticamente o Leitor
+helppopup.autocontrol.text = <p>Com esta opção seleccionada, o {0} iniciará automaticamente o Leitor quando você clicar em "Reproduzir" \
+ na lista. De outra maneira, tem de começar o Leitor manualmente.</p>
+helppopup.dynamicip.title = Endereço IP dinâmico
+helppopup.dynamicip.text = <p>Desligue esta opção se o Leitor usar um endereço IP estático.</p>
+
+# wap/index.jsp
+wap.index.missing = Nenhuma música encontrada
+wap.index.playlist = Lista
+wap.index.search = Pesquisa
+wap.index.settings = Configurações
+
+# wap/browse.jsp
+wap.browse.playone = Reproduzir música
+wap.browse.playall = Reproduzir todas
+wap.browse.addone = Adicionar música
+wap.browse.addall = Adicionar todas
+wap.browse.downloadone = Descarregar música
+wap.browse.downloadall = Descarregar todas
+
+# wap/playlist.jsp
+wap.playlist.title = Lista
+wap.playlist.noplayer = Nenhum Leitor ligado
+wap.playlist.clear = Limpar
+wap.playlist.load = Abrir
+wap.playlist.random = Aleatório
+wap.playlist.play = Reproduzir no telemóvel
+
+# wap/search.jsp
+wap.search.title = Pesquisa
+
+# wap/searchResult.jsp
+wap.searchresult.index = O índice de pesquisa está a ser criado. Por favor, tente novamente mais tarde.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Selecionar Leitor
+wap.settings.allplayers = Todos
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ru.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ru.properties
new file mode 100644
index 00000000..c72169d1
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ru.properties
@@ -0,0 +1,664 @@
+#
+# Russian localization.
+# Authors: Iaroslav Andrusiak (pontostroy at mail.ru)
+# and Anton Khoruzhy (antroids at gmail.com)
+
+common.home = \u0413\u043B\u0430\u0432\u043D\u0430\u044F
+common.back = \u041D\u0430\u0437\u0430\u0434
+common.help = \u0418\u043D\u0444\u043E
+common.play = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u0442\u044C
+common.add = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C
+common.download = \u0421\u043A\u0430\u0447\u0430\u0442\u044C
+common.close = \u0417\u0430\u043A\u0440\u044B\u0442\u044C
+common.refresh = \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C
+common.next = \u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439
+common.previous = \u041F\u0440\u0435\u0434\u0438\u0434\u0443\u0449\u0438\u0439
+common.more = \u041E\u041A
+common.ok = \u041E\u041A
+common.cancel = \u041E\u0442\u043C\u0435\u043D\u0430
+common.save = \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C
+common.create = \u0421\u043E\u0437\u0434\u0430\u0442\u044C
+common.delete = \u0423\u0434\u0430\u043B\u0438\u0442\u044C
+common.unknown = (\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)
+common.default = (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E)
+
+# login.jsp
+login.username = \u0418\u043C\u044F
+login.password = \u041F\u0430\u0440\u043E\u043B\u044C
+login.login = \u0412\u043E\u0439\u0442\u0438
+login.remember = \u0417\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C
+login.logout = \u0412\u044B \u0432\u044B\u0448\u043B\u0438
+login.error = \u0418\u043C\u044F \u0438\u043B\u0438 \u043F\u0430\u0440\u043E\u043B\u044C \u043D\u0435\u0432\u0435\u0440\u043D\u044B.
+login.insecure = {0} \u0432 \u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u0438. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441 \u0438\u043C\u0435\u043D\u0435\u043C \u0438 <br> \u043F\u0430\u0440\u043E\u043B\u0435\u043C "admin". \u0417\u0430\u0442\u0435\u043C \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C.
+
+# accessDenied.jsp
+accessDenied.title = \u0414\u043E\u0441\u0442\u0443\u043F \u0437\u0430\u043F\u0440\u0435\u0449\u0451\u043D
+accessDenied.text = \u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, \u0432\u044B \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043B\u0438\u0441\u044C \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043C\u0443\u044E \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044E.
+
+# top.jsp
+top.home = \u0413\u043B\u0430\u0432\u043D\u0430\u044F
+top.now_playing = \u041F\u043B\u0435\u0435\u0440
+top.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+top.status = \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430
+top.podcast = \u041F\u043E\u0434\u043A\u0430\u0441\u0442
+top.more = \u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E
+top.help = \u041E \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0435
+top.search = \u041F\u043E\u0438\u0441\u043A
+top.upgrade = <b>\u0412\u043D\u0438\u043C\u0435\u043D\u0438\u0435!</b> \u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430<br>\u0421\u043A\u0430\u0447\u0430\u0442\u044C {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u0442\u0443\u0442</a>.
+top.missing = \u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043F\u0430\u043F\u043E\u043A \u0441 \u043C\u0443\u0437\u044B\u043A\u043E\u0439. \u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438.
+top.logout = \u0412\u044B\u0439\u0442\u0438 {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;\u0410\u0440\u0442\u0438\u0441\u0442\u043E\u0432<br>\
+ {1}&nbsp;\u0410\u043B\u044C\u0431\u043E\u043C\u043E\u0432<br>\
+ {2}&nbsp;\u041F\u0435\u0441\u0435\u043D<br>\
+ {3} (&#126; {4} \u0447\u0430\u0441\u043E\u0432)
+left.shortcut = \u0421\u0441\u044B\u043B\u043A\u0438
+left.radio = \u0418\u043D\u0442\u0435\u0440\u043D\u0435\u0442 TV/radio
+left.allfolders = \u0412\u0441\u0435 \u043F\u0430\u043F\u043A\u0438
+
+# playlist.jsp
+playlist.stop = \u0421\u0442\u043E\u043F
+playlist.start = \u0421\u0442\u0430\u0440\u0442
+playlist.confirmclear = \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C
+playlist.clear = \u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C
+playlist.shuffle = \u0421\u043C\u0435\u0448\u0430\u0442\u044C
+playlist.repeat_on = \u041F\u043E\u0432\u0442\u043E\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D
+playlist.repeat_off = \u041F\u043E\u0432\u0442\u043E\u0440 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D
+playlist.undo = \u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C
+playlist.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+playlist.more = \u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F
+playlist.more.playlist = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+playlist.more.sortbytrack = \u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E \u0442\u0440\u0435\u043A\u0443
+playlist.more.sortbyartist = \u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E \u0430\u0440\u0442\u0438\u0441\u0442\u0443
+playlist.more.sortbyalbum = \u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E \u0430\u043B\u044C\u0431\u043E\u043C\u0443
+playlist.more.selection = \u0412\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0435 \u043F\u0435\u0441\u043D\u0438
+playlist.more.selectall = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435
+playlist.more.selectnone = \u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435
+playlist.getflash = \u0421\u043A\u0430\u0447\u0430\u0442\u044C Flash-\u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C
+playlist.load = \u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C
+playlist.save = \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C
+playlist.append = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0432 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+playlist.remove = \u0423\u0434\u0430\u043B\u0438\u0442\u044C
+playlist.up = \u0412\u0432\u0435\u0440\u0445
+playlist.down = \u0412\u043D\u0438\u0437
+playlist.empty = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u043F\u0443\u0441\u0442\u043E\u0439
+
+# status.jsp
+status.title = \u0421\u0442\u0430\u0442\u0443\u0441
+status.type = \u0422\u0438\u043F
+status.stream = \u041F\u043E\u0442\u043E\u043A
+status.download = \u0421\u043A\u0430\u0447\u0438\u0432\u0430\u0435\u0442
+status.upload = \u0417\u0430\u043A\u0430\u0447\u0438\u0432\u0430\u0435\u0442
+status.player = \u041F\u043B\u0435\u0435\u0440
+status.user = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C
+status.current = \u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u0444\u0430\u0439\u043B
+status.transmitted = \u041E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043D\u043E
+status.bitrate = \u0411\u0438\u0442\u0440\u0435\u0439\u0442 (Kbps)
+
+# search.jsp
+search.title = \u041F\u043E\u0438\u0441\u043A
+search.search = \u0418\u0441\u043A\u0430\u0442\u044C
+search.index = \u0421\u0435\u0439\u0447\u0430\u0441 \u0444\u0430\u0439\u043B\u044B \u0438\u043D\u0434\u0435\u043A\u0441\u0438\u0440\u0443\u044E\u0442\u0441\u044F \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0434\u0436\u0435.
+search.hits.none = \u0421\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.
+
+# gettingStarted.jsp
+gettingStarted.title = \u041E\u0437\u043D\u0430\u043A\u043E\u043C\u043B\u0435\u043D\u0438\u0435
+gettingStarted.text = <p>\u0414\u043E\u0431\u0440\u043E \u043F\u043E\u0436\u0430\u043B\u043E\u0432\u0430\u0442\u044C \u0432 Subsonic! \u041C\u044B \u043F\u043E\u043C\u043E\u0436\u0435\u043C \u0432\u0430\u043C \u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u0437\u0430 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E\u0441\u0442\u044B\u0445 \u0448\u0430\u0433\u043E\u0432.<br> \
+ \u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043A\u043D\u043E\u043F\u043A\u0443 "\u0413\u043B\u0430\u0432\u043D\u0430\u044F" \u043D\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432, \u0447\u0442\u043E \u0431\u044B \u0432\u0435\u0440\u043D\u0443\u0442\u044C\u0441\u044F \u0441\u044E\u0434\u0430.</p>
+gettingStarted.step1.title = \u0421\u043C\u0435\u043D\u0430 \u043F\u0430\u0440\u043E\u043B\u044F \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430.
+gettingStarted.step1.text = \u0417\u0430\u0449\u0438\u0442\u0438\u0442\u0435 \u0432\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440, \u0441\u043C\u0435\u043D\u0438\u0432 \u043F\u0430\u0440\u043E\u043B\u044C \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430. \
+ \u0422\u0430\u043A \u0436\u0435 \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C \u0434\u0440\u0443\u0433\u0438\u0435 \u0430\u043A\u0430\u0443\u043D\u0442\u044B \u0441 \u0440\u0430\u0437\u043B\u0438\u0447\u043D\u044B\u043C\u0438 \u043F\u0440\u0438\u0432\u0438\u043B\u0435\u0433\u0438\u044F\u043C\u0438.
+gettingStarted.step2.title = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043F\u0430\u043F\u043E\u043A \u0441 \u043C\u0443\u0437\u044B\u043A\u043E\u0439.
+gettingStarted.step2.text = \u041F\u043E\u043A\u0430\u0436\u0438\u0442\u0435 Subsonic \u0433\u0434\u0435 \u0432\u044B \u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u043C\u0443\u0437\u044B\u043A\u0443.
+gettingStarted.step3.title = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0441\u0435\u0442\u0438.
+gettingStarted.step3.text = \u041D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u044B\u0445 \u043E\u043F\u0446\u0438\u0439, \u0435\u0441\u043B\u0438 \u0432\u044B \u043F\u043B\u0430\u043D\u0438\u0440\u0443\u0435\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u0447\u0435\u0440\u0435\u0437 \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442, \
+ \u043B\u0438\u0431\u043E \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0439 \u0441\u0435\u0442\u0438.
+gettingStarted.hide = \u041D\u0435 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u044D\u0442\u043E \u0431\u043E\u043B\u044C\u0448\u0435
+gettingStarted.hidealert = \u0427\u0442\u043E \u0431\u044B \u043E\u043F\u044F\u0442\u044C \u0443\u0432\u0438\u0434\u0435\u0442\u044C \u044D\u0442\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0437\u0430\u0439\u0434\u0438\u0442\u0435 \u0432 \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 - \u041E\u0441\u043D\u043E\u0432\u043D\u044B\u0435.
+
+# home.jsp
+home.random.title = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0435
+home.newest.title = \u041D\u043E\u0432\u044B\u0435
+home.highest.title = \u0421 \u0432\u044B\u0441\u043E\u043A\u0438\u043C\u0438 \u043E\u0446\u0435\u043D\u043A\u0430\u043C\u0438
+home.frequent.title = \u0427\u0430\u0441\u0442\u043E \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0438\u0432\u0430\u0435\u043C\u044B\u0435
+home.recent.title = \u041D\u0435\u0434\u0430\u0432\u043D\u043E \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0438\u0432\u0430\u0435\u043C\u044B\u0435
+home.users.title = \u042E\u0437\u0435\u0440\u044B
+home.random.text = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0435 \u0430\u043B\u044C\u0431\u043E\u043C\u044B
+home.newest.text = \u041D\u0435\u0434\u0430\u0432\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0439 \u0438\u043B\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u044B\u0439
+home.highest.text = \u0421 \u0432\u044B\u0441\u043E\u043A\u0438\u043C\u0438 \u043E\u0446\u0435\u043D\u043A\u0430\u043C\u0438
+home.frequent.text = \u0427\u0430\u0441\u0442\u043E \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u0430\u043B\u044C\u0431\u043E\u043C\u044B
+home.recent.text = \u041D\u0435\u0434\u0430\u0432\u043D\u043E \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u0430\u043B\u044C\u0431\u043E\u043C\u044B
+home.users.text = \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439
+home.scan = \u041F\u0430\u043F\u043A\u0430 \u0441 \u043C\u0443\u0437\u044B\u043A\u043E\u0439 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F. \u0416\u0434\u0438\u0442\u0435.
+home.listsize = {0} \u0430\u043B\u044C\u0431\u043E\u043C\u043E\u0432 \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435
+home.albums = \u0410\u043B\u044C\u0431\u043E\u043C\u044B {0} - {1}
+home.playcount = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u043D\u043E {0} \u0440\u0430\u0437
+home.lastplayed = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u043D\u043E {0}
+home.created = \u0421\u043E\u0437\u0434\u0430\u043D\u043D\u043E {0}
+home.chart.total = \u0412\u0441\u0435\u0433\u043E (MB)
+home.chart.stream = \u041E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043D\u043E \u043F\u043E\u0442\u043E\u043A\u043E\u043C (MB)
+home.chart.download = \u0421\u043A\u0430\u0447\u0435\u043D\u043E (MB)
+home.chart.upload = \u0417\u0430\u043A\u0430\u0447\u0435\u043D\u043E (MB)
+
+# more.jsp
+more.title = \u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E
+more.random.title = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0439 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+more.random.text = \u0421\u043E\u0437\u0434\u0430\u0442\u044C \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0439 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+more.random.songs = {0} \u041F\u0435\u0441\u0435\u043D
+more.random.auto = \u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u044C \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0435 \u0442\u0440\u0435\u043A\u0438, \u0435\u0441\u043B\u0438 \u0434\u043E\u0441\u0442\u0438\u0433\u043D\u0443\u0442 \u043A\u043E\u043D\u0435\u0439 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430.
+more.random.ok = OK
+more.random.genre = \u041F\u043E \u0436\u0430\u043D\u0440\u0443
+more.random.anygenre = \u0412\u0441\u0435
+more.random.year = \u0438 \u0433\u043E\u0434\u0443
+more.random.anyyear = \u0412\u0441\u0435
+more.random.folder = \u0432 \u043F\u0430\u043F\u043A\u0435
+more.random.anyfolder = \u041B\u044E\u0431\u043E\u0439
+more.mobile.title = \u0421\u043B\u0443\u0448\u0430\u0442\u044C \u0441 \u043C\u043E\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E
+more.mobile.text = <p>\u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0441\u043B\u0443\u0448\u0430\u0442\u044C \u043F\u0435\u0441\u043D\u0438 \u0447\u0435\u0440\u0435\u0437 \u043C\u043E\u0431\u0438\u043B\u044C\u043D\u044B\u0439 \u0441 WAP <br> \
+ \u041F\u0440\u043E\u0441\u0442\u043E \u043F\u043E\u0441\u0435\u0442\u0438\u0442\u0435 \u0434\u0430\u043D\u043D\u0443\u044E \u0441\u0441\u044B\u043B\u043A\u0443 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043E\u0439 \u0442\u0435\u043B\u0435\u0444\u043E\u043D: <b>http://yourhostname/wap</b></p> \
+ <p>\u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432\u0438\u0434\u0435\u043D \u0432 \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442\u0435.</p>
+more.podcast.title = \u041F\u043E\u0434\u043A\u0430\u0441\u0442
+more.podcast.text = <p>\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B\u0435 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u044B \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u043A\u0430\u043A \u041F\u043E\u0434\u043A\u0430\u0441\u0442\u044B.<br>\
+ \u0421\u0441\u044B\u043B\u043A\u0430 \u0434\u043B\u044F \u043F\u043E\u0434\u043A\u0430\u0441\u0442\u043E\u0432: <b>http://yourhostname/podcast</b>, \
+ \u0438\u043B\u0438 <b><a href="podcast.view?suffix=.rss">\u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u0441\u044E\u0434\u0430</a>.</b></p>
+more.upload.title = \u0417\u0430\u043A\u0430\u0447\u0430\u0442\u044C \u0444\u0430\u0439\u043B
+more.upload.source = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0444\u0430\u0439\u043B
+more.upload.target = \u0417\u0430\u043A\u0430\u0447\u0430\u0442\u044C \u0432
+more.upload.browse = \u0412\u044B\u0431\u0440\u0430\u0442\u044C
+more.upload.ok = \u0417\u0430\u043A\u0430\u0447\u0430\u0442\u044C
+more.upload.unzip = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u044B\u0432\u0430\u0442\u044C zip.
+more.upload.progress = % \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u043E. \u0416\u0434\u0438\u0442\u0435...
+
+# upload.jsp
+upload.title = \u0417\u0430\u043A\u0430\u0447\u0430\u0442\u044C \u0444\u0430\u0439\u043B
+upload.success = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u0437\u0430\u043A\u0430\u0447\u0430\u043D\u043E <b>{0}</b>
+upload.empty = \u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432 \u0434\u043B\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438.
+upload.failed = \u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0430 \u0438\u0437-\u0437\u0430 \u043E\u0448\u0438\u0431\u043A\u0438:<br><b>"{0}"</b>
+upload.unzipped = \u0420\u0430\u0441\u043F\u0430\u043A\u043E\u0432\u0430\u043D\u043D\u043E {0}
+
+# help.jsp
+help.title = \u041E {0}
+help.upgrade = <b>\u0412\u043D\u0438\u043C\u0430\u043D\u0438\u0435!</b> \u041D\u043E\u0432\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430. \u0421\u043A\u0430\u0447\u0430\u0442\u044C {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u0442\u0443\u0442</a>.
+help.version.title = \u0412\u0435\u0440\u0441\u0438\u044F
+help.builddate.title = \u0414\u0430\u0442\u0430 \u0431\u0438\u043B\u0434\u0430
+help.server.title = \u0421\u0435\u0440\u0432\u0435\u0440
+help.license.title = \u041B\u0438\u0446\u0435\u043D\u0437\u0438\u044F
+help.license.text = {0} \u0431\u0435\u0441\u043F\u043B\u0430\u0442\u043D\u0430\u044F \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0440\u0430\u0441\u043F\u043E\u0441\u0442\u0440\u0430\u043D\u044F\u0435\u043C\u0430\u044F \u043F\u043E <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a>. \
+ {0} \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">\u043B\u0438\u0446\u0435\u043D\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u0442\u0440\u0435\u0442\u044C\u0438\u0445 \u0441\u0442\u043E\u0440\u043E\u043D</a>.
+help.homepage.title = \u0413\u043B\u0430\u0432\u043D\u0430\u044F \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430
+help.forum.title = \u0424\u043E\u0440\u0443\u043C
+help.shop.title = Merchandise
+help.contact.title = \u041A\u043E\u043D\u0442\u0430\u043A\u0442\u044B
+help.contact.text = {0} \u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u043D\u0430 Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ \u043F\u043E \u0432\u0441\u0435\u043C \u0432\u043E\u043F\u0440\u043E\u0441\u0430\u043C \u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u044F\u043C \u043E\u0431\u0440\u0430\u0449\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic \u0424\u043E\u0440\u0443\u043C</a>.
+help.donate = {0} \u0431\u0435\u0441\u043F\u043B\u0430\u0442\u043D\u0430 \u043D\u043E \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043B\u0430\u0442\u044C <b><a href="donate.view?">\u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u043D\u0438\u0435</a></b>.
+help.log = \u041B\u043E\u0433
+help.logfile = \u041F\u043E\u043B\u043D\u044B\u0439 \u043B\u043E\u0433 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D \u0432 {0}.
+
+# settingsHeader.jsp
+settingsheader.title = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+settingsheader.general = \u041E\u0441\u043D\u043E\u0432\u043D\u044B\u0435
+settingsheader.advanced = \u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435
+settingsheader.personal = \u0412\u0438\u0434
+settingsheader.musicFolder = \u041F\u0430\u043F\u043A\u0438 \u0441 \u043C\u0443\u0437\u044B\u043A\u043E\u0439
+settingsheader.internetRadio = \u0418\u043D\u0442\u0435\u0440\u043D\u0435\u0442 TV/\u0440\u0430\u0434\u0438\u043E
+settingsheader.podcast = \u041F\u043E\u0434\u043A\u0430\u0441\u0442
+settingsheader.player = \u041F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u0438
+settingsheader.network = \u0421\u0435\u0442\u0435\u0432\u044B\u0435
+settingsheader.transcoding = \u0422\u0440\u0430\u043D\u0441\u043A\u043E\u0434\u0438\u043D\u0433
+settingsheader.user = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438
+settingsheader.search = \u0418\u043D\u0434\u0435\u043A\u0441\u0430\u0446\u0438\u044F
+settingsheader.coverArt = \u041E\u0431\u043B\u043E\u0436\u043A\u0438
+settingsheader.password = \u041F\u0430\u0440\u043E\u043B\u044C
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \u041F\u0430\u043F\u043A\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u043E\u0432
+generalsettings.musicmask = \u0424\u043E\u0440\u043C\u0430\u0442\u044B \u043C\u0443\u0437\u044B\u043A\u0438
+generalsettings.coverartmask = \u0424\u043E\u0440\u043C\u0430\u0442\u044B \u043E\u0431\u043B\u043E\u0436\u0435\u043A
+generalsettings.index = \u0418\u043D\u0434\u0435\u043A\u0441\u0430\u0446\u0438\u044F
+generalsettings.ignoredarticles = \u0418\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0440\u0435\u0444\u0438\u043A\u0441\u044B
+generalsettings.shortcuts = \u0421\u0441\u044B\u043B\u043A\u0438
+generalsettings.showgettingstarted = \u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C "\u041E\u0437\u043D\u0430\u043A\u043E\u043C\u043B\u0435\u043D\u0438\u0435" \u043F\u0440\u0438 \u0437\u0430\u043F\u0443\u0441\u043A\u0435
+generalsettings.welcometitle = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A
+generalsettings.welcomesubtitle = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u043F\u043E\u0434\u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A
+generalsettings.welcomemessage = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435
+generalsettings.loginmessage = \u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u0432\u0445\u043E\u0434\u0435
+generalsettings.language = \u042F\u0437\u044B\u043A \u043F\u043E-\u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E
+generalsettings.theme = \u0422\u0435\u043C\u0430 \u043F\u043E-\u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0441\u043D\u0438\u0436\u0435\u043D\u0438\u044F \u0441\u0435\u043C\u043F\u043B\u0430
+advancedsettings.coverartlimit = \u041B\u0438\u043C\u0438\u0442 \u043E\u0431\u043B\u043E\u0436\u0435\u043A<br><div class="detail">(0 = \u0411\u0435\u0441\u043A\u043E\u043D\u0435\u0447\u043D\u043E)</div>
+advancedsettings.downloadlimit = \u041B\u0438\u043C\u0438\u0442 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F (Kbps)<br><div class="detail">(0 = Unlimited)</div>
+advancedsettings.uploadlimit = \u041B\u0438\u043C\u0438\u0442 \u0437\u0430\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F (Kbps)<br><div class="detail">( = \u0411\u0435\u0441\u043A\u043E\u043D\u0435\u0447\u043D\u043E)</div>
+advancedsettings.streamport = Non-SSL \u043F\u043E\u0440\u0442 \u043F\u043E\u0442\u043E\u043A\u0430<br><div class="detail">(0 = \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D)</div>
+advancedsettings.ldapenabled = \u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044E LDAP
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = \u0424\u0438\u043B\u044C\u0442\u0440 \u043F\u043E\u0438\u0441\u043A\u0430 LDAP
+advancedsettings.ldapmanagerdn = LDAP \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440 DN<br><div class="detail">(Optional)</div>
+advancedsettings.ldapmanagerpassword = \u041F\u0430\u0440\u043E\u043B\u044C
+advancedsettings.ldapautoshadowing = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 \u0432 {0}
+
+# personalSettings.jsp
+personalsettings.title = \u0412\u043D\u0435\u0448\u043D\u0438\u0439 \u0432\u0438\u0434 \u0434\u043B\u044F {0}
+personalsettings.language = \u042F\u0437\u044B\u043A
+personalsettings.theme = \u0422\u0435\u043C\u0430
+personalsettings.display = \u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C
+personalsettings.browse = \u041E\u0441\u043D\u043E\u0432\u043D\u043E\u0439
+personalsettings.playlist = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+personalsettings.tracknumber = \u0422\u0440\u0435\u043A #
+personalsettings.artist = \u0410\u0440\u0442\u0438\u0441\u0442
+personalsettings.album = \u0410\u043B\u044C\u0431\u043E\u043C
+personalsettings.genre = \u0416\u0430\u043D\u0440
+personalsettings.year = \u0413\u043E\u0434
+personalsettings.bitrate = \u0411\u0438\u0442\u0440\u044D\u0439\u0442
+personalsettings.duration = \u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C
+personalsettings.format = \u0424\u043E\u0440\u043C\u0430\u0442
+personalsettings.filesize = \u0420\u0430\u0437\u043C\u0435\u0440
+personalsettings.captioncutoff = \u0414\u043B\u0438\u043D\u0430
+personalsettings.partymode = \u0423\u043F\u0440\u043E\u0449\u0435\u043D\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C
+personalsettings.shownowplaying = \u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0447\u0442\u043E \u0441\u043B\u0443\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433\u0438\u0435
+personalsettings.nowplayingallowed = \u041F\u043E\u0437\u0432\u043E\u043B\u0438\u0442\u044C \u0434\u0440\u0443\u0433\u0438\u043C \u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0447\u0442\u043E \u044F \u0441\u0435\u0439\u0447\u0430\u0441 \u0441\u043B\u0443\u0448\u0430\u044E
+personalsettings.showchat = \u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0447\u0430\u0442\u0430
+personalsettings.finalversionnotification = \u0421\u043E\u043E\u0431\u0449\u0430\u0442\u044C \u043E \u043D\u043E\u0432\u044B\u0445 \u0432\u0435\u0440\u0441\u0438\u044F\u0445
+personalsettings.betaversionnotification = \u0421\u043E\u043E\u0431\u0449\u0430\u0442\u044C \u043E \u043D\u043E\u0432\u044B\u0445 \u0431\u0435\u0442\u0430-\u0432\u0435\u0440\u0441\u0438\u044F\u0445
+personalsettings.lastfmenabled = \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0447\u0442\u043E \u044F \u0441\u043B\u0443\u0448\u0430\u044E \u043D\u0430 <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm \u043B\u043E\u0433\u0438\u043D
+personalsettings.lastfmpassword = Last.fm \u043F\u0430\u0440\u043E\u043B\u044C
+personalsettings.avatar.title = \u041F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u044C\u043D\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435
+personalsettings.avatar.none = \u041D\u0435\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F
+personalsettings.avatar.custom = \u0421\u0432\u043E\u0451 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435
+personalsettings.avatar.changecustom = \u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0432\u043E\u0451 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435
+personalsettings.avatar.upload = \u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C
+personalsettings.avatar.courtesy = \u0418\u043A\u043E\u043D\u043A\u0438 \u043B\u044E\u0431\u0435\u0437\u043D\u043E \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = \u0421\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u044C\u043D\u043E\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435
+avataruploadresult.success = \u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u043E "{0}".
+avataruploadresult.failure = \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F. \u041F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0432 <a href="help.view?">\u043B\u043E\u0433\u0435</a>.
+
+# passwordSettings.jsp
+passwordsettings.title = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C \u0434\u043B\u044F {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \u041F\u0430\u043F\u043A\u0430
+musicfoldersettings.name = \u0418\u043C\u044F
+musicfoldersettings.enabled = \u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0430
+musicfoldersettings.add = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0430\u043F\u043A\u0438 \u0441 \u043C\u0443\u0437\u044B\u043A\u043E\u0439
+musicfoldersettings.nopath = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0430\u043F\u043A\u0443.
+
+# networkSettings.jsp
+networksettings.text = \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u044D\u0442\u0438 \u043E\u043F\u0446\u0438\u0438 \u0434\u043B\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0432\u0430\u0448\u0435\u043C\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0447\u0435\u0440\u0435\u0437 \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442.
+networksettings.portforwardingenabled = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C \u0432\u0430\u0448 \u0440\u043E\u0443\u0442\u0435\u0440 \u0434\u043B\u044F \u043F\u0440\u0438\u043D\u044F\u0442\u0438\u044F \u0432\u0445\u043E\u0434\u044F\u0449\u0438\u0445 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0439 \u043A Subsonic (UPnP port forwarding).
+networksettings.urlredirectionenabled = \u0421\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \u0432\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u043C \u043F\u043E \u043B\u0435\u0433\u043A\u043E\u0437\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u044E\u0449\u0435\u043C\u0443\u0441\u044F \u0430\u0434\u0440\u0435\u0441\u0443.
+networksettings.status = \u0421\u0442\u0430\u0442\u0443\u0441:
+networksettings.trialexpired = \u0422\u0435\u0441\u0442\u043E\u0432\u044B\u0439 \u043F\u0435\u0440\u0438\u043E\u0434 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u0442\u0441\u044F {0}. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430 <b><a href="donate.view?">\u043F\u043E\u0436\u0435\u0440\u0442\u0432\u0443\u0439\u0442\u0435</a></b> \u0434\u043B\u044F \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0431\u044B \u043D\u0430 \u0432\u0441\u0435\u0433\u0434\u0430 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u044D\u0442\u0443 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C.
+networksettings.trialnotexpired = \u042D\u0442\u0430 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0431\u0443\u0434\u0435\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u0430 \u0434\u043E {0}. \u041F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E \u0432\u044B \u0434\u043E\u043B\u0436\u043D\u044B <b><a href="donate.view?">\u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u0442\u044C</a></b> \u0434\u043B\u044F \u0434\u0430\u043B\u044C\u043D\u0435\u0439\u0448\u0435\u0433\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u044D\u0442\u043E\u0433\u0439 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u0438.
+
+# transcodingSettings.jsp
+transcodingsettings.name = \u0418\u043C\u044F
+transcodingsettings.sourceformat = \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441
+transcodingsettings.targetformat = \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432
+transcodingsettings.step1 = \u0428\u0430\u0433 1
+transcodingsettings.step2 = \u0428\u0430\u0433 2
+transcodingsettings.step3 = \u0428\u0430\u0433 3
+transcodingsettings.defaultactive = \u041F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E
+transcodingsettings.enabled = \u0412\u043A\u043B\u044E\u0447\u0435\u043D\u043E
+transcodingsettings.add = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u043E\u0440
+transcodingsettings.noname = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F.
+transcodingsettings.nosourceformat = \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441.
+transcodingsettings.notargetformat = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432.
+transcodingsettings.nostep1 = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B 1 \u0448\u0430\u0433.
+transcodingsettings.info = <p class="detail">(%s = \u0424\u0430\u0439\u043B, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u0430\u043D, %b = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0431\u0438\u0442\u0440\u044D\u0439\u0442 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F)</p> \
+ <p>\u041F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 - \u044D\u0442\u043E \u043F\u0440\u043E\u0446\u0435\u0441\u0441, \u043F\u0440\u0438 \u043A\u043E\u0442\u043E\u0440\u043E\u043C \u043E\u0434\u0438\u043D \u0444\u043E\u0440\u043C\u0430\u0442 \u043C\u0435\u0434\u0438\u0430 \u0444\u0430\u0439\u043B\u0430 \u0441\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439. \u0421\u0438\u0441\u0442\u0435\u043C\u0430 \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u043A\u0438 {1} \
+ \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u044C \u0442\u0430\u043A\u0438\u0435 \u043C\u0435\u0434\u0438\u0430-\u0444\u0430\u0439\u043B\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u044F\u0442\u0441\u044F \u043D\u0430 \u043B\u0435\u0442\u0443. \u041F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0438\u0432\u0430\u043D\u0438\u044F \
+ \u0438 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0434\u0438\u0441\u043A\u043E\u0432\u043E\u0433\u043E \u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u0430.<p/> \
+ <p>\u0424\u0430\u043A\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u043A\u0430 \u043E\u0431\u0435\u0441\u043F\u0435\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0443\u0436\u0435 \u0433\u043E\u0442\u043E\u0432\u044B\u043C\u0438 \u043A\u043E\u043D\u0441\u043E\u043B\u044C\u043D\u044B\u043C\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438 \u0442\u0440\u0435\u0442\u044C\u0438\u0445 \u0441\u0442\u043E\u0440\u043E\u043D, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043C\u043E\u0433\u0443\u0442 \u0431\u044B\u0442\u044C \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B \u0432 {0}. \
+ \u041F\u0430\u043A\u0435\u0442 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u043A\u0438 \u043F\u043E\u0434 Windows \
+ \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>\u0437\u0434\u0435\u0441\u044C</b></a>. \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u0449\u0438\u043A, \
+ \u043D\u043E \u043E\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u0443\u0434\u043E\u0432\u043B\u0435\u0442\u0432\u043E\u0440\u044F\u0442\u044C \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0443\u0441\u043B\u043E\u0432\u0438\u044F\u043C: \
+ <ul> \
+ <li>\u0418\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043A\u043E\u043D\u0441\u043E\u043B\u044C\u043D\u044B\u043C.</li> \
+ <li>\u041E\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0432 stdout.</li> \
+ <li>\u0415\u0441\u043B\u0438 \u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043D\u0430 2 \u0438\u043B\u0438 3 \u0441\u0442\u0430\u0434\u0438\u0438 \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F, \u0442\u043E \u043E\u043D \u0434\u043E\u043B\u0436\u0435\u043D \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E stdin.</li> \
+ </ul> \
+ </p> \
+ <p> \u041F\u043E\u043C\u043D\u0438\u0442\u0435, \u0447\u0442\u043E \u043A\u0430\u0436\u0434\u044B\u0439 \u043C\u0435\u0442\u043E\u0434 \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F \u0438\u043D\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043B\u044C\u043D\u043E \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F. \u0415\u0441\u043B\u0438 \u043C\u0435\u0442\u043E\u0434 \u043F\u043E\u043C\u0435\u0447\u0435\u043D \
+ "\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E", \u0442\u043E \u043E\u043D \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u043D\u043E\u0432\u043E\u0433\u043E \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL \u043F\u043E\u0442\u043E\u043A\u0430
+internetradiosettings.homepageurl = \u0414\u043E\u043C\u0430\u0448\u043D\u044F\u044F \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430
+internetradiosettings.name = \u0418\u043C\u044F
+internetradiosettings.enabled = \u0412\u043A\u043B\u044E\u0447\u0435\u043D\u043E
+internetradiosettings.add = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442 TV/\u0440\u0430\u0434\u0438\u043E
+internetradiosettings.nourl = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 URL.
+internetradiosettings.noname = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F.
+
+# podcastSettings.jsp
+podcastsettings.update = \u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u043E\u0432\u044B\u0435
+podcastsettings.keep = \u041F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u0442\u044C
+podcastsettings.keep.all = \u0412\u0441\u0435
+podcastsettings.keep.one = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439
+podcastsettings.keep.many = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0438 {0}
+podcastsettings.download = \u041D\u043E\u0432\u044B\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B
+podcastsettings.download.all = \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0432\u0441\u0435
+podcastsettings.download.one = \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u044E
+podcastsettings.download.many = \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435\u043D\u0438\u0438 {0}
+podcastsettings.download.none = \u041D\u0438\u0447\u0435\u0433\u043E
+podcastsettings.interval.manually = \u0412\u0440\u0443\u0447\u043D\u0443\u044E
+podcastsettings.interval.hourly = \u041A\u0430\u0436\u0434\u044B\u0439 \u0447\u0430\u0441
+podcastsettings.interval.daily = \u041A\u0430\u0436\u0434\u044B\u0439 \u0434\u0435\u043D\u044C
+podcastsettings.interval.weekly = \u041A\u0430\u0436\u0434\u0443\u044E \u043D\u0435\u0434\u0435\u043B\u044E
+podcastsettings.folder = \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u043E\u0434\u043A\u0430\u0441\u0442 \u0432
+
+# playerSettings.jsp
+playersettings.noplayers = \u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E \u043F\u043B\u0435\u0435\u0440\u043E\u0432.
+playersettings.type = \u0422\u0438\u043F
+playersettings.lastseen = \u0412\u0438\u0434\u0435\u043D \u0432 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0440\u0430\u0437
+playersettings.title = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430
+
+playersettings.technology.web.title = \u0412\u0435\u0431 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C
+playersettings.technology.external.title = \u0412\u043D\u0435\u0448\u043D\u0438\u0439 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C
+playersettings.technology.external_with_playlist.title = \u0412\u043D\u0435\u0448\u043D\u0438\u0439 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u043E\u043C
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = \u0412\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442 \u043C\u0443\u0437\u044B\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0432 \u0438\u043D\u0442\u0435\u0433\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u043C flash-\u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u0435.
+playersettings.technology.external.text = \u0412\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442 \u043C\u0443\u0437\u044B\u043A\u0443 \u0432 \u0432\u0430\u0448\u0435\u043C \u043B\u044E\u0431\u0438\u043C\u043E\u043C \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u0435, \u0442\u0430\u043A\u043E\u043C \u043A\u0430\u043A WinAmp \u0438\u043B\u0438 Windows Media Player.
+playersettings.technology.external_with_playlist.text =\u041A\u0430\u043A \u0438 \u043F\u0440\u0435\u0434\u0438\u0434\u0443\u0449\u0438\u0439, \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C \u0438\u043C\u0435\u0435\u0442 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \
+ \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u044B \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 Subsonic. \u0412 \u044D\u0442\u043E\u043C \u0440\u0435\u0436\u0438\u043C\u0435 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u044B \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0438 \u0442\u0440\u0435\u043A\u043E\u0432.
+playersettings.technology.jukebox.text = \u0412\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0435 \u043C\u0443\u0437\u044B\u043A\u0443 \u043D\u0430 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0435, \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u043C \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 Subsonic. (\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439).
+playersettings.name = \u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u043B\u0435\u0435\u0440\u0430
+playersettings.coverartsize = \u0420\u0430\u0437\u043C\u0435\u0440 \u043E\u0431\u043B\u043E\u0436\u043A\u0438
+playersettings.maxbitrate = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0431\u0438\u0442\u0440\u044D\u0439\u0442
+playersettings.coverart.off = \u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E
+playersettings.coverart.small = \u041C\u0430\u043B\u0435\u043D\u044C\u043A\u0438\u0439
+playersettings.coverart.medium = \u0421\u0440\u0435\u0434\u043D\u0438\u0439
+playersettings.coverart.large = \u0411\u043E\u043B\u044C\u0448\u043E\u0439
+playersettings.nolame = <em>\u0412\u043D\u0438\u043C\u0430\u043D\u0438\u0435:</em> LAME \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D.<br>\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043A\u043D\u043E\u043F\u043A\u0443 \u043F\u043E\u043C\u043E\u0449\u0438 \u0434\u043B\u044F \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0438.
+playersettings.autocontrol = \u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438
+playersettings.dynamicip = \u0423 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0434\u0438\u043D\u0430\u043C\u0438\u0447\u0435\u0441\u043A\u0438\u0439 IP
+playersettings.transcodings = \u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0442\u0440\u0430\u043D\u0441\u043A\u043E\u0434\u0438\u043D\u0433
+playersettings.ok = \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C
+playersettings.forget = \u0423\u0434\u0430\u043B\u0438\u0442\u044C
+playersettings.clone = \u041A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C
+
+# userSettings.jsp
+usersettings.title = \u0412\u044B\u0431\u0440\u0430\u0442\u044C
+usersettings.newuser = \u041D\u043E\u0432\u044B\u0439
+usersettings.admin = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C - \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440
+usersettings.settings = \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044E \u043C\u0435\u043D\u044F\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0438 \u043F\u0430\u0440\u043E\u043B\u044C
+usersettings.stream = \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044E \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0438\u0432\u0430\u0442\u044C \u0442\u0440\u0435\u043A\u0438
+usersettings.jukebox = \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044E \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u0432 \u0440\u0435\u0436\u0438\u043C\u0435 jukebox
+usersettings.download = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043C\u043E\u0436\u0435\u0442 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u0442\u044C
+usersettings.upload = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043C\u043E\u0436\u0435\u0442 \u0437\u0430\u043A\u0430\u0447\u0438\u0432\u0430\u0442\u044C
+usersettings.playlist= \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C \u0438 \u0443\u0434\u0430\u043B\u044F\u0442\u044C \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u044B
+usersettings.coverart = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043C\u043E\u0436\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u044F\u0442\u044C \u043E\u0431\u043B\u043E\u0436\u043A\u0438
+usersettings.comment= \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043C\u043E\u0436\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u044F\u0442\u044C \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u0438 \u043E\u0446\u0435\u043D\u043A\u0438
+usersettings.podcast= \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043C\u043E\u0436\u0435\u0442 \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u043F\u043E\u0434\u043A\u0430\u0441\u0442\u043E\u043C
+usersettings.username = \u0418\u043C\u044F
+usersettings.changepassword = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C
+usersettings.password = \u041F\u0430\u0440\u043E\u043B\u044C
+usersettings.newpassword = \u041D\u043E\u0432\u044B\u0439 \u043F\u0430\u0440\u043E\u043B\u044C
+usersettings.confirmpassword = \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C
+usersettings.delete = \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+usersettings.ldap = \u0410\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0432 LDAP
+usersettings.nousername = \u041D\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C
+usersettings.useralreadyexists = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0443\u0436\u0435 \u0435\u0441\u0442\u044C
+usersettings.nopassword = \u041D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u043E\u043B\u044C
+usersettings.wrongpassword = \u041D\u0435 \u0442\u043E\u0442 \u043F\u0430\u0440\u043E\u043B\u044C
+usersettings.ldapdisabled = LDAP \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044F \u043D\u0435 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u0430. \u0421\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438.
+usersettings.passwordnotsupportedforldap = \u041D\u0435\u043B\u044C\u0437\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C \u0434\u043B\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439, \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0432 LDAP.
+usersettings.ok = \u041F\u0430\u0440\u043E\u043B\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D \u0434\u043B\u044F {0}.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = \u041D\u0438\u043A\u043E\u0433\u0434\u0430
+musicfoldersettings.interval.one = \u041A\u0430\u0436\u0434\u044B\u0439 \u0434\u0435\u043D\u044C
+musicfoldersettings.interval.many = \u041A\u0430\u0436\u0434\u044B\u0435 {0} \u0434\u043D\u044F
+musicfoldersettings.hour = \u0432 {0}:00
+
+# coverArtSettings.jsp
+coverartsettings.auto = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0438\u0435 \u043E\u0431\u043B\u043E\u0436\u043A\u0438 \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F \u0438\u043D\u0434\u0435\u043A\u0441\u0430.
+coverartsettings.manual = \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0438\u0435 \u043E\u0431\u043B\u043E\u0436\u043A\u0438 \u0441\u0435\u0439\u0447\u0430\u0441.
+coverartsettings.missing = {0} \u0438\u0437 {1} \u0430\u043B\u044C\u0431\u043E\u043C\u043E\u0432 \u043D\u0435 \u0438\u043C\u0435\u044E\u0442 \u043E\u0431\u043B\u043E\u0436\u0435\u043A.
+coverartsettings.running = \u0418\u0434\u0435\u0442 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435 \u043E\u0431\u043B\u043E\u0436\u0435\u043A. \u042D\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u0437\u0430\u043D\u044F\u0442\u044C \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043C\u0438\u043D\u0443\u0442, \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \
+ \u043E\u0442 \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u0432\u0430\u0448\u0435\u0439 \u043C\u0435\u0434\u0438\u0430-\u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438.
+coverartsettings.albumList = \u0421\u043F\u0438\u0441\u043E\u043A \u0430\u043B\u044C\u0431\u043E\u043C\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043D\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442 \u043E\u0431\u043B\u043E\u0436\u043A\u0438.
+
+# main.jsp
+main.up = \u0412\u0432\u0435\u0440\u0445
+main.playall = \u0412\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u0441\u0442\u0438 \u0432\u0441\u0435
+main.playrandom = \u0412\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u0441\u0442\u0438 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u043E
+main.addall = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0432\u0441\u0435
+main.tags = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u0435\u0433
+main.playcount = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u043D\u043E {0} \u0440\u0430\u0437.
+main.lastplayed = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0440\u0430\u0437 \u043F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u043D\u043E {0}.
+main.comment = \u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__\u0442\u0435\u043A\u0441\u0442__</td><td>\u0416\u0438\u0440\u043D\u044B\u0439 </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>\u0420\u0430\u0437\u0440\u044B\u0432 \u0441\u0442\u0440\u043E\u043A\u0438</td></tr>\
+ <tr><td style="padding-right:1em">~~\u0442\u0435\u043A\u0441\u0442~~</td><td>\u041A\u0443\u0440\u0441\u0438\u0432 </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>\u041D\u043E\u0432\u044B\u0439 \u0430\u0431\u0437\u0430\u0446</td></tr>\
+ <tr><td style="padding-right:1em">* \u0442\u0435\u043A\u0441\u0442 </td><td>\u0421\u043F\u0438\u0441\u043E\u043A </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>\u0421\u0441\u044B\u043B\u043A\u0430</td></tr>\
+ <tr><td style="padding-right:1em">1. \u0442\u0435\u043A\u0441\u0442 </td><td>\u041D\u0443\u043C\u0435\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0441\u043F\u0438\u0441\u043E\u043A</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>\u0418\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u043D\u0430\u044F \u0441\u0441\u044B\u043B\u043A\u0430</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">\u041F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u0442\u044C</a>{1}!<br>(\u0438 \u0443\u0431\u0440\u0430\u0442\u044C \u044D\u0442\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435)
+main.nowplaying = \u0421\u0435\u0439\u0447\u0430\u0441 \u0438\u0433\u0440\u0430\u0435\u0442
+main.lyrics = \u0422\u0435\u043A\u0441\u0442
+main.minutesago = \u043C\u0438\u043D\u0443\u0442 \u043D\u0430\u0437\u0430\u0434
+main.chat = \u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0447\u0430\u0442\u0430
+main.message = \u041D\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435
+
+# rating.jsp
+rating.rating = \u041E\u0446\u0435\u043D\u043A\u0430
+rating.clearrating = \u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043E\u0446\u0435\u043D\u043A\u0438
+
+# coverArt.jsp
+coverart.change = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C
+coverart.zoom = \u0423\u0432\u0435\u043B\u0438\u0447\u0438\u0442\u044C
+
+# allmusic.jsp
+allmusic.text = \u0418\u0449\u0435\u0442\u0441\u044F \u0430\u043B\u044C\u0431\u043E\u043C <em>{0}</em> \u043D\u0430 allmusic.com - \u0436\u0434\u0438\u0442\u0435.
+
+# changeCoverArt.jsp
+changecoverart.title = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043E\u0431\u043B\u043E\u0436\u043A\u0443
+changecoverart.address = \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u043E\u0431\u043B\u043E\u0436\u043A\u0438
+changecoverart.artist = \u0418\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C
+changecoverart.album = \u0410\u043B\u044C\u0431\u043E\u043C
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0441\u043C\u0435\u043D\u0435 \u043E\u0431\u043B\u043E\u0436\u043A\u0438:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0442\u044D\u0433
+edittags.file = \u0424\u0430\u0439\u043B
+edittags.track = \u0422\u0440\u0435\u043A
+edittags.songtitle = \u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435
+edittags.artist = \u0410\u0440\u0442\u0438\u0441\u0442
+edittags.album = \u0410\u043B\u044C\u0431\u043E\u043C
+edittags.year = \u0413\u043E\u0434
+edittags.genre = \u0421\u0442\u0438\u043B\u044C
+edittags.status = \u0421\u0442\u0430\u0442\u0443\u0441
+edittags.suggest = \u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F
+edittags.reset = \u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C
+edittags.working = \u0420\u0430\u0431\u043E\u0442\u0430\u044E
+edittags.updated = \u041E\u0431\u043D\u0430\u0432\u043B\u0435\u043D\u043D\u043E
+edittags.skipped = \u041F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043D\u043E
+edittags.error = \u041E\u0448\u0438\u0431\u043A\u0430
+
+# donate.jsp
+donate.title = \u041F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u0442\u044C
+donate.invalidlicense = \u041D\u0435 \u0442\u043E\u0442 \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u043E\u043D\u043D\u044B\u0439 \u043A\u043B\u044E\u0447.
+donate.amount = \u041F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u0442\u044C {0}
+donate.textbefore = <p>\u0421\u043F\u0430\u0441\u0438\u0431\u043E \u0437\u0430 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 {0}! \
+ \u041F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u0432 \u043F\u0440\u043E\u0435\u043A\u0442\u0443 \u0432\u044B \u043F\u043E\u043B\u0443\u0447\u0430\u0435\u0442\u0435 \u043A\u043B\u044E\u0447, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043E \u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u043D\u0438\u0438, \u043D\u0435\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043D\u043E \
+ \u0441\u043B\u0443\u0448\u0430\u0442\u044C \u043F\u0440\u043E\u0438\u0437\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0447\u0435\u0440\u0435\u0437 \u0442\u0435\u043B\u0435\u0444\u043E\u043D \u0441 Android, \u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0432\u0441\u0435\u043C\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u043D\u044B\u043C\u0438 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044F\u043C\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043D\u044B \u0432 \u0434\u0430\u043B\u044C\u043D\u0435\u0439\u0448\u0435\u043C. \
+ \u042D\u0442\u043E\u0442 \u043A\u043B\u044E\u0447 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0438 \u0432\u0441\u0435\u0445 \u043F\u043E\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0445 \u0432\u0435\u0440\u0441\u0438\u0439 {0}.</p> \
+ <p>\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u043D\u0430\u044F \u0441\u0443\u043C\u043C\u0430 \u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u043D\u0438\u044F <b>&euro;20</b>, \u043D\u043E \u043D\u0438\u0447\u0435\u0433\u043E \u0441\u0442\u0440\u0430\u0448\u043D\u043E\u0433\u043E \u0435\u0441\u043B\u0438 \u0432\u044B \u043F\u043E\u0436\u0435\u0440\u0442\u0432\u0443\u0435\u0442\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u043B\u0438 \u043C\u0435\u043D\u044C\u0448\u0435 \u044D\u0442\u043E\u0439 \u0441\u0443\u043C\u043C\u044B. \
+ \u0423\u0447\u0442\u0438\u0442\u0435, \u0447\u0442\u043E \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u043E\u043D\u043D\u044B\u0439 \u043A\u043B\u044E\u0447 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043F\u043E \u0430\u0434\u0440\u0435\u0441\u0443 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u044B \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0435, \
+ \u043F\u043E\u0442\u043E\u043C\u0443 \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0439\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441 \u043F\u0440\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u043D\u0438\u044F \u043D\u0430 PayPal.</p>
+donate.textafter = <p>\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043D\u0430 \u043E\u0434\u043D\u0443 \u0438\u0437 \u043A\u043D\u043E\u043F\u043E\u043A \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u043A \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u043D\u0438\u044F, \u0433\u0434\u0435 \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u043E\u043F\u043B\u0430\u0442\u0438\u0442\u044C \
+ \u043F\u043E\u0436\u0435\u0440\u0442\u0432\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u0438 \u043F\u043E\u043C\u043E\u0449\u0438 \u0432\u0430\u0448\u0435\u0439 \u043A\u0440\u0435\u0434\u0438\u0442\u043D\u043E\u0439 \u043A\u0430\u0440\u0442\u044B \u0438\u043B\u0438 \u0441\u0447\u0435\u0442\u0430 PayPal, \u043F\u043E\u0441\u043B\u0435 \u0447\u0435\u0433\u043E \u0432\u044B \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u0435 \u043A\u043B\u044E\u0447 \u043D\u0430 \u0441\u0432\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0432\u044B\u0439 \u044F\u0449\u0438\u043A.</p> \
+ <p>\u0415\u0441\u043B\u0438 \u0443 \u0432\u0430\u0441 \u0438\u043C\u0435\u044E\u0442\u0441\u044F \u0432\u043E\u043F\u0440\u043E\u0441\u044B, \u043F\u0438\u0448\u0438\u0442\u0435 \u043F\u043E \u0430\u0434\u0440\u0435\u0441\u0443 \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = \u042D\u0442\u0430 \u043A\u043E\u043F\u0438\u044F {2} \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u0430 \u0434\u043B\u044F {0} \u043D\u0430 {1}. \u0421\u043F\u0430\u0441\u0438\u0431\u043E
+donate.register = \u041F\u043E\u0441\u043B\u0435 \u0442\u043E\u0433\u043E, \u043A\u0430\u043A \u0432\u044B \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u0438 \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u043E\u043D\u043D\u044B\u0439 \u043A\u043B\u044E\u0447, \u043F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0438\u0436\u0435.
+donate.register.email = Email
+donate.register.license = \u041B\u0438\u0446\u0435\u043D\u0437\u0438\u044F
+
+# podcastReceiver.jsp
+podcastreceiver.title = \u041F\u043E\u043B\u0443\u0447\u0430\u0442\u0435\u043B\u044C \u043F\u043E\u0434\u043A\u0430\u0441\u0442\u043E\u0432
+podcastreceiver.expandall = \u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0432\u044B\u043F\u0443\u0441\u043A\u0438
+podcastreceiver.collapseall = \u0421\u043F\u0440\u044F\u0442\u0430\u0442\u044C \u0432\u044B\u043F\u0443\u0441\u043A\u0438
+podcastreceiver.status.new = \u041D\u043E\u0432\u044B\u0439
+podcastreceiver.status.downloading = \u0421\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435
+podcastreceiver.status.completed = \u0412\u044B\u043F\u043E\u043B\u0435\u043D\u043D\u044B\u0435
+podcastreceiver.status.error = \u041E\u0448\u0438\u0431\u043A\u0430
+podcastreceiver.status.deleted = \u0423\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0435
+podcastreceiver.status.skipped = \u041F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043D\u044B\u0435
+podcastreceiver.downloadselected= \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435
+podcastreceiver.deleteselected= \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435
+podcastreceiver.confirmdelete= \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435?
+podcastreceiver.check = \u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u043E\u0432\u044B\u0435
+podcastreceiver.refresh = \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C
+podcastreceiver.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u043E\u0434\u043A\u0430\u0441\u0442
+podcastreceiver.subscribe = \u041F\u043E\u0434\u043F\u0438\u0441\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u043F\u043E\u0434\u043A\u0430\u0441\u0442
+
+# lyrics.jsp
+lyrics.title = \u0422\u0435\u043A\u0441\u0442
+lyrics.artist = \u0410\u0440\u0442\u0438\u0441\u0442
+lyrics.song = \u041F\u0435\u0441\u043D\u044F
+lyrics.search = \u0418\u0441\u043A\u0430\u0442\u044C
+lyrics.wait = \u0418\u0434\u0435\u0442 \u043F\u043E\u0438\u0441\u043A, \u0436\u0434\u0438\u0442\u0435...
+lyrics.courtesy = (Lyrics by <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = \u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.
+
+# helpPopup.jsp
+helppopup.title = {0} \u043F\u043E\u043C\u043E\u0448\u044C
+helppopup.cover.title = \u0420\u0430\u0437\u043C\u0435\u0440 \u043E\u0431\u043B\u043E\u0436\u043A\u0438
+helppopup.cover.text = <p>\u041F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0440\u0430\u0437\u043C\u0435\u0440 \u043E\u0431\u043B\u043E\u0436\u0435\u043A, \u043B\u0438\u0431\u043E \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0438\u0445 \u0441\u043E\u0432\u0441\u0435\u043C.</p>
+helppopup.transcode.title = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0431\u0438\u0442\u0440\u044D\u0439\u0442
+helppopup.transcode.text = <p>\u0415\u0441\u043B\u0438 \u0443 \u0432\u0430\u0441 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043D\u0430\u044F \u043F\u043E\u043B\u043E\u0441\u0430 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u043D\u0438\u044F, \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0432\u0435\u0440\u0445\u043D\u0438\u0439 \u043F\u0440\u0435\u0434\u0435\u043B \u0434\u043B\u044F \u0431\u0438\u0442\u0440\u044D\u0439\u0442\u0430 \u043C\u0443\u0437\u044B\u043A\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u043F\u043E\u0442\u043E\u043A\u0430. \
+ \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0435\u0441\u043B\u0438 \u0443 \u0432\u0430\u0441 mp3-\u0444\u0430\u0439\u043B\u044B \u043D\u0430\u0445\u043E\u0434\u044F\u0442\u0441\u044F \u0432 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435 256\u041A\u0431/\u0441, \u0430 \u0432\u044B \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B\u0438 128, \
+ \u0442\u043E {0} \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u0435\u0440\u0435\u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u043F\u043E\u0442\u043E\u043A \u0438\u0437 256 \u0432 128\u041A\u0431/\u0441.</p> \
+ <p>\u0414\u043B\u044F \u044D\u0442\u043E\u0439 \u043E\u043F\u0446\u0438\u0438 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0439 LAME. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \u044D\u0442\u043E \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u043A\u043E\u0434\u0438\u0440\u043E\u0432\u043A\u0438 \u0441 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u043C \u043A\u043E\u0434\u043E\u043C. \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">\u0441\u043A\u0430\u0447\u0430\u0442\u044C \u0435\u0451 \u0437\u0434\u0435\u0441\u044C</a>. \
+ \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 \u0435\u0451 \u0432 \u0434\u0438\u0440\u0435\u043A\u0442\u043E\u0440\u0438\u044E SUBSONIC_HOME/transcode, \u0438\u043B\u0438 \u0432 \u0434\u0438\u0440\u0435\u043A\u0442\u043E\u0440\u0438\u044E, \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u0443\u044E \u0432 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F PATH.</p>
+helppopup.playlistfolder.title = \u041F\u0430\u043F\u043A\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u043E\u0432
+helppopup.playlistfolder.text = <p>\u0423\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043F\u0430\u043F\u043A\u0443, \u0432 \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u0431\u0443\u0434\u0443\u0442 \u0445\u0440\u0430\u043D\u0438\u0442\u044C\u0441\u044F \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u044B.</p>
+helppopup.musicmask.title = \u041C\u0430\u0441\u043A\u0430 \u043C\u0443\u0437\u044B\u043A\u0438
+helppopup.musicmask.text = <p>\u041F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0432\u0430\u043C \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043C\u0430\u0441\u043A\u0443 \u0434\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u043D\u043D\u044B \u043A\u0430\u043A \u043C\u0443\u0437\u044B\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u0438 \u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0438 \u0434\u0438\u0440\u0435\u043A\u0442\u043E\u0440\u0438\u0439.</p>
+helppopup.coverartmask.title = \u041C\u0430\u0441\u043A\u0430 \u043E\u0431\u043B\u043E\u0436\u0435\u043A
+helppopup.coverartmask.text = <p>\u041F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043C\u0430\u0441\u043A\u0443 \u0434\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432 \u043E\u0431\u043B\u043E\u0436\u0435\u043A, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u043D\u043D\u044B \u043F\u0440\u0438 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435 \u043F\u0430\u043F\u043E\u043A \u0441 \u043C\u0443\u0437\u044B\u043A\u043E\u0439.</p>
+helppopup.downsamplecommand.title = \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u043F\u043E\u043D\u0438\u0436\u0435\u043D\u0438\u044F \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0430
+helppopup.downsamplecommand.text = <p>\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u0443, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0431\u0443\u0434\u0435\u0442 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0442\u044C\u0441\u044F \u043F\u0440\u0438 \u043F\u043E\u043D\u0438\u0436\u0435\u043D\u0438\u0438 \u0431\u0438\u0442\u0440\u044D\u0439\u0442\u0430.</p>\
+ <p>(%s = \u0424\u0430\u0439\u043B, %b = \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0431\u0438\u0442\u0440\u044D\u0439\u0442 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F)</p>
+helppopup.index.title = \u0418\u043D\u0434\u0435\u043A\u0441
+helppopup.index.text = <p>\u041F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C \u0432\u043D\u0435\u0448\u043D\u0438\u0439 \u0432\u0438\u0434 \u0438\u043D\u0434\u0435\u043A\u0441\u0430, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0432\u0432\u0435\u0440\u0445\u0443 \u0438 \u0432\u043D\u0438\u0437\u0443 \u043E\u0442 \u0441\u043F\u0438\u0441\u043A\u0430 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u0435\u0439. \u0424\u0430\u0439\u043B\u044B \u0438 \u043F\u0430\u043F\u043A\u0438 \
+ \u0432 \u043A\u043E\u0440\u043D\u0435\u0432\u043E\u0439 \u043F\u0430\u043F\u043A\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u043D\u0430\u043F\u0440\u044F\u043C\u0443\u044E \u043F\u0440\u0438 \u043F\u043E\u043C\u043E\u0449\u0438 \u044D\u0442\u043E\u0433\u043E \u0438\u043D\u0434\u0435\u043A\u0441\u0430.</p> \
+ <p>\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442\u0441\u044F \u0441\u043F\u0438\u0441\u043A\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432, \u0440\u0430\u0437\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0445 \u043F\u0440\u043E\u0431\u0435\u043B\u0430\u043C\u0438. \u041E\u0431\u044B\u0447\u043D\u043E \u043A\u0430\u0436\u0434\u044B\u0439 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u043E\u0431\u043E\u0439 \u043E\u0434\u0438\u043D \u0441\u0438\u043C\u0432\u043E\u043B, \
+ \u043D\u043E \u043C\u043E\u0436\u043D\u043E \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E. \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u044D\u043B\u0435\u043C\u0435\u043D\u0442 <em>The</em> \u0431\u0443\u0434\u0435\u0442 \u0441\u0432\u044F\u0437\u0430\u043D \u0441\u043E \u0432\u0441\u0435\u043C\u0438 \u0444\u0430\u0439\u043B\u0430\u043C\u0438 \u0438 \u043F\u0430\u043F\u043A\u0430\u043C\u0438, \
+ \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u044E\u0442\u0441\u044F \u0441 "The".</p> \
+ <p>\u0422\u0430\u043A \u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u043C \u043C\u043E\u0436\u0435\u0442 \u044F\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u0433\u0440\u0443\u043F\u043F\u0430 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432. \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u044D\u043B\u0435\u043C\u0435\u043D\u0442 \
+ <em>A-E(ABCDE)</em> \u0431\u0443\u0434\u0435\u0442 \u0432\u044B\u0433\u043B\u044F\u0434\u0435\u0442\u044C \u043A\u0430\u043A <em>A-E</em> \u0438 \u0441\u043E\u0435\u0434\u0438\u043D\u044F\u0435\u0442\u0441\u044F \u0441\u043E \u0432\u0441\u0435\u043C\u0438 \u0444\u0430\u0439\u043B\u0430\u043C\u0438 \u0438 \u043F\u0430\u043F\u043A\u0430\u043C\u0438, \u043D\u0430\u0447\u0438\u043D\u0430\u044E\u0449\u0438\u043C\u0438\u0441\u044F \u0441 \
+ A, B, C, D \u0438\u043B\u0438 E. \u042D\u0442\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u043E \u043F\u0440\u0438 \u0433\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0435 \u0440\u0435\u0434\u043A\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 (\u0442\u0430\u043A\u0438\u0445 \u043A\u0430\u043A X, Y \u0438\u043B\u0438 Z), \u0438\u043B\u0438 \
+ \u0434\u043B\u044F \u0433\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0438 \u0441\u0445\u043E\u0436\u0438\u0445 \u0437\u043D\u0430\u043A\u043E\u0432 (\u0442\u0430\u043A\u0438\u0445 \u043A\u0430\u043A A, \u00C0 \u0438 \u00C1)</p> \
+ <p>\u0412\u0441\u0451, \u0447\u0442\u043E \u043D\u0435 \u043F\u043E\u043F\u0430\u043B\u043E \u043D\u0438 \u0432 \u043E\u0434\u0438\u043D \u044D\u043B\u0435\u043C\u0435\u043D\u0442 \u0438\u043D\u0434\u0435\u043A\u0441\u0430, \u0440\u0430\u0441\u043F\u043E\u043B\u0430\u0433\u0430\u0435\u0442\u0441\u044F \u0432\u043D\u0438\u0437\u0443 \u0432 \u0431\u043B\u043E\u043A\u0435 "#".</p>
+helppopup.ignoredarticles.title = \u0418\u0433\u043D\u043E\u0440\u0438\u0440\u0443\u0435\u043C\u044B\u0435 \u0430\u0440\u0442\u0438\u043A\u043B\u0438
+helppopup.ignoredarticles.text = <p>\u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0430\u0440\u0442\u0438\u043A\u043B\u0438(\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "The"), \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u043F\u0440\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0438 \u0438\u043D\u0434\u0435\u043A\u0441\u0430.</p>
+helppopup.shortcuts.title = \u0421\u0441\u044B\u043B\u043A\u0438
+helppopup.shortcuts.text = <p>\u0421\u043F\u0438\u0441\u043E\u043A \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0445 \u043F\u0440\u043E\u0431\u0435\u043B\u0430\u043C\u0438 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0439 \u043A\u043E\u0440\u043D\u0435\u0432\u044B\u0445 \u0434\u0438\u0440\u0435\u043A\u0442\u043E\u0440\u0438\u0439, \u043D\u0430 \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043E\u0437\u0434\u0430\u043D\u043D\u044B \u044F\u0440\u043B\u044B\u043A\u0438.\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u043A\u043E\u0432\u044B\u0447\u043A\u0438 \u0434\u043B\u044F \u0433\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0438 \u0441\u043B\u043E\u0432 \u0432 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0438, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = \u042F\u0437\u044B\u043A
+helppopup.language.text = <p>\u041F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u044F\u0437\u044B\u043A \u0441\u0438\u0441\u0442\u0435\u043C\u044B.</p>
+helppopup.visibility.title = \u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435
+helppopup.visibility.text = <p>\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u043A\u0430\u044F \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F \u0431\u0443\u0434\u0435\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u043A\u0430\u0436\u0434\u043E\u0439 \u043F\u0435\u0441\u043D\u0438, \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043D\u044B\u0435 \u043A\u0430\u043A \u0438 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435. \u042D\u0442\u043E \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \
+ \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u043C\u044B\u0445 \u0432 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0438 \u0442\u0440\u0435\u043A\u0430, \u0430\u043B\u044C\u0431\u043E\u043C\u0430, \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044F.</p>
+helppopup.partymode.title = \u0423\u043F\u0440\u043E\u0449\u0435\u043D\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C
+helppopup.partymode.text = <p>\u041F\u0440\u0438 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0438 \u0443\u043F\u0440\u043E\u0449\u0435\u043D\u043D\u043E\u0433\u043E \u0440\u0435\u0436\u0438\u043C\u0430 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0441\u0438\u0441\u0442\u0435\u043C\u044B \u0443\u043F\u0440\u043E\u0449\u0430\u0435\u0442\u0441\u044F \u0434\u043B\u044F \u043D\u0435\u043E\u043F\u044B\u0442\u043D\u044B\u0445 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439. \
+ \u0412 \u0447\u0430\u0441\u0442\u043D\u043E\u0441\u0442\u0438, \u0438\u0441\u043A\u043B\u044E\u0447\u0430\u0435\u0442\u0441\u044F \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0432 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u044B\u0445 \u0442\u0440\u0435\u043A\u043E\u0432.</p>
+helppopup.theme.title = \u0422\u0435\u043C\u0430
+helppopup.theme.text = <p>\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u0430\u043A\u0443\u044E \u0442\u0435\u043C\u0443 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0434\u043B\u044F \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F \u0441\u0430\u0439\u0442\u0430 {0}. \u0412 \u0442\u0435\u043C\u0435 \u043C\u043E\u0433\u0443\u0442 \u0431\u044B\u0442\u044C \u0441\u0432\u043E\u0438 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438, \u0446\u0432\u0435\u0442\u0430, \u0448\u0440\u0438\u0444\u0442\u044B</p>
+helppopup.welcomemessage.title = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435
+helppopup.welcomemessage.text = <p>\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435.</p>
+helppopup.loginmessage.title = \u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u0432\u0445\u043E\u0434\u0435
+helppopup.loginmessage.text = <p>\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435, \u043A\u043E\u0442\u043E\u0440\u043E\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0432\u0445\u043E\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043C\u0443.</p>
+helppopup.coverartlimit.title = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E \u043E\u0431\u043B\u043E\u0436\u0435\u043A
+helppopup.coverartlimit.text = <p>\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043E\u0431\u043B\u043E\u0436\u0435\u043A \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435.</p>
+helppopup.downloadlimit.title = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F
+helppopup.downloadlimit.text = <p>\u0412\u0435\u0440\u0445\u043D\u0438\u0439 \u043F\u0440\u0435\u0434\u0435\u043B \u0434\u043B\u044F \u0441\u043A\u043E\u0440\u043E\u0441\u0442\u0438 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F \u0444\u0430\u0439\u043B\u043E\u0432.</p>
+helppopup.uploadlimit.title = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F
+helppopup.uploadlimit.text = <p>\u0412\u0435\u0440\u0445\u043D\u0438\u0439 \u043F\u0440\u0435\u0434\u0435\u043B \u0441\u043A\u043E\u0440\u043E\u0441\u0442\u0438 \u0437\u0430\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F \u0444\u0430\u0439\u043B\u043E\u0432.</p>
+helppopup.streamport.title = \u041F\u043E\u0440\u0442 \u0431\u0435\u0437 SSL
+helppopup.streamport.text = <p>\u041E\u043F\u0446\u0438\u044F \u043F\u043E\u043B\u0435\u0437\u043D\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0432\u044B \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0435 {0} \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0441 SSL (HTTPS).</p><p>\u041D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u0438 \
+ (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 Winamp) \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044E\u0442 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u0434\u0435\u043D\u0438\u0435 \u043F\u043E\u0442\u043E\u043A\u043E\u0432 \u0441 SSL. \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u043E\u043C\u0435\u0440 \u043F\u043E\u0440\u0442\u0430 \u0434\u043B\u044F http (\u043E\u0431\u044B\u0447\u043D\u043E 80 \
+ \u0438\u043B\u0438 4040) \u0435\u0441\u043B\u0438 \u0432\u044B \u043D\u0435 \u0445\u043E\u0442\u0438\u0442\u0435, \u0447\u0442\u043E \u0431\u044B \u043F\u043E\u0442\u043E\u043A\u0438 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u043B\u0438\u0441\u044C \u0447\u0435\u0440\u0435\u0437 \u043A\u0430\u043D\u0430\u043B \u0441 SSL. \u0412\u043D\u0438\u043C\u0430\u043D\u0438\u0435, \u0442\u0430\u043A\u0438\u0435 \u043F\u043E\u0442\u043E\u043A\u0438 \u043D\u0435 \u0431\u0443\u0434\u0443\u0442 \u0448\u0438\u0444\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F.</p>
+helppopup.ldap.title = \u0410\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u044F LDAP
+helppopup.ldap.text = <p>\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u043C\u043E\u0433\u0443\u0442 \u0431\u044B\u0442\u044C \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C LDAP (\u0432\u043A\u043B\u044E\u0447\u0430\u044F Windows Active Directory). \
+ \u0415\u0441\u043B\u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 LDAP \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0432 {0}, \u0442\u043E \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0438 \u043F\u0430\u0440\u043E\u043B\u044C \u0443\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u044E\u0442\u0441\u044F \u0432\u043D\u0435\u0448\u043D\u0438\u043C LDAP-\u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C, \u0430 \u043D\u0435 {0} \u0438\u043B\u0438 \u0441\u0430\u043C\u043E\u0441\u0442\u043E\u044F\u0442\u0435\u043B\u044C\u043D\u043E.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>URL \u0441\u0435\u0440\u0432\u0435\u0440\u0430 LDAP. \u041F\u0440\u043E\u0442\u043E\u043A\u043E\u043B \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C <em>ldap://</em> \u0438\u043B\u0438 <em>ldaps://</em> \
+ (\u0434\u043B\u044F LDAP \u0447\u0435\u0440\u0435\u0437 SSL). \u0421\u043C\u043E\u0442\u0440\u0438\u0442\u0435 <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">\u0437\u0434\u0435\u0441\u044C</a> \
+ \u0434\u0435\u0442\u0430\u043B\u044C\u043D\u0443\u044E \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044E.</p>
+helppopup.ldapsearchfilter.title = \u041F\u043E\u0438\u0441\u043A\u043E\u0432\u043E\u0439 \u0444\u0438\u043B\u044C\u0442\u0440 LDAP
+helppopup.ldapsearchfilter.text = <p>\u0424\u0438\u043B\u044C\u0442\u0440 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u043F\u043E\u0438\u0441\u043A\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439. \u042D\u0442\u043E \u043F\u043E\u0438\u0441\u043A\u043E\u0432\u043E\u0439 \u0444\u0438\u043B\u044C\u0442\u0440 LDAP \
+ (\u0441\u043F\u0435\u0446\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F: <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ \u0428\u0430\u0431\u043B\u043E\u043D "'{0'}" \u0437\u0430\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u0438\u043C\u0435\u043D\u0435\u043C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \
+ <ul>\
+ <li>(uid='{0'}) - \u043D\u0430\u0439\u0434\u0435\u0442 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439, \u0438\u043C\u044F \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0443\u0434\u043E\u0432\u043B\u0435\u0442\u0432\u043E\u0440\u044F\u0435\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0443 uid.</li> \
+ <li>(sAMAccountName='{0'}) - \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440 DN
+helppopup.ldapmanagerdn.text = <p>\u0415\u0441\u043B\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 LDAP \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0443 \u0430\u043D\u043E\u043D\u0438\u043C\u043D\u044B\u0445 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439, \u0432\u044B \u0434\u043E\u043B\u0436\u043D\u044B \u0443\u043A\u0430\u0437\u0430\u0442\u044C DN \
+ (<em>Distinguished Name</em>) \u0438 \u043F\u0430\u0440\u043E\u043B\u044C LDAP-\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438.</p>
+helppopup.ldapautoshadowing.title = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C LDAP \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 \u0432 {0}
+helppopup.ldapautoshadowing.text = <p>\u0421 \u044D\u0442\u043E\u0439 \u043E\u043F\u0446\u0438\u0435\u0439, LDAP-\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0434 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432 \u0440\u0443\u0447\u043D\u0443\u044E \u043D\u0430 {0}.</p> \
+ <p>\u0412\u043D\u0438\u043C\u0430\u043D\u0438\u0435! \u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u0441 \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u043C LDAP \u0438\u043C\u0435\u043D\u0435\u043C \u0438 \u043F\u0430\u0440\u043E\u043B\u0435\u043C \u043C\u043E\u0433\u0443\u0442 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0432 {0}, \
+ \u044D\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u044F\u0432\u043B\u044F\u0442\u0441\u044F \u043D\u0435 \u0442\u0435\u043C, \u0447\u0442\u043E \u0432\u044B \u0445\u043E\u0442\u0438\u0442\u0435.</p>
+helppopup.playername.title = \u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F
+helppopup.playername.text = <p>\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044F, \u043A\u043E\u0442\u043E\u0440\u043E\u0435 \u043C\u043E\u0436\u043D\u043E \u043B\u0435\u0433\u043A\u043E \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C.</p>
+helppopup.autocontrol.title = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u0435\u043C
+helppopup.autocontrol.text = <p>\u0421 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043D\u043E\u0439 \u043E\u043F\u0446\u0438\u0435\u0439 {0} \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C \u043A\u043E\u0433\u0434\u0430 \u0432\u044B \u043D\u0430\u0436\u043C\u0435\u0442\u0435 \u043A\u043D\u043E\u043F\u043A\u0443 "\u0412\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u0441\u0442\u0438"\
+ \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u0434\u0435\u043D\u0438\u044F. \u0418\u043D\u0430\u0447\u0435 \u0432\u044B \u0441\u0430\u043C\u0438 \u0434\u043E\u043B\u0436\u043D\u044B \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0438 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C.</p>
+helppopup.dynamicip.title = \u0414\u0438\u043D\u0430\u043C\u0438\u0447\u0435\u0441\u043A\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441
+helppopup.dynamicip.text = <p>\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u044D\u0442\u0443 \u043E\u043F\u0446\u0438\u044E, \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u0442\u0435\u043B\u044C \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441.</p>
+
+# wap/index.jsp
+wap.index.missing = \u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u043C\u0443\u0437\u044B\u043A\u0430
+wap.index.playlist = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+wap.index.search = \u0418\u0441\u043A\u0430\u0442\u044C
+wap.index.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+
+# wap/browse.jsp
+wap.browse.playone = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u0442\u044C
+wap.browse.playall = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u0442\u044C \u0432\u0441\u0435
+wap.browse.addone = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C
+wap.browse.addall = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0432\u0441\u0435
+wap.browse.downloadone = \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u043F\u0435\u0441\u043D\u044E
+wap.browse.downloadall = \u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0432\u0441\u0451
+
+# wap/playlist.jsp
+wap.playlist.title = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442
+wap.playlist.noplayer = \u041D\u0435\u0442\u0443 \u043F\u043B\u0435\u0435\u0440\u043E\u0432
+wap.playlist.clear = \u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C
+wap.playlist.load = \u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C
+wap.playlist.random = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u043E
+wap.playlist.play = \u041F\u0440\u043E\u0438\u0433\u0440\u0430\u0442\u044C \u043D\u0430 \u0442\u0435\u043B\u0435\u0444\u043E\u043D\u0435
+
+# wap/search.jsp
+wap.search.title = \u0438\u0441\u043A\u0430\u0442\u044C
+
+# wap/searchResult.jsp
+wap.searchresult.index = \u0421\u0435\u0439\u0447\u0430\u0441 \u0438\u0434\u0435\u0442 \u0438\u043D\u0434\u0435\u043A\u0441\u0430\u0446\u0438\u044F, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0434\u0436\u0435.
+
+# wap/settings.jsp
+wap.settings.selectplayer = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u043F\u043B\u0435\u0435\u0440
+wap.settings.allplayers = \u0412\u0441\u0435 \ No newline at end of file
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sl.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sl.properties
new file mode 100644
index 00000000..f0c623f3
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sl.properties
@@ -0,0 +1,785 @@
+#
+# Slovenian (sl) localization.
+#
+# Authors: Jan Jamsek and Marko Kastelic
+# Based on the original translation by Andrej Zizmond
+#
+
+common.home = Domov
+common.back = Nazaj
+common.help = Pomo\u010d
+common.play = Predvajaj
+common.add = Dodaj
+common.download = Prenesi
+common.close = Zapri
+common.refresh = Osve\u017ei
+common.next = Naslednji
+common.previous = Prej\u0161nji
+common.more = Dodatno
+common.ok = V redu
+common.cancel = Prekli\u010di
+common.save = Shrani
+common.create = Ustvari
+common.delete = Izbri\u0161i
+common.edit = Urejanje
+common.confirm = Potrditev
+common.unknown = (Neznano)
+common.default = (Privzeto)
+
+# login.jsp
+login.username = Uporabni\u0161ko ime:
+login.password = Geslo:
+login.login = Prijava
+login.remember = Zapomni si me
+login.logout = Sedaj ste odjavljeni.
+login.error = Napa\u010dno uporabni\u0161ko ime ali geslo.
+login.insecure = {0} ni zavarovan. Prosimo, prijavite se z uporabni\u0161kim imenom in<br>geslom "admin", oziroma kliknite <a href="login.view?user=admin&amp;password=admin">tukaj</a>. Potem nemudoma spremenite geslo.
+login.recover = Ste pozabili geslo?
+
+# recover.jsp
+recover.title = Ste pozabili geslo?
+recover.text = \u010ce \u017eelite ponastaviti geslo, prosimo vnesite va\u0161e <b>uporabni\u0161ko ime</b> ali <b>e-po\u0161tni naslov</b> v spodnje polje.
+recover.username = Uporabni\u0161ko ime ali e-po\u0161tni naslov
+recover.send = Ponastavi geslo
+recover.success = Va\u0161e geslo je ponastavljeno in poslano na {0}.
+recover.error.usernotfound = Oprostite, uporabnik ne obstaja.
+recover.error.noemail = Oprostite, uporabnik nima dolo\u010denega e-po\u0161tnega naslova.
+recover.error.sendfailed = Napaka pri po\u0161iljanju e-po\u0161te, prosimo poskusite kasneje.
+
+# accessDenied.jsp
+accessDenied.title = Dostop zavrnjen
+accessDenied.text = Oprosite, vendar nimate dovoljenja, da bi izvedli \u017eeleno zahtevo.
+
+# notFound.jsp
+notFound.title = Ni zadetkov
+notFound.text = <p>Oprostite, va\u0161a iskalna zahteva ni bila najdena.</p><p>Posku\u0161ajte ponovno nalo\u017eiti spletno stran. \u010ce to ne pomaga, \
+ posku\u0161ajte ponovno indeksirati imenike z medijsko vsebino.</p>
+notFound.reload = Ponovno nalo\u017ei stran
+notFound.scan = Nastavitve imenika z medijsko vsebino
+
+# top.jsp
+top.home = Domov
+top.now_playing = Predvaja se
+top.starred = Ozna\u010deno z zvezdico
+top.settings = Nastavitve
+top.status = Stanje
+top.podcast = Podcast
+top.more = Dodatno
+top.help = O Subsonicu
+top.search = Iskanje
+top.upgrade = <b>Pozor!</b> Na voljo je nova razli\u010dica.<br>Prenesite {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">tukaj</a>.
+top.missing = Ne najdem imenikov z glasbo. Prosimo, popravite nastavitve.
+top.logout = Odjava: {0}
+
+# left.jsp
+left.scanning = Pregledovanje imenikov z medijsko vsebino...
+left.statistics = {0}&nbsp;izvajalcev<br>\
+ {1}&nbsp;albumov<br>\
+ {2}&nbsp;skladb<br>\
+ {3}<br>\
+ {4}&nbsp;ur
+left.shortcut = Bli\u017enjice
+left.playlists = Seznami predvajanja
+left.radio = Spletni TV/radio
+left.allfolders = Vsi imeniki
+left.createplaylist = Ustvari nov seznam predvajnja
+left.importplaylist = Uvozi seznam predvajnja
+
+# playQueue.jsp
+playlist.stop = Ustavi
+playlist.start = Predvajaj
+playlist.confirmclear = \u017delite resni\u010dno zbrisati vrstni red predvajanja?
+playlist.clear = Zbri\u0161i
+playlist.shuffle = Naklju\u010dno
+playlist.repeat_on = Ponavljanje je vklopljeno
+playlist.repeat_off = Ponavljanje je izklopljeno
+playlist.undo = Razveljavi
+playlist.settings = Nastavitve
+playlist.more = Ve\u010d dejanj...
+playlist.more.playlist = Vrstni red predvajanja
+playlist.more.sortbytrack = Razvrsti po skladbah
+playlist.more.sortbyartist = Razvrsti po izvajalcih
+playlist.more.sortbyalbum = Razvrsti po albumih
+playlist.more.selection = Izbrane pesmi
+playlist.more.selectall = Izberi vse
+playlist.more.selectnone = Ne izberi ni\u010desar
+playlist.getflash = Prenesi Flash predvajalnik
+playlist.save = Shrani kot seznam predvajanja
+playlist.append = Dodaj k seznamu predvajanja
+playlist.remove = Odstrani
+playlist.up = Gor
+playlist.down = Dol
+playlist.empty = Vrstni red predvajanja je prazen
+
+# playlist.jsp
+playlist2.created = Ustvaril {0} dne {1}
+playlist2.name = Ime seznama predvajanja
+playlist2.comment = Komentar k seznamu predvajanja
+playlist2.public = Daj ta seznam predvajanja v skupno rabo.
+playlist2.confirmdelete = \u017delite resni\u010dno zbrisati ta seznam predvajanja?
+playlist2.empty = Seznam predvajanja je prazen
+playlist2.export = Izvozi
+
+# importPlaylist.jsp
+importPlaylist.title = Uvozi seznam predvajanja
+importPlaylist.text = Izberite seznam predvajanja za uvoz (m3u, pls, xspf)
+importPlaylist.success = Seznam predvajanja "{0}" je bil uspe\u0161no uvo\u017een.
+importPlaylist.error = Napaka pri uva\u017eanju seznama predvajanja. {0}
+
+# videoPlayer.jsp
+videoPlayer.getflash = Prosimo, namestite si Flash predvajalnik
+videoPlayer.popout = Odpri v novem oknu
+
+# status.jsp
+status.title = Stanje
+status.type = Vrsta
+status.stream = Preto\u010dna oblika
+status.download = Prena\u0161anje
+status.upload = Nalaganje
+status.player = Predvajalnik
+status.user = Uporabnik
+status.current = Trenutna datoteka
+status.transmitted = Preneseno
+status.bitrate = Bitna hitrost (Kbps)
+
+# starred.jsp
+starred.title = Ozna\u010deno z zvezdico
+starred.empty = Kliknite na zvezdico, da ozna\u010dite va\u0161e najbolj priljubljene izvajalce, albume in skladbe.
+
+# search.jsp
+search.title = Iskanje
+search.query = Izvajalec, album ali naslov skladbe:
+search.search = I\u0161\u010di
+search.index = Iskalni indeks je trenutno v izdelovanju. Prosimo, poskusite kasneje.
+search.hits.none = Ni zadetkov.
+search.hits.more = Ve\u010d
+search.hits.artists = Izvajalci
+search.hits.albums = Albumi
+search.hits.songs = Skladbe
+
+# gettingStarted.jsp
+gettingStarted.title = Uvod
+gettingStarted.text = <p>Dobrodo\u0161li v Subsonicu! Namestitev bo hitra, le spodnjim korakom morate slediti.<br> \
+ Kliknite gumb "Domov" v orodni vrstici za vrnitev v to okno.</p> \
+ <p>Za ve\u010d informacij si poglejte <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Uvod</b></a>.</p>
+gettingStarted.root = Pozor! Program Subsonic se izvaja kot root uporabnik. Prosimo premislite, \u010de \u017eelite \
+ <a href="http://subsonic.org/pages/installation.jsp" target="_blank">to spremeniti.</a>
+gettingStarted.step1.title = Sprememba administratorskega gesla.
+gettingStarted.step1.text = Zavarujte va\u0161 stre\u017enik tako, da spremenite privzeto administratorsko geslo. \
+ Ustvarite lahko tudi nove uporabni\u0161ke ra\u010dune z razli\u010dnimi pravicami.
+gettingStarted.step2.title = Dolo\u010dite imenike z glasbo.
+gettingStarted.step2.text = Povejte Subsonicu, kje hranite va\u0161e glasbene datoteke.
+gettingStarted.step3.title = Nastavi omre\u017ene nastavitve.
+gettingStarted.step3.text = Nekaj uporabnih nastavitev, \u010de \u017eelite poslu\u0161ati va\u0161o glasbo preko spleta, \
+ oz. jo deliti z dru\u017eino in/ali prijatelji. Priskrbite si svoj osebni <b><em>va\u0161e_ime</em>.subsonic.org</b> \
+ spletni naslov.
+gettingStarted.hide = Ne prikazuj ve\u010d teh navodil.
+gettingStarted.hidealert = \u010ce \u017eelite ponovno prebrati ta navodila, poglejte v Nastavitve > Splo\u0161no.
+
+# home.jsp
+home.random.title = Naklju\u010dno
+home.alphabetical.title = Vse
+home.newest.title = Najnovej\u0161e
+home.starred.title = Ozna\u010deno z zvezdico
+home.highest.title = Najbolje ocenjeno
+home.frequent.title = Najpogosteje predvajano
+home.recent.title = Nazadnje predvajano
+home.users.title = Uporabniki
+home.random.text = Naklju\u010dno izbrani albumi
+home.alphabetical.text = Vsi albumi
+home.newest.text = Nazadnje dodani albumi
+home.starred.text = Albumi ozna\u010deni z zvezdico
+home.highest.text = Najbolje ocenjeni albumi
+home.frequent.text = Najpogosteje predvajani albumi
+home.recent.text = Nazadnje predvajani albumi
+home.users.text = Statistika uporabnikov
+home.scan = Pregledovanje imenikov z medijskimi vsebinami, zato vse mo\u017enosti niso na voljo.
+home.listsize = {0} albumov na stran
+home.albums = Albumi {0} - {1}
+home.playcount = \u0160tevilo predvajanih skladb: {0}
+home.lastplayed = Nazadnje predvajano: {0}
+home.created = Ustvarjeno {0}
+home.chart.total = Skupaj (MB)
+home.chart.stream = Preto\u010deno (MB)
+home.chart.download = Preneseno s stre\u017enika (MB)
+home.chart.upload = Nalo\u017eeno na stre\u017enik (MB)
+
+# more.jsp
+more.title = Dodatno
+more.random.title = Naklju\u010dni seznam predvajanja
+more.random.text = Ustvari naklju\u010dni seznam predvajanja:
+more.random.songs = {0} skladb
+more.random.auto = Predvajaj ve\u010d naklju\u010dnih pesmi po koncu seznama predvajanja.
+more.random.ok = V redu
+more.random.genre = zvrst
+more.random.anygenre = katerakoli
+more.random.year = leto
+more.random.anyyear = katerokoli
+more.random.folder = imenik
+more.random.anyfolder = katerikoli
+more.apps.title = Subsonic aplikacije
+more.apps.text = <p>Preverite vedno ve\u010dji seznam <a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic aplikacij</a>. \
+ Te aplikacije vam omogo\u010dajo nove in bolj zabavne na\u010dine dostopa do va\u0161e medijske zbirke \u2013 ne glede na to kje se nahajate. \
+ Aplikacije so na voljo za Android, iPhone, Windows Phone, PlayBook, Roku in mnoge druge naprave.</p>
+more.mobile.title = Mobilni telefon
+more.mobile.text = <p>Nadzor {0} je mogo\u010d s kateregakoli WAP mobilnega telefona ali dlan\u010dnika.<br> \
+ Preprosto obi\u0161\u010dite naslednji spletni naslov na va\u0161em telefonu: <b>http://vas.streznik/wap</b></p> \
+ <p>to seveda zahteva, da je va\u0161 stre\u017enik dostopen preko spleta.</p>
+more.podcast.title = Podcasti
+more.podcast.text = <p>Shranjeni seznami predvajanja so dostopni kot podcasti.<br>\
+ Vnesite naslednji spletni naslov v va\u0161 podcast odjemalec: <b>http://vas.streznik/podcast</b>, \
+ oz. <b><a href="podcast.view?suffix=.rss">kliknite tukaj</a>.</b></p>
+more.upload.title = Nalo\u017ei datoteko na stre\u017enik
+more.upload.source = Izberi datoteko
+more.upload.target = Shrani v
+more.upload.browse = Izberi
+more.upload.ok = Nalo\u017ei
+more.upload.unzip = Avtomatsko razpakiraj ZIP datoteko.
+more.upload.progress = Dokon\u010dano: %. Prosimo, po\u010dakajte...
+
+# upload.jsp
+upload.title = Nalaganje datoteke
+upload.success = Uspe\u0161no nalo\u017eeno <b>{0}</b>
+upload.empty = Ni datotek za nalaganje.
+upload.failed = Nalagnje ni uspelo; napaka:<br><b>"{0}"</b>
+upload.unzipped = Razpakirano: {0}
+
+# help.jsp
+help.title = O programu {0}
+help.upgrade = <b>Pozor!</b> Na vojo je nova razli\u010dica. Prenesite {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">tukaj</a>.
+help.version.title = Razli\u010dica
+help.builddate.title = Izdelana dne
+help.server.title = Stre\u017enik
+help.license.title = Licenca
+help.license.text = {0} je zastonj program na voljo pod <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> odprtokodno licenco. \
+ {0} uporablja <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licencirane knji\u017eice tretjih oseb</a>. {0} <em>ni</em> \
+ namenjen razpe\u010devanju ilegalnega in piratskega materiala. Vedno upo\u0161tevajte zakone, ki veljajo v va\u0161i dr\u017eavi.
+help.homepage.title = Spletna stran
+help.forum.title = Forum
+help.shop.title = Prodaja
+help.contact.title = Kontakt
+help.contact.text = {0} je razvil in vzdr\u017euje Sindre Mehus \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ \u010ce imate vpra\u0161anja, pripombe ali predloge za izbolj\u0161ave, obi\u0161\u010dite \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonicov forum</a>.
+help.donate = {0} je brezpla\u010den, vendar lahko prispevate k projektu <b><a href="donate.view?">z donacijo</a></b>.
+help.log = Dnevnik
+help.logfile = Celotna datoteka z dnevnikom je shranjena v {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Nastavitve
+settingsheader.general = Splo\u0161no
+settingsheader.advanced = Napredno
+settingsheader.personal = Osebno
+settingsheader.musicFolder = Imeniki z glasbo
+settingsheader.internetRadio = Spletni TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Predvajalniki
+settingsheader.share = Skupna raba vsebin
+settingsheader.network = Omre\u017eje
+settingsheader.transcoding = Prekodiranje
+settingsheader.user = Uporabniki
+settingsheader.search = Iskanje
+settingsheader.coverArt = Slike ovitkov
+settingsheader.password = Geslo
+
+# generalSettings.jsp
+generalsettings.musicmask = Glasbene datoteke
+generalsettings.videomask = Video datoteke
+generalsettings.coverartmask = Prikazane slike ovitkov
+generalsettings.index = Indeks
+generalsettings.ignoredarticles = Ignoriraj predpone
+generalsettings.shortcuts = Bli\u017enjice
+generalsettings.sortalbumsbyyear = Razvrsti albume po letnicah
+generalsettings.showgettingstarted = Poka\u017ei "Uvod" ob zagonu
+generalsettings.welcometitle = Naslov dobrodo\u0161lice
+generalsettings.welcomesubtitle = Podnaslov dobrodo\u0161lice
+generalsettings.welcomemessage = Vsebina dobrodo\u0161lice
+generalsettings.loginmessage = Sporo\u010dilo ob prijavi
+generalsettings.language = Privzeti jezik
+generalsettings.theme = Privzeta tema
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Ukaz za zmanj\u0161anje velikosti
+advancedsettings.coverartlimit = Omejitev za slike ovitkov<br><div class="detail">(0 = neomejeno)</div>
+advancedsettings.downloadlimit = Omejitev za prena\u0161anje (Kbps)<br><div class="detail">(0 = neomejeno)</div>
+advancedsettings.uploadlimit = Omejitev za nalaganje (Kbps)<br><div class="detail">(0 = neomejeno)</div>
+advancedsettings.streamport = Vrata ne-SSL podatkovnega toka<br><div class="detail">(0 = onemogo\u010den)</div>
+advancedsettings.ldapenabled = Omogo\u010di LDAP preverjanje pristnosti
+advancedsettings.ldapurl = LDAP URL naslov
+advancedsettings.ldapsearchfilter = LDAP iskalni filter
+advancedsettings.ldapmanagerdn = LDAP upravitelj DN<br><div class="detail">(neobvezno)</div>
+advancedsettings.ldapmanagerpassword = Geslo
+advancedsettings.ldapautoshadowing = Samodejno ustvari uporabnike v {0}
+
+# personalSettings.jsp
+personalsettings.title = Osebne nastavitve za {0}
+personalsettings.language = Jezik
+personalsettings.theme = Tema
+personalsettings.display = Prikaz
+personalsettings.browse = Brskanje
+personalsettings.playlist = Seznam predvajanja
+personalsettings.tracknumber = Skladba #
+personalsettings.artist = Izvajalec
+personalsettings.album = Album
+personalsettings.genre = Zvrst
+personalsettings.year = Leto
+personalsettings.bitrate = Bitna hitrost
+personalsettings.duration = Trajanje
+personalsettings.format = Oblika
+personalsettings.filesize = Velikost datoteke
+personalsettings.captioncutoff = \u0160tevilo prikazanih znakov
+personalsettings.partymode = Na\u010din "zabava"
+personalsettings.shownowplaying = Poka\u017ei, kaj poslu\u0161ajo drugi
+personalsettings.nowplayingallowed = Naj drugi vidijo, kaj poslu\u0161am
+personalsettings.showchat = Poka\u017ei sporo\u010dila iz klepeta
+personalsettings.finalversionnotification = Obvesti me o novih razli\u010dicah
+personalsettings.betaversionnotification = Obvesti me o novih beta razli\u010dicah
+personalsettings.lastfmenabled = Zabele\u017ei, kaj poslu\u0161am, na <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm uporabni\u0161ko ime
+personalsettings.lastfmpassword = Last.fm geslo
+personalsettings.avatar.title = Osebna slika
+personalsettings.avatar.none = Brez slike
+personalsettings.avatar.custom = Slika po meri
+personalsettings.avatar.changecustom = Spremeni sliko po meri
+personalsettings.avatar.upload = Nalo\u017ei
+personalsettings.avatar.courtesy = Zahvala za ikone gre naslednjim spletnim stranem: <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, in \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Spremeni osebno sliko
+avataruploadresult.success = Uspe\u0161no nalo\u017eena osebna slika "{0}".
+avataruploadresult.failure = Nalaganje slike ni uspelo. Poglejte <a href="help.view?">dnevni\u0161ko datoteko</a> za podrobnosti.
+
+# passwordSettings.jsp
+passwordsettings.title = Spremeni geslo za {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = Imenik
+musicfoldersettings.name = Ime
+musicfoldersettings.enabled = Omogo\u010deno
+musicfoldersettings.add = Dodaj imenik z glasbo
+musicfoldersettings.nopath = Prosimo, vpi\u0161ite ime imenika.
+musicfoldersettings.notfound = Imenik ni bil najden
+musicfoldersettings.scan = Preglej medijske imenike
+musicfoldersettings.interval.never = Nikoli
+musicfoldersettings.interval.one = Vsak dan
+musicfoldersettings.interval.many = Vsake/-ih {0} dni
+musicfoldersettings.hour = ob {0}:00
+musicfoldersettings.nowscanning = Pregledovanje medijskih imenikov je v poteku. To lahko traja nekaj minut, odvisno od \
+ velikosti va\u0161e medijske zbirke.
+musicfoldersettings.scannow = Preglej medijske imenike zdaj
+musicfoldersettings.fastcache = Mo\u017enost hitrega dostopa
+musicfoldersettings.fastcache.description = Uporabite to mo\u017enost za zmanj\u0161anje dostopa do diska, na primer, \u010de se va\u0161e medijske datoteke nahajajo na mre\u017enem pogonu. \
+ Opomba: Spremembe na datotekah bodo vidne le, ko bodo vsi va\u0161i medijski imeniki pregledani.
+
+musicfoldersettings.organizebyfolderstructure = Organiziraj po imeni\u0161ki strukuri
+musicfoldersettings.organizebyfolderstructure.description = Uporabite to mo\u017enost za brskanje po va\u0161ih medijskih datotekah s pomo\u010djo imeni\u0161ke strukture, namesto uporabe podatkov o izvajalcih/albumih iz ID3 zna\u010dk.
+
+# networkSettings.jsp
+networksettings.text = Uporabi spodnje nastavitve za nadzor dostopa do va\u0161ega Subsonic stre\u017enika na Internetu.<br> \
+ \u010ce naletite na te\u017eave, si prosimo oglejte <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Uvod</b></a> k programu.
+networksettings.portforwardingenabled = Samodejno nastavi usmerjevalnik, da omogo\u010di povezavo do Subsonic stre\u017enika (z uporabo UPnP ali NAT-PMP posredovanja vrat).
+networksettings.portforwardinghelp = \u010ce va\u0161ega usmerjevalnika ni mo\u010d samodejno nastaviti, ga lahko nastavite ro\u010dno. \
+ Sledite navodilom na <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Posredovati morate vrata {0} do ra\u010dunalnika s Subsonic stre\u017enikom.
+networksettings.urlredirectionenabled = Dostopajte do svojega stre\u017enika preko interneta z uporabo naslova, ki si ga je lahko zapomniti.
+networksettings.status = Stanje:
+networksettings.trialexpired = Preizkusno obdobje je poteklo {0}. Prosimo <b><a href="donate.view?">prispevajte</a></b>, \u010de \u017eelite omogo\u010diti to mo\u017enost za stalno.
+networksettings.trialnotexpired = Ta mo\u017enost je na vojo do {0}. Po tem datumu morate <b><a href="donate.view?">prispevati</a></b>, \u010de jo \u017eelite stalno uporabljati.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Ime
+transcodingsettings.sourceformat = Pretvori iz
+transcodingsettings.targetformat = Pretvori v
+transcodingsettings.step1 = 1. korak
+transcodingsettings.step2 = 2. korak
+transcodingsettings.step3 = 3. korak
+transcodingsettings.defaultactive = Privzeto
+transcodingsettings.enabled = Omogo\u010deno
+transcodingsettings.add = Dodaj prekodiranje
+transcodingsettings.recommended = Priporo\u010dene nastavitve
+transcodingsettings.noname = Prosimo, dolo\u010dite ime.
+transcodingsettings.nosourceformat = Prosimo, dolo\u010dite obliko zapisa vhodne datoteke.
+transcodingsettings.notargetformat = Prosimo, dolo\u010dite obliko zapisa izhodne datoteke.
+transcodingsettings.nostep1 = Prosimo, dolo\u010dite vsaj en korak za prekodiranje.
+transcodingsettings.info = <p class="detail">(%s = Ime datoteke za prekodiranje, %b = Najve\u010dja bitna hitrost predvajalnika, %t = Naslov, %a = Izvajalec, %l = Album)</p> \
+ <p>Prekodiranje je proces pretvarjanja iz ene oblike zapisa ve\u010dpredstavnostne datoteke v drugo. Program {1} tako omogo\u010da \
+ pretakanje ve\u010dpredstavnostnih datotek, ki sicer obi\u010dajno niso preto\u010dne. Prekodiranje se izvaja v realnem \u010dasu \
+ in ne zahteva dodatnega prostora na disku.<p/> \
+ <p>Dejansko prekodiranje izvajajo drugi CLI programi, ki morajo biti name\u0161\u010deni v {0}. \
+ Dodate seveda lahko tudi poljuben program za prekodiranje, ki pa mora izpolnjevati naslednje zahteve: \
+ <ul> \
+ <li>imeti mora vmesnik za uporabo v ukazni vrstici,</li> \
+ <li>program mora znati posredovati svoje rezultate v "stdout",</li> \
+ <li>\u010de se uporablja v 2. ali 3. koraku prekodiranja, mora znati tudi brati podatke iz "stdin".</li> \
+ </ul> \
+ </p> \
+ <p> Vzemite na znanje, da se prekodiranje vklju\u010di ob zagonu posameznega predvajalnika iz <b>Nastavitve &gt; Predvajalniki</b>.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = URL naslov
+internetradiosettings.homepageurl = Doma\u010da stran
+internetradiosettings.name = Ime postaje
+internetradiosettings.enabled = Omogo\u010deno
+internetradiosettings.add = Dodaj spletni TV/radio
+internetradiosettings.nourl = Prosimo, navedite URL naslov.
+internetradiosettings.noname = Prosimo, navedite ime postaje.
+
+# podcastSettings.jsp
+podcastsettings.update = Preveri za nove epizode
+podcastsettings.keep = Obdr\u017ei
+podcastsettings.keep.all = Vse epizode
+podcastsettings.keep.one = Zadnjo epizodo
+podcastsettings.keep.many = Zadnjih {0} epizod
+podcastsettings.download = Kadar so na voljo nove epizode:
+podcastsettings.download.all = Prenesi vse
+podcastsettings.download.one = Prenesi najnovej\u0161o
+podcastsettings.download.many = Prenesi zadnjih {0} epizod
+podcastsettings.download.none = Ne stori ni\u010desar
+podcastsettings.interval.manually = Ro\u010dno
+podcastsettings.interval.hourly = Vsako uro
+podcastsettings.interval.daily = Vsak dan
+podcastsettings.interval.weekly = Vsak teden
+podcastsettings.folder = Shrani podcaste v imenik
+
+# playerSettings.jsp
+playersettings.noplayers = Ne najdem nobenega predvajalnika.
+playersettings.type = Vrsta
+playersettings.lastseen = Zadnji obisk
+playersettings.title = Izberi predvajalnik
+playersettings.technology.web.title = Spletni predvajalnik
+playersettings.technology.external.title = Zunanji predvajalnik
+playersettings.technology.external_with_playlist.title = Zunanji predvajalnik s seznami predvajanja
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Predvajanje glasbe neposredno v spletnem brskalniku z uporabo integriranega Flash predvajalnika.
+playersettings.technology.external.text = Predvajanje glasbe z va\u0161im najljub\u0161im zunanjim predvajalnikom, kot je npr. WinAmp ali Windows Media Player.
+playersettings.technology.external_with_playlist.text = Isto kot zgoraj, le da zunanji predvajalnik ureja tudi sezname predvajanja in le-teh ne ureja \
+ Subsonic stre\u017enik. V tem na\u010dinu je mogo\u010de "preskakovanje" skladb.
+playersettings.technology.jukebox.text = Predvajanje glasbe neposredno s pomo\u010djo zvo\u010dne naprave Subsonic stre\u017enika. (Uporaba je dovoljena le avtoriziranim uporbanikom).
+playersettings.name = Ime predvajalnika
+playersettings.coverartsize = Velikost slik ovitkov
+playersettings.maxbitrate = Najve\u010dja bitna hitrost
+playersettings.coverart.off = Izklop
+playersettings.coverart.small = Majhno
+playersettings.coverart.medium = Srednje
+playersettings.coverart.large = Veliko
+playersettings.nolame = <em>Obvestilo:</em> Izgleda, da LAME ni name\u0161\u010den.<br>Kliknite na gumb Pomo\u010d za ve\u010d informacij.
+playersettings.autocontrol = Samodejni nadzor predvajalnika
+playersettings.dynamicip = Predvajalnik ima dinami\u010den IP naslov
+playersettings.transcodings = Aktivno prekodiranje
+playersettings.ok = Shrani
+playersettings.forget = Izbri\u0161i predvajalnik
+playersettings.clone = Podvoji (kloniraj) predvajalnik
+
+# shareSettings.jsp
+sharesettings.name = Ime
+sharesettings.owner = Skupno uporabo omogo\u010dil
+sharesettings.description = Opis
+sharesettings.visits = Obiski
+sharesettings.lastvisited = Nazadnje obiskano
+sharesettings.expires = Pote\u010de
+sharesettings.files = Datoteke v skupni rabi
+sharesettings.expirein = Pote\u010de v
+sharesettings.expirein.week = 1t
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1l
+sharesettings.expirein.never = nikoli
+
+# userSettings.jsp
+usersettings.title = Izberi uporabnika
+usersettings.newuser = Nov uporabnik
+usersettings.admin = Uporabnik je administrator
+usersettings.settings = Uporabnik lahko spreminja nastavitve in geslo
+usersettings.stream = Uporabnik lahko predvaja datoteke
+usersettings.jukebox = Uporabnik lahko predvaja datoteke v jukebox na\u010dinu
+usersettings.download = Uporabnik lahko prena\u0161a datoteke s stre\u017enika
+usersettings.upload = Uporabnik lahko nalaga datoteke na stre\u017enik
+usersettings.playlist= Uporabnik lahko ustvarja in bri\u0161e sezname predvajanja
+usersettings.coverart = Uporabnik lahko spreminja slike ovitkov in zna\u010dke
+usersettings.comment= Uporabnik lahko ustvarja in ureja komentarje in ocene
+usersettings.podcast= Uporabnik lahko upravlja s podcasti
+usersettings.username = Uporabni\u0161ko ime
+usersettings.email = e-po\u0161ta
+usersettings.changepassword = Spremeni geslo
+usersettings.password = Geslo
+usersettings.newpassword = Novo geslo
+usersettings.confirmpassword = Potrdi geslo
+usersettings.delete = Izbri\u0161i tega uporabnika
+usersettings.ldap = Avtentificiraj uporabnika v LDAP
+usersettings.nousername = Manjka uporabni\u0161ko ime.
+usersettings.noemail= Napa\u010den e-po\u0161tni naslov.
+usersettings.useralreadyexists = Uporabnik \u017ee obstaja.
+usersettings.nopassword = Potrebno je geslo.
+usersettings.wrongpassword = Gesli se ne ujemata.
+usersettings.ldapdisabled = LDAP avtentikacija ni omogo\u010dena. Poglejte dodatne nastavitve.
+usersettings.passwordnotsupportedforldap = Ni mogo\u010de nastaviti ali spremeniti gesla za LDAP-avtenticirane uporabnike.
+usersettings.ok = Geslo uporabnika {0} uspe\u0161no spremenjeno.
+
+# main.jsp
+main.up = Gor
+main.playall = Predvajaj vse
+main.playrandom = Predvajaj naklju\u010dno
+main.addall = Dodaj vse
+main.tags = Uredi zna\u010dke
+main.playcount = Predvajano {0} krat.
+main.lastplayed = Nazadnje predvajano {0}.
+main.comment = Komentar
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Poudarjeno </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Nova vrstica</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Po\u0161evno </td><td style="padding-left:3em;padding-right:1em">(prazna vrstica) </td><td>Nov odstavek</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>Seznam </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Povezava</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>O\u0161tevil\u010den seznam</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Poimenovana povezava</td></tr>\
+ </table>
+main.sharealbum = Skupna uporaba
+main.more = Ve\u010d dejanj...
+main.more.selection = Izbrane skladbe
+main.more.share = Skupna uporaba
+main.donate = <a href="{0}" style="text-decoration:underline">Prispevajte</a> k projektu {1}!<br>(in odstrani ta oglas)
+main.nowplaying = Predvaja se
+main.lyrics = Besedilo
+main.minutesago = minut nazaj
+main.chat = Sporo\u010dilo v klepetalnici
+main.message = Napi\u0161i sporo\u010dilo
+main.clearchat = Izbri\u0161i sporo\u010dila
+main.addtoplaylist.title = Dodaj na seznam predvajanja
+main.addtoplaylist.text = Dodaj ozna\u010dene skladbe na trenutni seznam predvajanja:
+
+# rating.jsp
+rating.rating = Ocena
+rating.clearrating = Izbri\u0161i oceno
+
+# coverArt.jsp
+coverart.change = Spremeni
+coverart.zoom = Zoom
+
+# allmusic.jsp
+allmusic.text = Iskanje albuma <em>{0}</em> na allmusic.com - prosimo, po\u010dakajte.
+
+# changeCoverArt.jsp
+changecoverart.title = Spremeni sliko ovitka
+changecoverart.address = Ali vnesite URL naslov do slike
+changecoverart.artist = Izvajalec
+changecoverart.album = Album
+changecoverart.search = Google slike - iskanje
+changecoverart.wait = Prosimo, po\u010dakajte...
+changecoverart.success = Slika je bila uspe\u0161no prene\u0161ena.
+changecoverart.error = Prenos slike ni uspel.
+changecoverart.noimagesfound = Slike ni mogo\u010de najti.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Spreminjanje slike ovitka ni uspelo:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Uredi zna\u010dke
+edittags.file = Datoteka
+edittags.track = Skladba
+edittags.songtitle = Naslov
+edittags.artist = Izvajalec
+edittags.album = Album
+edittags.year = Leto
+edittags.genre = Zvrst
+edittags.status = Stanje
+edittags.suggest = Priporo\u010daj
+edittags.reset = Ponastavi
+edittags.suggest.short = P
+edittags.reset.short = R
+edittags.set = Nastavi
+edittags.working = Popravljam...
+edittags.updated = Popravljeno
+edittags.skipped = Presko\u010deno
+edittags.error = Napaka
+
+# share.jsp
+share.title = Skupna uporaba
+share.warning = <h2>POMEMBNO OPOZORILO!</h2><p>Igrajte po\u0161teno &ndash; Ne dovolite dostopa do avtorsko za\u0161\u010ditenih vsebin, \u010de je to v nasprotju z zakonom</p>
+share.facebook = Daj v skupno uporabo na Facebook
+share.twitter = Daj v skupno uporaba na Twitter
+share.googleplus = Daj v skupno uporaba na Google+
+share.link = Ali pa dajte datoteko v skupno uporabo tako, da jim po\u0161ljete to povezavo: <a href="{0}" target="_blank">{0}</a>
+share.disabled = \u010ce \u017eelite deliti va\u0161o glasbo z drugimi, si morate najprej ustvariti svoj <em>subsonic.org</em> naslov.<br> \
+ To lahko storite pod <a href="networkSettings.view"><b>Nastavitve &gt; Omre\u017eje</b></a> (potrebovali boste administratorske pravice).
+share.manage = Upravljanje z vsebinami v skupni uporabi
+
+# donate.jsp
+donate.title = Prispevajte
+donate.invalidlicense = Neveljaven licen\u010dni klju\u010d.
+donate.amount = Prispevajte {0}
+
+donate.textbefore = <p>Hvala, ker ste pripravljeni prispevati in podpreti projekt {0}! \
+ Kot donator imate dostop do dodatnih vsebin, kot so:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Applikacije</a> za Android, iPhone in Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Applikacije</a> za PlayBook, Roku, Mac, Chrome in druge*.</li> \
+ <li>Predvajanje videa.</li> \
+ <li>Va\u0161 osebni naslov stre\u017enika: <em>va\u0161e_ime</em>.subsonic.org (glejte <a href="networkSettings.view">Nastavitve &gt; Omre\u017eje</a>).</li> \
+ <li>Dodajanje vsebin v skupno uporabo preko portalov Facebook, Twitter, Google+.</li> \
+ <li>Odstranitev oglasov iz spletnega vmesnika.</li> \
+ <li>Dodatne vsebine, ki bodo na vojo kasneje.</li> \
+ </ul> \
+ <p style="font-size:9px;">* Nekatere aplikacije so na prodaj pri neodvisnih razvijalcih.</p>\
+ <p>Kot donator boste prejeli licen\u010dni klju\u010d, ki velja za to \
+ in vse bodo\u010de razli\u010dice programa {0}.</p> \
+ <p>Priporo\u010damo donacijo v vi\u0161ini <b>&euro;20</b>, vendar lahko izberete katerokoli vsoto:</p>
+donate.textafter = <p>Pritisnite na gumb za prehod na PayPal, kjer lahko pla\u010date s kreditno kartico ali pa \
+ z va\u0161im PayPal ra\u010dunom (\u010de ga imate). Licen\u010dni klju\u010d boste prejeli po e-po\u0161ti v nekaj minutah.</p> \
+ <p>\u010ce imate kakr\u0161nokoli vpra\u0161anje, nam po\u0161jite e-po\u0161tno sporo\u010dilo na \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Ta kopija programa {2} je bila licencirana na {0} dne {1}. Hvala za va\u0161o podporo!
+donate.register = Potem, ko boste prejeli licen\u010dni klju\u010d, ga prosimo registrirajte spodaj.
+donate.resend = Imate \u017ee kupljeno licenco, vendar ste izgubili klju\u010d? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Po\u0161ljite ga ponovno</a>.
+donate.register.email = E-po\u0161ta
+donate.register.license = Licenca
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast sprejemnik
+podcastreceiver.expandall = Poka\u017ei epizode
+podcastreceiver.collapseall = Skrij epizode
+podcastreceiver.status.new = Novo
+podcastreceiver.status.downloading = Prena\u0161anje
+podcastreceiver.status.completed = Dokon\u010dano
+podcastreceiver.status.error = Napaka
+podcastreceiver.status.deleted = Izbrisano
+podcastreceiver.status.skipped = Presko\u010deno
+podcastreceiver.downloadselected= Prenesi izbrane
+podcastreceiver.deleteselected= Izbri\u0161i izbrane
+podcastreceiver.confirmdelete= Resni\u010dno \u017eelite izbrisati izbrane podcaste?
+podcastreceiver.check = Preveri za nove epizode
+podcastreceiver.refresh = Osve\u017ei stran
+podcastreceiver.settings = Podcast nastavitve
+podcastreceiver.subscribe = Naro\u010di se na podcast
+
+# lyrics.jsp
+lyrics.title = Besedilo
+lyrics.artist = Izvajalec
+lyrics.song = Skladba
+lyrics.search = I\u0161\u010di
+lyrics.wait = Iskanje besedila v teku, prosimo po\u010dakajte...
+lyrics.courtesy = (Besedila na vojo z <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Ne najdem besedila.
+
+# helpPopup.jsp
+helppopup.title = {0} pomo\u010d
+helppopup.cover.title = Velikost slike ovitka
+helppopup.cover.text = <p>Omogo\u010da nastavitev velikosti slike ovitka, ki se prika\u017ee ob albumu, z mo\u017enostjo, da se ovitki sploh ne prikazujejo.</p>
+helppopup.transcode.title = Najve\u010dja bitna hitrost
+helppopup.transcode.text = <p>\u010ce nimate najhitrej\u0161e povezave, lahko omejite bitno hitrost, s katero boste pretakali glasbo. \
+ Primer: \u010de imate mp3 datoteke kodirane v 256 Kbps (kilobitih na sekundo), bo z nastavitvijo \
+ maksimalne bitne hitrosti na 128 Kbps {0} samodejno ponovno vzor\u010dil vse pesmi iz 256 na 128 Kbps.</p> \
+ <p>Ta opcija zahteva, da imate name\u0161\u010den LAME. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ je odprtokodni mp3 kodirnik. Lahko ga <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">prenesete tukaj</a>. \
+ Preverite, da ga boste namestili v mapo SUBSONIC_HOME/transcode.</p>
+helppopup.musicmask.title = Vrste glasbenih datotek
+helppopup.musicmask.text = <p>Dolo\u010dite vrsto datotek, ki bodo prepoznane kot "glasbene".</p>
+helppopup.videomask.title = Vrste video datotek
+helppopup.videomask.text = <p>Dolo\u010dite vrsto datotek, ki bodo prepoznane kot "video".</p>
+helppopup.coverartmask.title = Vrste slik ovitkov
+helppopup.coverartmask.text = <p>Dolo\u010dite vrsto datotek, ki bodo prepoznane kot "slike ovitkov", ko bo program pregledoval posamezne glasbene (pod)imenike.</p>
+helppopup.downsamplecommand.title = Ukaz za zmanj\u0161anje bitne hitrosti
+helppopup.downsamplecommand.text = <p>Dolo\u010dite ukaz, ki se bo izvedel ob zahtevi za zmanj\u0161evanje bitne hitrosti posnetkov.</p>\
+ <p>(%s = Ime datoteke, %b = Najve\u010dja bitna hitrost, %t = Naslov, %a = Izvajalec, %l = Album)</p>
+helppopup.index.title = Indeks
+helppopup.index.text = <p>Omogo\u010da vam dolo\u010diti, kak\u0161en bo izgled abecednega indeksa (na levi strani zaslona). Imeniki in podimeniki (pesmi in albumi) \
+ so neposredno dostopni s klikom na \u010drko oz. drug znak v tem indeksu.</p> \
+ <p>Abecedni indeks sestavljajo posamezne \u010drke oz. drugi znaki, med seboj lo\u010deni s presledkom. Obi\u010dajno boste kot element indeksa izbrali posamezno \u010drko, \
+ lahko pa izberete tudi dva ali ve\u010d znakov. Zapis <em>The</em> bi tako npr. podal povezavo na vse datoteke \
+ in/ali imenike, ki se pri\u010denjajo z oznako "The".</p> \
+ <p>Skupino znakov lahko postavite tudi v oklepaj. Niz znakov \
+ <em>A-E(ABCDE)</em> bi se prikazal kot <em>A-E</em>; tak vnos bi v to skupino postavil vse datoteke in imenike, ki se za\u010denjajo s \u010drkami \
+ A, B, C, D ali E. To je posebej uporabno za zdru\u017eevanje manj pogosto uporabljenih znakov (npr. X, Y in Z) ali \
+ za zdru\u017eevanje istega znaka z razli\u010dnimi akcenti (kot npr. A, \u00c0 in \u00c1)</p> \
+ <p>Datoteke in imeniki, katerih za\u010detni znaki niso v indeksnem seznamu, bodo prikazani v indeksni skupini, ozna\u010deni z "#".</p>
+helppopup.ignoredarticles.title = Ignoriraj naslednje predpone
+helppopup.ignoredarticles.text = <p>Tu lahko dolo\u010dite seznam predpon v imenih izvajalcev in/ali albumov (npr. "The"), ki ne bodo upo\u0161tevane pri izdelavi abecednega seznama.</p>
+helppopup.shortcuts.title = Bli\u017enjice
+helppopup.shortcuts.text = <p>Seznam najvi\u0161e uvr\u0161\u010denih imenikov, lo\u010den s presledki. \u010ce ima imenik ve\u010d besed, morate uporabiti narekovaje, npr.:</p> \
+ <p><em>Novi dohodni "Sound tracks"</em></p>
+helppopup.language.title = Jezik
+helppopup.language.text = <p>Omogo\u010da izbiro jezika uporabni\u0161kega vmesnika.</p>
+helppopup.visibility.title = Pogled
+helppopup.visibility.text = <p>Izberite, kateri podatki bodo prikazani za vsako pesem posebej, kot tudi dol\u017eino napisa. To je najve\u010dje \
+ \u0161tevilo znakov za prikaz imen skladb, albumov in izvajalcev.</p>
+helppopup.partymode.title = \u017dur na\u010din
+helppopup.partymode.text = <p>\u010ce izberete to mo\u017enost, bo uporabni\u0161ki vmesnik poenostavljen in ga bodo neizku\u0161eni uporabniki la\u017ee upravljali. \
+ \u0160e posebej je onemogo\u010deno, da bi kdo po nesre\u010di pome\u0161al seznam predvajanja.</p>
+helppopup.theme.title = Teme
+helppopup.theme.text = <p>Izberite med razli\u010dnimi temami, ki dolo\u010dajo barve, slike, izbiro pisav in druge elemente prikaza programa {0}.</p>
+helppopup.welcomemessage.title = Sporo\u010dilo dobrodo\u0161lice
+helppopup.welcomemessage.text = <p>Sporo\u010dilo dobrodo\u0161lice na izhodi\u0161\u010dni strani.</p>
+helppopup.loginmessage.title = Prijavno sporo\u010dilo
+helppopup.loginmessage.text = <p>Sporo\u010dilo na strani za prijavo v program.</p>
+helppopup.coverartlimit.title = Omejitve za prikaz slik ovitkov
+helppopup.coverartlimit.text = <p>Najve\u010dje \u0161tevilo slik ovitkov, ki bodo prikazane na strani.</p>
+helppopup.downloadlimit.title = Omejitev snemanja s stre\u017enika
+helppopup.downloadlimit.text = <p>Kolik\u0161en dele\u017e pasovne \u0161irine bo uporabljen za prenos podatkov s stre\u017enika</p>
+helppopup.uploadlimit.title = Omejitev nalaganja na stre\u017enik
+helppopup.uploadlimit.text = <p>Kolik\u0161en dele\u017e pasovne \u0161irine bo uporabljen za nalaganje podatkov na stre\u017enik.</p>
+helppopup.streamport.title = Vrata za ne-SSL povezavo
+helppopup.streamport.text = <p>Ta mo\u017enost je smiselna le, kadar uporabljate {0} na stre\u017eniku s SSL (HTTPS).</p><p>Nekateri predvajalniki \
+ (npr. Winamp) ne podpirajo predvajanja glasbe preko SSL. Dolo\u010dite \u0161tevilko vrat za obi\u010dajen http (obi\u010dajno 80 \
+ ali 4040), \u010de \u017eelite, da se glasba ne po predvajala preko SSL protokola. Ne pozabite da predvajanje ne bo \u0161ifrirano.</p>
+helppopup.ldap.title = LDAP preverjanje pristnosti
+helppopup.ldap.text = <p>Uporabnike lahko avtenticira zunanji LDAP stre\u017enik (tudi Windows Active Directory). \
+ Pri prijavi LDAP uporabnika v {0}, bo uporabni\u0161ko ime in geslo preveril zunanji stre\u017enik in ne program {0}.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>URL naslov LDAP stre\u017enika. Protokol mora biti ali <em>ldap://</em> ali <em>ldaps://</em> \
+ (za LDAP preko SSL). Poglejte <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">tukaj</a> \
+ za podrobnej\u0161e informacije.</p>
+helppopup.ldapsearchfilter.title = LDAP iskalni filter
+helppopup.ldapsearchfilter.text = <p>Isklani niz, uporabljen pri iskanju uporabnikov. To je LDAP iskalni filter \
+ (kot je dolo\u010deno v <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ Vzorec "'{0'}" zamenja uporabni\u0161ko ime, na primer: \
+ <ul>\
+ <li>(uid='{0'}) - iskanje po uporabni\u0161kem imenu v uid vrednosti.</li> \
+ <li>(sAMAccountName='{0'}) - navadno uporabljeno za preverjanje pristnosti v sistemu Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP upravitelj DN
+helppopup.ldapmanagerdn.text = <p>\u010ce LDAP stre\u017enik ne podpira anonimnega vpenjanja (anonymous binding), morate dolo\u010diti DN \
+ (<em>Distinguished Name</em>) in geslo LDAP uporabnika, ki naj se uporabi pri vpenjanju.</p>
+helppopup.ldapautoshadowing.title = Samodejno ustvari LDAP uporabnike v programu {0}
+helppopup.ldapautoshadowing.text = <p>\u010ce izberete to mo\u017enost, vam ne po potrebno ustvarjati LDAP uporabnikov v programu {0} pred prijavo v sistem.</p> \
+ <p>OPOZORILO! To pomeni, da se bo vsak uporabnik z veljavnim LDAP uporabni\u0161kim imenom in geslom lahko prijavil v {0}, \
+ \u010desar morda ne \u017eelite omogo\u010diti.</p>
+helppopup.playername.title = Ime predvajalnika
+helppopup.playername.text = <p>Omogo\u010da dolo\u010diti ime predvajalnika, ki si ga je lahko zapomniti (npr. "Slu\u017eba" ali "Dnevna soba").</p>
+helppopup.autocontrol.title = Samodejen nadzor predvajalnika
+helppopup.autocontrol.text = <p>\u010ce boste izbrali to mo\u017enost, bo {0} samodejno zagnal predvajalnik, ko boste izbrali "Predvajaj" \
+ v predvajalnem seznamu. \u010ce ne, boste morali zagnati in povezati predvajalnik sami.</p>
+helppopup.dynamicip.title = Dinami\u010den IP naslov
+helppopup.dynamicip.text = <p>Izklju\u010dite to mo\u017enost, \u010de predvajalnik uporablja stati\u010den IP naslov.</p>
+
+# wap/index.jsp
+wap.index.missing = Ne najdem glasbe
+wap.index.playlist = Seznam predvajanja
+wap.index.search = I\u0161\u010di
+wap.index.settings = Nastavitve
+
+# wap/browse.jsp
+wap.browse.playone = Predvajaj skladbo
+wap.browse.playall = Predvajaj vse
+wap.browse.addone = Dodaj skladbo
+wap.browse.addall = Dodaj vse
+wap.browse.downloadone = Prenesi skladbo
+wap.browse.downloadall = Prenesi vse
+
+# wap/playlist.jsp
+wap.playlist.title = Seznam predvajanja
+wap.playlist.noplayer = Noben predvajalnik ni povezan
+wap.playlist.clear = Po\u010disti
+wap.playlist.load = Nalo\u017ei
+wap.playlist.random = Naklju\u010dno
+wap.playlist.play = Predvajaj na telefonu
+
+# wap/search.jsp
+wap.search.title = I\u0161\u010di
+
+# wap/searchResult.jsp
+wap.searchresult.index = Iskalni indeks se trenutno ustvarja. Prosimo, poskusite ponovno kasneje.
+
+# wap/settings.jsp
+wap.settings.selectplayer = Izberi predvajalnik
+wap.settings.allplayers = Vse
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sv.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sv.properties
new file mode 100644
index 00000000..b5144604
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_sv.properties
@@ -0,0 +1,720 @@
+# Swedish localization.
+# Original Author: J\u00f6rgen Sj\u00f6berg (jorgen at famsjoberg.nu)
+# Refactored 09-04-10 by Fredrik Leufkens (fredrik.leufkens at gmail.com)
+
+
+common.home = Hem
+common.back = Bak\u00e5t
+common.help = Hj\u00e4lp
+common.play = Spela
+common.add = L\u00e4gg till
+common.download = Ladda ner
+common.close = St\u00e4ng
+common.refresh = Uppdatera
+common.next = N\u00e4sta
+common.previous = F\u00f6reg\u00e5ende
+common.more = Mer
+common.ok = OK
+common.cancel = Avbryt
+common.save = Spara
+common.create = Skapa
+common.delete = Radera
+common.unknown = (Ok\u00e4nd)
+common.default = (Standard)
+
+# login.jsp
+login.username = Anv\u00e4ndarnamn
+login.password = L\u00f6senord
+login.login = Logga in
+login.remember = Kom ih\u00e5g
+login.logout = Du \u00e4r nu loggat ut. V\u00e4lkommen tillbaka!
+login.error = Fel anv\u00e4ndarID eller l\u00f6senord.
+login.insecure = Subsonic \u00e4r inte s\u00e4kert. V\u00e4nligen logga in med anv\u00e4ndarID och<br>l\u00f6senord "admin". Byt sen l\u00f6senord omedlebart.
+
+# accessDenied.jsp
+accessDenied.title = \u00c5tkomst nekad
+accessDenied.text = Ledsen, du har inte r\u00e4ttigheter att utf\u00f6ra \u00f6nskad \u00e5tg\u00e4rd.
+
+# top.jsp
+top.home = Hem
+top.now_playing = Spelas
+top.settings = Inst\u00e4llningar
+top.status = Status
+top.podcast = Podcast
+top.more = Mer
+top.help = Hj\u00e4lp
+top.search = S\u00f6k
+top.upgrade = <b>OBS!</b> En ny verson \u00e4r tillg\u00e4nglig.<br>Ladda ner Subsonic {0} \
+ <a href="#" onclick="window.open('''http://subsonic.org/''')">h\u00e4r</a>.
+top.missing = Ingen musikmapp \u00e4r funnen. V\u00e4nligen \u00e4ndra inst\u00e4llningar.
+top.logout = Logga ut {0}
+
+# left.jsp
+left.statistics = {0}&nbsp;artister<br>\
+ {1}&nbsp;album<br>\
+ {2}&nbsp;melodier<br>\
+ {3} (&#126; {4} timmar)
+left.shortcut = Genv\u00e4gar
+left.radio = Internet TV/radio
+left.allfolders = Alla mappar
+
+# playlist.jsp
+playlist.stop = Stop
+playlist.start = Spela
+playlist.confirmclear = Vill du verkligen t\u00f6mma spellistan??
+playlist.clear = T\u00f6m
+playlist.shuffle = Blanda
+playlist.repeat_on = Repetera \u00e4r p\u00e5
+playlist.repeat_off = Repetera \u00e4r av
+playlist.undo = G\u00f6r om
+playlist.settings = Inst\u00e4llningar
+playlist.more = Fler alternativ...
+playlist.more.playlist = Spellista
+playlist.more.sortbytrack = Sortera efter sp\u00e5r
+playlist.more.sortbyartist = Sortera efter artist
+playlist.more.sortbyalbum = Sortera efter album
+playlist.more.selection = Valda l\u00e5tar
+playlist.more.selectall = V\u00e4lj alla
+playlist.more.selectnone = V\u00e4lj ingen
+playlist.getflash = H\u00e4mta Flashspelare
+playlist.load = Ladda
+playlist.save = Spara
+playlist.append = L\u00e4gg till i spellista
+playlist.remove = Ta bort
+playlist.up = Upp
+playlist.down = Ner
+playlist.empty = Spellistan \u00e4r tom
+
+# videoPlayer.jsp
+videoPlayer.getflash = V\u00e4nligen installera Flash Player
+videoPlayer.popout = \u00d6ppna i nytt f\u00f6nster
+
+# status.jsp
+status.title = Status
+status.type = Typ
+status.stream = Str\u00f6m
+status.download = Nerladdat
+status.upload = Uppladdat
+status.player = Spelare
+status.user = Anv\u00e4ndare
+status.current = Vald fil
+status.transmitted = \u00d6verf\u00f6rt
+status.bitrate = Bandbredd (Kbps)
+
+# search.jsp
+search.title = S\u00f6k
+search.query = Artist, album eller l\u00e5ttitel
+search.search = S\u00f6k
+search.index = S\u00f6k indexering h\u00e5ller just p\u00e5 att bli skapad. V\u00e4nligen f\u00f6rs\u00f6k senare.
+search.hits.none = Inget objekt funnet.
+search.hits.more = Mer
+search.hits.artists = Artister
+search.hits.albums = Album
+search.hits.songs = L\u00e5tar
+
+# gettingStarted.jsp
+gettingStarted.title = Kom ig\u00e5ng
+gettingStarted.text = <p>V\u00e4lkommen till Subsonic! F\u00f6r att komma ig\u00e5ng p\u00e5 nolltid, f\u00f6lj bara stegen nedan.<br> \
+ Klicka p\u00e5 "Hem"-knappen ovan f\u00f6r att \u00e5ter komma till denna sida.</p> \
+ <p>F\u00f6r mer information, v\u00e4nligen se <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Att komma ig\u00e5ng</b></a> guide.</p>
+gettingStarted.step1.title = \u00c4ndra administrationsl\u00f6senordet.
+gettingStarted.step1.text = S\u00e4kra din server genom att \u00e4ndra standardl\u00f6senordet till kontot f\u00f6r administration. \
+ Du kan ocks\u00e5 l\u00e4gga till anv\u00e4ndare med olika privilegier.
+gettingStarted.step2.title = S\u00e4tt upp mediamappar.
+gettingStarted.step2.text = Tala om f\u00f6r Subsonic s\u00f6kv\u00e4gen till din musik och dina videos.
+gettingStarted.step3.title = Konfigurera inst\u00e4llningarna f\u00f6r n\u00e4tverket.
+gettingStarted.step3.text = Anv\u00e4ndbara inst\u00e4llningar om du vill f\u00e5 tillg\u00e5ng till din musik \u00f6ver Internet, \
+ eller dela den med familj och v\u00e4nner. Skapa din personliga <b><em>dittnamn</em>.subsonic.org</b> \
+ adress.
+gettingStarted.hide = Visa inte detta igen
+gettingStarted.hidealert = F\u00f6r att visa denn info igen, g\u00e5 till Inst\u00e4llningar > Allm\u00e4nt.
+
+# home.jsp
+home.random.title = Slumpvis valda
+home.newest.title = Senaste
+home.highest.title = H\u00f6gst rankade
+home.frequent.title = Mest spelade
+home.recent.title = Nyligen spelade
+home.users.title = Anv\u00e4ndare
+home.random.text = Slumpvalda album
+home.newest.text = Nyligen valda eller modifierade album
+home.highest.text = H\u00f6gst rankade album
+home.frequent.text = Mest spelade albums
+home.recent.text = Nyligen spelade album
+home.users.text = Anv\u00e4ndarstatistik
+home.scan = Musikmappen blir just skannad. Alla funktioner \u00e4r inte tillg\u00e4ngliga \u00e4nnu.
+home.listsize = {0} album per sida
+home.albums = Album {0} - {1}
+home.playcount = Spelade {0} l\u00e5tar
+home.lastplayed = Spelad {0}
+home.created = Modifierad {0}
+home.chart.total = Totalt (MB)
+home.chart.stream = Str\u00f6mmat (MB)
+home.chart.download = Nedladdat (MB)
+home.chart.upload = Uppladdat (MB)
+
+# more.jsp
+more.title = Mer
+more.random.title = Slumpad spellista
+more.random.text = Skapa slumpad spellista med
+more.random.songs = {0} l\u00e5tar
+more.random.auto = Spela mer slumpade l\u00e5tar n\u00e4r spelistan \u00e4r slut.
+more.random.ok = OK
+more.random.genre = fr\u00e5n genre
+more.random.anygenre = alla
+more.random.year = och \u00e5r
+more.random.anyyear = alla
+more.random.folder = i mapp
+more.random.anyfolder = vilken som helst
+more.apps.title = Subsonic Apps
+more.apps.text = <p><a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic apps</a> finns tillg\u00e4nglig f\u00f6r <b>Android</b>, <b>iPhone</b>, \
+ <b>Windows Phone</b> och <b>AIR</b>.</p>
+more.mobile.title = Mobil
+more.mobile.text = <p>Du kan kontrollera Subsonic fr\u00e5n en WAP mobil eller PDA.<br> \
+ Surfa bara till denna URL fr\u00e5n mobilen: <b>http://yourhostname/wap</b></p> \
+ <p>Det h\u00e4r kr\u00e4ver att din server kan n\u00e5s fr\u00e5n internet.</p>
+more.podcast.title = Podcast
+more.podcast.text = <p>Sparade spellistor \u00e4r tillg\u00e4ngliga som Podcasts.<br>\
+ Anv\u00e4nd denna URL i din Podcast l\u00e4sare: <b>http://yourhostname/podcast</b>, \
+ eller <b><a href="podcast.view?suffix=.rss">Klicka h\u00e4r</a>.</b></p>
+more.upload.title = Ladda upp fil
+more.upload.source = V\u00e4lj fil
+more.upload.target = Spara filen p\u00e5
+more.upload.browse = V\u00e4lj
+more.upload.ok = Ladda upp
+more.upload.unzip = Packa upp zip-filer automatiskt.
+more.upload.progress = % f\u00e4rdigt. v\u00e4nligen v\u00e4nta...
+
+# upload.jsp
+upload.title = Ladda upp fil
+upload.success = Uppladdning lyckades <b>{0}</b>
+upload.empty = Inga filer att skicka.
+upload.failed = Uppladdning misslyckades med f\u00f6ljande error meddelande:<br><b>"{0}"</b>
+upload.unzipped = Uppackade: {0}
+
+# help.jsp
+help.title = Om Subsonic
+help.upgrade = <b>Note!</b> En ny version \u00e4r tillg\u00e4nglig. Ladda ner Subsonic {0} \
+ <a href="#" onclick="window.open('''http://subsonic.org/''')">h\u00e4r</a>.
+help.version.title = Version
+help.builddate.title = Kompilerad
+help.server.title = Server
+help.license.title = Licens
+help.license.text = {0} \u00e4r gratis distribuerad under <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> open-source licens. \
+ {0} anv\u00e4nder <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">licensierad 3:e partsprogram</a>. V\u00e4nligen notera att {0} \u00e4r <em>inte</em> \
+ ett verktyg f\u00f6r illegal distribution av copyrightskyddat material. Var alltid uppm\u00e4rksam p\u00e5 att f\u00f6lja de relevanta lagar som \u00e4r specifika f\u00f6r ditt land.
+help.homepage.title = Hemsida
+help.forum.title = Forum
+help.shop.title = Handelsvaror
+help.contact.title = Kontakt
+help.contact.text = Subsonic \u00e4r utvecklat och underh\u00e5llet av Sindre Mehus \
+ (<a href="mailto:sindre_mehus@users.sourceforge.net">sindre_mehus@users.sourceforge.net</a>). \
+ Om du har fr\u00e5gor, kommentarer eller f\u00f6rslag p\u00e5 f\u00f6rb\u00e4ttringar, V\u00e4nligen bes\u00f6k \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonics Forum</a>.
+help.donate = Subsonic \u00e4r gratis, men du kan g\u00e4rna l\u00e4mna bidrag till projektet genom en <b><a href="donate.view?">donation</a></b>.
+help.log = Logg
+help.logfile = Den kompletta loggen \u00e4r sparad i {0}.
+
+# settingsHeader.jsp
+settingsheader.title = Inst\u00e4llningar
+settingsheader.general = Generellt
+settingsheader.advanced = Avancerat
+settingsheader.personal = Personligt
+settingsheader.musicFolder = Musikmappar
+settingsheader.internetRadio = Internet TV/radio
+settingsheader.podcast = Podcast
+settingsheader.player = Spelare
+settingsheader.share = Delad Media
+settingsheader.network = N\u00e4tverk
+settingsheader.transcoding = Transcoding
+settingsheader.user = Anv\u00e4ndare
+settingsheader.search = S\u00f6k
+settingsheader.coverArt = Albumomslag
+settingsheader.password = L\u00f6senord
+
+# generalSettings.jsp
+generalsettings.playlistfolder = S\u00f6kv\u00e4g till spellistor
+generalsettings.musicmask = Musiktyper
+generalsettings.videomask = Videotyper
+generalsettings.coverartmask = Omslagsbildstyper
+generalsettings.index = Index
+generalsettings.ignoredarticles = Artiklar att ignorera
+generalsettings.shortcuts = Genv\u00e4gar
+generalsettings.showgettingstarted = Visa "Att komma ig\u00e5ng" vid start
+generalsettings.welcometitle = V\u00e4lkomstmeny
+generalsettings.welcomesubtitle = V\u00e4lkomst undermeny
+generalsettings.welcomemessage = V\u00e4lkomsttext
+generalsettings.loginmessage = Meddelande vid inloggning
+generalsettings.language = Standardspr\u00e5k
+generalsettings.theme = Standardtema
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = Downsample kommando
+advancedsettings.coverartlimit = Cover art begr\u00e4nsning<br><div class="detail">(0 = Obegr\u00e4nsad)</div>
+advancedsettings.downloadlimit = Download begr\u00e4nsning (Kbps)<br><div class="detail">(0 = Obegr\u00e4nsad)</div>
+advancedsettings.uploadlimit = Upload gr\u00e4ns (Kbps)<br><div class="detail">(0 = Obegr\u00e4nsad)</div>
+advancedsettings.streamport = Non-SSL str\u00f6m port<br><div class="detail">(0 = Inaktivt)</div>
+advancedsettings.ldapenabled = Aktivera LDAP autentisering
+advancedsettings.ldapurl = LDAP URL
+advancedsettings.ldapsearchfilter = LDAP s\u00f6k filter
+advancedsettings.ldapmanagerdn = LDAP manager DN<br><div class="detail">(Valfritt)</div>
+advancedsettings.ldapmanagerpassword = L\u00f6senord
+advancedsettings.ldapautoshadowing = Skapa automatiskt anv\u00e4ndare i Subsonic
+
+# personalSettings.jsp
+personalsettings.title = Personal inst\u00e4llningar f\u00f6r {0}
+personalsettings.language = Spr\u00e5k
+personalsettings.theme = Spr\u00e5k
+personalsettings.display = Visa
+personalsettings.browse = Utforska
+personalsettings.playlist = Spellista
+personalsettings.tracknumber = Sp\u00e5r #
+personalsettings.artist = Artist
+personalsettings.album = Album
+personalsettings.genre = Genre
+personalsettings.year = \u00c5r
+personalsettings.bitrate = Bit rate
+personalsettings.duration = Varaktighet
+personalsettings.format = Format
+personalsettings.filesize = Filstorlek
+personalsettings.captioncutoff = Max tecken
+personalsettings.partymode = Partyl\u00e4ge
+personalsettings.shownowplaying = Visa vad andra anv\u00e4ndare spelar
+personalsettings.nowplayingallowed = L\u00e5t andra se vad jag spelar
+personalsettings.showchat = Visa chatmeddelanden
+personalsettings.finalversionnotification = Meddela om nya versioner
+personalsettings.betaversionnotification = Meddela om nya beta versioner
+personalsettings.lastfmenabled = Registrera vad jag spelar hos <a href="http://last.fm/" target="_blank">Last.fm</a>
+personalsettings.lastfmusername = Last.fm anv\u00e4ndare
+personalsettings.lastfmpassword = Last.fm l\u00f6senord
+personalsettings.avatar.title = Personlig bild
+personalsettings.avatar.none = Ingen bild
+personalsettings.avatar.custom = Egen bild
+personalsettings.avatar.changecustom = Byt egen bild
+personalsettings.avatar.upload = Ladda upp
+personalsettings.avatar.courtesy = Ikoner med r\u00e4ttighet av <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = Byt personlig bild
+avataruploadresult.success = Uppdladdning av personlig bild "{0}" lyckades.
+avataruploadresult.failure = Uppladdningen misslyckades. Se <a href="help.view?">log</a> f\u00f6r detaljer.
+
+# passwordSettings.jsp
+passwordsettings.title = Byt l\u00f6senord f\u00f6r {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = S\u00f6kv\u00e4g
+musicfoldersettings.name = Namn
+musicfoldersettings.enabled = Aktiverad
+musicfoldersettings.add = L\u00e4gg till mapp
+musicfoldersettings.nopath = V\u00e4nligen specificera en s\u00f6kv\u00e4g.
+
+# networkSettings.jsp
+networksettings.text = Anv\u00e4nd inst\u00e4llningarna nedan f\u00f6r att f\u00e5 tillg\u00e5ng till Subsonicserver via internet.<br> \
+ Om du f\u00e5r problem, v\u00e4nligen konsultera <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>Att komma ig\u00e5ng</b></a> guiden.
+networksettings.portforwardingenabled = Automatiskt konfigurera din router f\u00f6r godk\u00e4nnande av inkommande anslutningar till Subsonic (via UPnP eller NAT-PMP port forwarding).
+networksettings.portforwardinghelp = Om din router inte kan bli konfigurerad automatiskt kan du s\u00e4tta upp den manuellt. \
+ F\u00f6lj instruktionerna p\u00e5 <a href="http://portforward.com/" target="_blank">portforward.com</a>. \
+ Du m\u00e5ste \u00f6ppna port {0} till datorn som har Subsonic installerad.
+networksettings.urlredirectionenabled = F\u00e5 tillg\u00e5ng till din server via internet genom att anv\u00e4nda en adress som \u00e4r l\u00e4tt att komma ih\u00e5g.
+networksettings.status = Status:
+networksettings.trialexpired = F\u00f6rs\u00f6ksperioden utg\u00e5r den {0}. V\u00e4nligen <b><a href="donate.view?">donera</a></b> f\u00f6r att f\u00e5 tillg\u00e5ng till denna funktion permanent.
+networksettings.trialnotexpired = Denna funktion \u00e4r tillg\u00e4nglig till {0}. Efter detta m\u00e5ste du <b><a href="donate.view?">donera</a></b> f\u00f6r att f\u00e5 tillg\u00e5ng till den.
+
+# transcodingSettings.jsp
+transcodingsettings.name = Namn
+transcodingsettings.sourceformat = Fr\u00e5n
+transcodingsettings.targetformat = Till
+transcodingsettings.step1 = Steg 1
+transcodingsettings.step2 = Steg 2
+transcodingsettings.step3 = Steg 3
+transcodingsettings.add = L\u00e4gg till transcoding
+transcodingsettings.defaultactive = Standard
+transcodingsettings.recommended = Rekommenderad konfiguration
+transcodingsettings.noname = V\u00e4nligen ange ett namn.
+transcodingsettings.nosourceformat = V\u00e4nligen ange ett format att konvertera fr\u00e5n.
+transcodingsettings.notargetformat = V\u00e4nligen ange ett format att konvertera till.
+transcodingsettings.nostep1 = V\u00e4nligen ange minst ett transcoding steg.
+transcodingsettings.info = <p class="detail">(%s = Filen som ska bli transkodad, %b = Max bitrate av spelaren)</p> \
+ <p>Transkodning \u00e4r en process f\u00f6ratt konvertera ett media format till ett annat. Subsonics transkoding \
+ motor till\u00e5ter d\u00e4rigenom str\u00f6mning av format som normalt inte \u00e4r str\u00f6mningsbart. Transkodningen sker on-the-fly och \
+ kr\u00e4ver inget diskutrymme.<p/> \
+ <p>Den aktiva transkodningen sker av tredje parts kommadorads program som m\u00e5ste vara installerade i {0}. \
+ Ett transkodnings paket f\u00f6r Windows \
+ \u00e4r tillg\u00e4ngligt <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>h\u00e4r</b></a>. Du kan l\u00e4gga till egna \
+ transkodningar om de uppfyller dessa krav:\
+ <ul> \
+ <li>Det m\u00e5ste g\u00e5 att k\u00f6ra via kommandorad</li> \
+ <li>Det m\u00e5ste s\u00e4nda utdata till stdout.</li> \
+ <li>Om det anv\u00e4nds i steg 2 eller 3, m\u00e5ste det kuna l\u00e4sa indata fr\u00e5n stdin.</li> \
+ </ul> \
+ </p> \
+ <p> OBS! transkodning \u00e4r aktiverat p\u00e5 en per spelare basis fr\u00e5n spellar inst\u00e4llningssidan. Om "Standard" \u00e4r aktiverat, blir transkodningen \
+ automatiskt aktiverad f\u00f6r en ny spelare.</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = Str\u00f6m URL
+internetradiosettings.homepageurl = Hemsida
+internetradiosettings.name = Namn
+internetradiosettings.enabled = Aktiverad
+internetradiosettings.add = L\u00e4gg till Internet TV/radio
+internetradiosettings.nourl = V\u00e4nligen ange en URL.
+internetradiosettings.noname = V\u00e4nligen Specifisera ett namn.
+
+# podcastSettings.jsp
+podcastsettings.update = S\u00f6k efter nya avsnitt
+podcastsettings.keep = Spara
+podcastsettings.keep.all = Alla avsnitt
+podcastsettings.keep.one = Senaste avsnitt
+podcastsettings.keep.many = {0} senaste avsnitten
+podcastsettings.download = N\u00e4r nya avsnitt \u00e4r tillg\u00e4ngliga
+podcastsettings.download.all = Ladda ner alla
+podcastsettings.download.one = Ladda ner de senaste
+podcastsettings.download.many = Ladda ner {0} senaste avsnitten
+podcastsettings.download.none = G\u00f6r inget
+podcastsettings.interval.manually = Manuellt
+podcastsettings.interval.hourly = Varje timme
+podcastsettings.interval.daily = Varje dag
+podcastsettings.interval.weekly = Varje vecka
+podcastsettings.folder = Spara Podcasts i
+
+# playerSettings.jsp
+playersettings.noplayers = Ingen spelare funnen.
+playersettings.type = Typ
+playersettings.lastseen = Senast anv\u00e4nd
+playersettings.title = V\u00e4lj spelare
+
+playersettings.technology.web.title = Webspelare
+playersettings.technology.external.title = Extern spelare
+playersettings.technology.external_with_playlist.title = Extern spelare med spellista
+playersettings.technology.jukebox.title = Jukebox
+playersettings.technology.web.text = Spela musik direkt i web browsern genom den integrerade Flash spelaren.
+playersettings.technology.external.text = Spela musik i din favoritspelare, exempelvis WinAmp eller Windows Media Player.
+playersettings.technology.external_with_playlist.text = Samma som ovan, men spellistan hanteras av spelaren, ist\u00e4llet f\u00f6r \
+ Subsonic servern. Med det h\u00e4r l\u00e4get aktivt \u00e4r det m\u00f6jligt att hoppa mellan l\u00e5tar.
+playersettings.technology.jukebox.text = Spela musik direkt p\u00e5 ljudkortet p\u00e5 servern. (Endast auktoriserade anv\u00e4ndare).
+playersettings.name = Spelarens namn
+playersettings.coverartsize = Omslagsbildens storlek
+playersettings.maxbitrate = Max bandbredd
+playersettings.coverart.off = Av
+playersettings.coverart.small = Liten
+playersettings.coverart.medium = Mellan
+playersettings.coverart.large = Stor
+playersettings.nolame = <em>Meddelande:</em> LAME verkar inte vara installerat.<br>Klicka p\u00e5 hj\u00e4lp knappen f\u00f6r mer information.
+playersettings.autocontrol = Kontrollera spelare automatiskt
+playersettings.dynamicip = Spelaren har dynamisk IP adress
+playersettings.transcodings = Aktivera transcodings
+playersettings.ok = Spara
+playersettings.forget = Radera spelare
+playersettings.clone = Klona spelare
+
+# shareSettings.jsp
+sharesettings.name = Namn
+sharesettings.owner = Delad av
+sharesettings.description = Beskrivning
+sharesettings.visits = Antal bes\u00f6k
+sharesettings.lastvisited = Senaste bes\u00f6k
+sharesettings.expires = Utg\u00e5r
+sharesettings.files = Delade filer
+sharesettings.expirein = Utg\u00e5r om
+sharesettings.expirein.week = 1v
+sharesettings.expirein.month = 1m
+sharesettings.expirein.year = 1\u00e5r
+sharesettings.expirein.never = aldrig
+
+# userSettings.jsp
+usersettings.title = V\u00e4lj anv\u00e4ndare
+usersettings.newuser = Ny anv\u00e4ndare
+usersettings.admin = Anv\u00e4ndare \u00e4r administrat\u00f6r
+usersettings.settings = Anv\u00e4ndare \u00e4r till\u00e5ten att \u00e4ndra inst\u00e4llningar och l\u00f6senord
+usersettings.stream = Anv\u00e4ndare \u00e4r till\u00e5ten att spela filer
+usersettings.jukebox = Anv\u00e4ndare \u00e4r till\u00e5ten att spela filer i Jukebox mode
+usersettings.download = Anv\u00e4ndare till\u00e5ts ladda ner filer
+usersettings.upload = Anv\u00e4ndare till\u00e5ts ladda upp filer
+usersettings.playlist= Anv\u00e4ndare till\u00e5ts skapa och tabort spellistor
+usersettings.coverart = Anv\u00e4ndare till\u00e5ts att \u00e4ndra omslagsbild och taggar
+usersettings.comment= Anv\u00e4ndare till\u00e5ts att skapa och editera kommentarer och ratings
+usersettings.podcast= Anv\u00e4ndare till\u00e5ts att administrera Podcasts
+usersettings.username = Anv\u00e4ndarnamn
+usersettings.changepassword = Byt l\u00f6senord
+usersettings.password = L\u00f6senord
+usersettings.newpassword = Nytt L\u00f6senord
+usersettings.confirmpassword = Bekr\u00e4fta l\u00f6senord
+usersettings.delete = Tabort anv\u00e4ndare
+usersettings.ldap = Autentisera anv\u00e4ndare via LDAP
+usersettings.nousername = Saknas anv\u00e4ndarnamn.
+usersettings.useralreadyexists = Anv\u00e4ndare finns redan.
+usersettings.nopassword = L\u00f6senord kr\u00e4vs.
+usersettings.wrongpassword = L\u00f6senordet matchade inte.
+usersettings.ldapdisabled = LDAP autentisering \u00e4r inte aktiverat. Kontrollera Avancerade inst\u00e4llningar.
+usersettings.passwordnotsupportedforldap = Kan inte l\u00e4sa eller skriva l\u00f6senord f\u00f6r LDAP-autentiserade anv\u00e4ndare.
+usersettings.ok = L\u00f6senordsbytet lyckades f\u00f6r anv\u00e4ndare {0}.
+
+# searchSettings.jsp
+searchsettings.auto = Uppdatera s\u00f6kindex med automatik
+searchsettings.manual = Uppdatera s\u00f6kindex nu.
+searchsettings.interval.never = Aldrig
+searchsettings.interval.one = Varje dag
+searchsettings.interval.many = Var {0}:e dag
+searchsettings.hour = vid {0}:00
+searchsettings.text = S\u00f6kindex skapas just nu och det kan ta flera minuter beroende \
+ p\u00e5 storleken p\u00e5 ditt mediabibliotek.<br>Du kan fortfarande anv\u00e4nda {0} under tiden som s\u00f6kindexet skapas.
+
+# main.jsp
+main.up = Upp
+main.playall = Spela alla
+main.playrandom = Spela slumpvis
+main.addall = L\u00e4gg till alla
+main.tags = \u00c4ndra Taggar
+main.playcount = Spelad {0} g\u00e5nger.
+main.lastplayed = Senast spelad {0}.
+main.comment = Kommentera
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Creates a line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>Creates a new paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/</td><td>Creates an external link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em"> </td><td> </td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">Donera</a> till Subsonic!<br>(och tabort den h\u00e4r reklamen)
+main.nowplaying = Nu spelas
+main.lyrics = L\u00e5ttext
+main.minutesago = minuter sedan
+main.chat = Chatmeddelanden
+main.message = Skriv ett meddelande
+main.clearchat = Rensa meddelanden
+
+# rating.jsp
+rating.rating = Rankad
+rating.clearrating = T\u00f6m rankning
+
+# coverArt.jsp
+coverart.change = Byt
+coverart.zoom = Zoom
+
+# allmusic.jsp
+allmusic.text = S\u00f6ker efter album <em>{0}</em> hos allmusic.com - V\u00e4nligen v\u00e4nta.
+
+# changeCoverArt.jsp
+changecoverart.title = Byt omslagsbild
+changecoverart.address = Skriv adressen till omslagsbilden
+changecoverart.artist = Artister
+changecoverart.album = Album
+changecoverart.search = Google Image Search
+changecoverart.wait = V\u00e4nligen v\u00e4nta...
+changecoverart.success = Nedladdningen av omslagsbilden lyckades.
+changecoverart.error = Misslyckades att ladda ner omslagsbilden.
+changecoverart.noimagesfound = Hittade inget omslag.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = Misslyckades att byta omslagsbild:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = Editera taggar
+edittags.file = Fil
+edittags.track = Sp\u00e5r
+edittags.songtitle = Titel
+edittags.artist = Artist
+edittags.album = Album
+edittags.year = \u00c5r
+edittags.genre = Genre
+edittags.status = Status
+edittags.suggest = F\u00f6resl\u00e5
+edittags.reset = T\u00f6m
+edittags.suggest.short = F
+edittags.reset.short = T
+edittags.set = S\u00e4tt
+edittags.working = Arbetar
+edittags.updated = Uppdaterade
+edittags.skipped = Skippade
+edittags.error = Error
+
+# share.jsp
+share.title = Dela
+share.warning = <h2>VIKTIG NOTERING!</h2><p>Play fair &ndash; Dela inte ut filer som du inte har r\u00e4ttigheterna till.</p>
+share.facebook = Dela p\u00e5 Facebook
+share.twitter = Dela p\u00e5 Twitter
+share.googleplus = Dela p\u00e5 Google+
+share.link = Eller dela genom att skicka denna l\u00e4nk: <a href="{0}" target="_blank">{0}</a>
+share.disabled = F\u00f6r att kunna dela med dig m\u00e5ste du f\u00f6rst registrera din egna <em>subsonic.org</em> adress.<br> \
+ V\u00e4nligen g\u00e5 till <a href="networkSettings.view"><b>Inst\u00e4llningar &gt; N\u00e4tverk</b></a> (adminr\u00e4ttigheter kr\u00e4vs).
+share.manage = Hantera min delade media
+
+# donate.jsp
+donate.title = Donera
+donate.invalidlicense = Ogiltig licensnyckel.
+donate.amount = Donear {0}
+
+donate.textbefore = <p>Tack f\u00f6r att du \u00f6verv\u00e4ger att donera f\u00f6r att st\u00f6dja detta {0} projekt! \
+ Donerare f\u00e5r tillg\u00e5ng till premiumfunktioner s\u00e5som:</p> \
+ <ul> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> f\u00f6r Android, iPhone och Windows Phone*.</li> \
+ <li><a href="http://subsonic.org/pages/apps.jsp" target="blank">Apps</a> f\u00f6r PlayBook, Roku, Mac, Chrome och fler*.</li> \
+ <li>Videostreaming.</li> \
+ <li>Din personliga serveradress: <em>ditnamn</em>.subsonic.org (se <a href="networkSettings.view">Inst\u00e4llningar &gt; N\u00e4tverk</a>).</li> \
+ <li>Dela din media p\u00e5 Facebook, Twitter, Google+.</li> \
+ <li>Ingen reklam p\u00e5 webbsidan.</li> \
+ <li>Andra funktioner som kommer i framtiden.</li> \
+ </ul> \
+ <p style="font-size:9px;">* En del appar s\u00e4ljs av 3:e parts utvecklare.</p>\
+ <p>Som donerare kommer du att f\u00e5 en licensnyckel som \u00e4r f\u00f6r personligt bruk, icke-kommersiellt bruk, \
+ och alla framtida versioner av {0}. F\u00f6r kommersiellt bruk, v\u00e4nligen <a href="mailto:subsonic_donation@activeobjects.no">kontakta</a> oss f\u00f6r en licensieringstillval.</p> \
+ <p>Den f\u00f6reslagna bidragssumman \u00e4r <b>&euro;20</b>, men du kan v\u00e4lja vilken summa du vill:</p>
+donate.textafter = <p>Klicka p\u00e5 knappen f\u00f6r att g\u00e5 till Paypal d\u00e4r du kan betala med hj\u00e4lp av ett kreditkort eller anv\u00e4nda \
+ ditt PayPalkonto (om du har ett). Du kommer att erh\u00e5lla din licensnyckel inom ett par minuter.</p> \
+ <p>Om du har n\u00e5gra fr\u00e5gor, v\u00e4nligen skicka ett email till \
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = Denna version {2} blev licensierad till {0} den {1}. Tack f\u00f6r ditt bidrag!
+donate.register = Efter att du f\u00e5tt din licensnyckel, v\u00e4nligen registrera den nedan.
+donate.resend = Redan k\u00f6pt en licens men f\u00f6rlagt registreringsnyckeln? <a href="http://subsonic.org/backend/requestLicense.view" target="_blank">Skicka den till mig igen</a>.
+donate.register.email = Email
+donate.register.license = Licens
+
+# podcastReceiver.jsp
+podcastreceiver.title = Podcast mottagare
+podcastreceiver.expandall = Visa avsnitt
+podcastreceiver.collapseall = D\u00f6lj avsnitt
+podcastreceiver.status.new = Nya
+podcastreceiver.status.downloading = Laddar ner
+podcastreceiver.status.completed = Avslutad
+podcastreceiver.status.error = Error
+podcastreceiver.status.deleted = Borttagen
+podcastreceiver.status.skipped = Skippade
+podcastreceiver.downloadselected= Ladda ner valda
+podcastreceiver.deleteselected= Tabort valda
+podcastreceiver.confirmdelete= Vill du verkligen tabort valda Podcasts?
+podcastreceiver.check = S\u00f6k efter nya avsnitt
+podcastreceiver.refresh = Uppdatera sidan
+podcastreceiver.settings = Podcast inst\u00e4llningar
+podcastreceiver.subscribe = Prenummerera p\u00e5 Podcast
+
+# lyrics.jsp
+lyrics.title = L\u00e5ttext
+lyrics.artist = Artist
+lyrics.song = Melodi
+lyrics.search = S\u00f6k
+lyrics.wait = S\u00f6ker efter text v\u00e4nligen v\u00e4nta...
+lyrics.courtesy = (Lyrik av <a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>)
+lyrics.nolyricsfound = Ingen text funnen.
+
+# helpPopup.jsp
+helppopup.title = Subsonic Hj\u00e4lp
+helppopup.cover.title = Omslagsbildens storlek
+helppopup.cover.text = <p>L\u00e5ter dig specificera storleken p\u00e5 den visade omslagsbilden, med valet att st\u00e4nga av den helt.</p>
+helppopup.transcode.title = Max bandbredd
+helppopup.transcode.text = <p>Om du har en begr\u00e4nsad bandbredd kan du s\u00e4tta en \u00f6vre gr\u00e4ns f\u00f6r bandbredden p\u00e5 st\u00f6mmen. \
+ Tex. om du har en MP3 fil som \u00e4r kodad i 256 kbps (kilobits per second), men maximal bandbredd st\u00e4lld till \
+ 128 Kbps kommer Subsonic automatiskt att g\u00f6ra om filen fr\u00e5n 256 till 128 Kbps.</p> \
+ <p>Det h\u00e4r valet kr\u00e4ver att LAME \u00e4r installerat. LAME <a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \u00e4r en open source mp3 kodare. Du kan <a target="_blank" href="http://subsonic.org/pages/transcoding.jsp/">ladda ner den h\u00e4r</a>. \
+ Se till att installera den i SUBSONIC_HOME/transcode, eller ett bibliotek som finns med i milj\u00f6variabeln PATH.</p>
+helppopup.playlistfolder.title = S\u00f6kv\u00e4g till spellistor
+helppopup.playlistfolder.text = <p>L\u00e5ter dig specificera i vilken mapp spellistorna sparas.</p>
+helppopup.musicmask.title = Musik typer
+helppopup.musicmask.text = <p>L\u00e5ter dig specificera vilka filtyper som ska k\u00e4nnas igen som musik n\u00e4r Subsonic s\u00f6ker igenom dina musikfoldrar.</p>
+helppopup.coverartmask.title = Omslagsbilds typer
+helppopup.coverartmask.text = <p>L\u00e5ter dig specificera vilka filtyper som ska k\u00e4nnas igen som omslagsbilder n\u00e4r Subsonic s\u00f6ker igenom dina musikfoldrar.</p>
+helppopup.downsamplecommand.title = Downsample kommando
+helppopup.downsamplecommand.text = <p>L\u00e5ter dig specificera vilket komando somska k\u00f6ras f\u00f6r att s\u00e4nka bandbredd p\u00e5 str\u00f6mmen.</p>\
+ <p>(%s = Vilken fil som ska \u00e4ndras, %b = Max bandbredd p\u00e5 spelaren)</p>
+helppopup.index.title = Index
+helppopup.index.text = <p>L\u00e5ter dig specificera hur Indexet ska skapas (det du ser \u00f6verst p\u00e5 sk\u00e4rmen). Filer och bibliotek \
+ Kan snabbt hittas genom Indexet.</p> \
+ <p>Anv\u00e4nd mellanslags separering mellan de olika index entrys. Normalt, best\u00e5r ett entry av varje enskilda bokstav, \
+ Men du kan ocks\u00e5 anv\u00e4nda fler tecken. Tex entryt <em>The</em> kommer att l\u00e4nkas till alla filer och foldrar \
+ som b\u00f6rjar med "The".</p> \
+ <p>Du kan ocks\u00e5 skapa ett entry som best\u00e5r av en grupp tecken genom att anv\u00e4nda parantes. Tex Entryt \
+ <em>A-E(ABCDE)</em> komemr att visas som <em>A-E</em> och l\u00e4nkas till alla fielr och foldrar som b\u00f6rjar med n\u00e5got av tecknen \
+ A, B, C, D eller E. Det kan vara anv\u00e4nbart n\u00e4r man vill gruppera mindre vanligt f\u00f6rekommande tecken (som tex \u00c5, \u00c4 och \u00d6), eller \
+ f\u00f6r att gruppera tecken med accent (som tex A, \u00c0 och \u00c1)</p> \
+ <p>Filer och foldrar som b\u00f6rjar med en siffra kommer att indexeras under entryt "#".</p>
+helppopup.ignoredarticles.title = Articles to ignore
+helppopup.ignoredarticles.text = <p>L\u00e5ter dig specificera vilka etiketter som ska ignoreras i indexeringen (exmpelvis "The").</p>
+helppopup.shortcuts.title = Genv\u00e4gar
+helppopup.shortcuts.text = <p>En mellanslags sepparerad lista av de senaste foldrarna att skapa genv\u00e4gat till. Anv\u00e4nd citationstecken f\u00f6r att gruppera</p> \
+ <p>Tex: <em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = Spr\u00e5k
+helppopup.language.text = <p>L\u00e5ter dig v\u00e4lja vilket spr\u00e5k du ska anv\u00e4nda.</p>
+helppopup.visibility.title = Visning
+helppopup.visibility.text = <p>V\u00e4lj vilka detaljer som ska visas vid varje spelad melodi, \u00e4ven rubrik brytning. Det \u00e4r det maximala antalet tecken \
+ Som visas f\u00f6r varje melodi, album eller artist.</p>
+helppopup.partymode.title = Party l\u00e4ge
+helppopup.partymode.text = <p>N\u00e4r Party l\u00e4get \u00e4r aktivt, kommer anv\u00e4ndaresn gr\u00e4nsnitt att f\u00f6renklas f\u00f6r att l\u00e4ttar kunna anv\u00e4ndas av mindre erfarna anv\u00e4ndare. \
+ Som att tex. undvika att spellistor av misstag raderas.</p>
+helppopup.theme.title = Tema
+helppopup.theme.text = <p>L\u00e5ter dig v\u00e4lja vilket tema som ska anv\u00e4ndas. Ett tema definerar hur Subsonic ska se ut och k\u00e4nnas i termer av f\u00e4rger, typsnitt, bilder etc.</p>
+helppopup.welcomemessage.title = V\u00e4lkomstmeddelande
+helppopup.welcomemessage.text = <p>Det h\u00e4r meddelandet visas p\u00e5 f\u00f6rstasidan.</p>
+helppopup.coverartlimit.title = Omslagsbilds gr\u00e4ns
+helppopup.coverartlimit.text = <p>V\u00e4ljer det maximala antalet omslag att visas p\u00e5 en sida.</p>
+helppopup.downloadlimit.title = Nedladdningsgr\u00e4ns
+helppopup.downloadlimit.text = <p>En maxgr\u00e4ns f\u00f6r bandbredden som ska f\u00e5 anv\u00e4ndas vid nedladdning.</p>
+helppopup.uploadlimit.title = Uppladdningsgr\u00e4ns
+helppopup.uploadlimit.text = <p>En maxgr\u00e4ns f\u00f6r bandbredden som ska f\u00e5 anv\u00e4ndas vid uppladdning.</p>
+helppopup.streamport.title = Non-SSL str\u00f6mnings port
+helppopup.streamport.text = <p>Det h\u00e4r valet \u00e4r bara intressant om din server \u00e4nv\u00e4nder SSL med Subsonic (HTTPS).</p><p>En del spelare \
+ (ex.Winamp) klarar inte str\u00f6mmar via SSL. Specificera den port som anv\u00e4nds f\u00f6r vanligt HTTP (vanligen 80 \
+ eller 4040) om du inte vill att str\u00f6men ska g\u00e5 via SSL. Notera att str\u00f6mmen kommer inte att vara krypterad.</p>
+helppopup.ldap.title = LDAP autentisering
+helppopup.ldap.text = <p>Anv\u00e4ndare kan autentisera via en extern LDAP server (\u00e4ven Windows Active Directory). \
+ n\u00e4r LDAP-aktiverade anv\u00e4ndare loggar p\u00e5 Subsonic, kollas anv\u00e4ndare ID och l\u00f6senord av den externa LDAP servern inte Subsonic.</p>
+helppopup.ldapurl.title = LDAP URL
+helppopup.ldapurl.text = <p>URLtill LDAP servern. Protokolelt moste vara n\u00e5got av <em>ldap://</em> eller <em>ldaps://</em> \
+ (LDAP \u00f6ver SSL). L\u00e4s <a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">h\u00e4r</a> \
+ f\u00f6r en mer detaljerad beskrivning.</p>
+helppopup.ldapsearchfilter.title = LDAP s\u00f6k filter
+helppopup.ldapsearchfilter.text = <p>S\u00f6k filtret anger vilket attribut som ska matchas i anv\u00e4ndars\u00f6kningen. \
+ (definerat i <a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ "'{0'}" ers\u00e4tts av anv\u00e4ndarnamnet, Tex: \
+ <ul>\
+ <li>(uid='{0'}) - S\u00f6ker efter en anv\u00e4ndare med som matchas med attributet uid.</li> \
+ <li>(sAMAccountName='{0'}) - Anv\u00e4nds f\u00f6r att autentisera med Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP anv\u00e4ndare DN
+helppopup.ldapmanagerdn.text = <p>Om LDAP servern in till\u00e5ter anonym s\u00f6kningm\u00e5ste du speficera DN p\u00e5 den anv\u00e4ndare som till\u00e5ts \
+ ( DN = <em>Distinguished Name</em>) och l\u00f6senord.</p>
+helppopup.ldapautoshadowing.title = Skapa A tomatiskt LDAP anv\u00e4ndare i Subsonic
+helppopup.ldapautoshadowing.text = <p>Med det h\u00e4r valet aktiverat, beh\u00f6ver du inte att manuellt skapa LDAP konton i Subsonic innan inloggning.</p> \
+ <p>OBS! Detta val inneb\u00e4r att alla anv\u00e4ndare med ett aktivt LDAP konto kan logga in i Subsonic, \
+ \u00e4r det det du vill?</p>
+helppopup.playername.title = Spelarensnamn
+helppopup.playername.text = <p>L\u00e5ter dig specificera ett namn som \u00e4r l\u00e4tt att komma ih\u00e5g p\u00e5 den spelare, som tex "Jobb" eller "Vardagsrum".</p>
+helppopup.autocontrol.title = Kontrollera spelare automatiskt
+helppopup.autocontrol.text = <p>Med det h\u00e4r valet aktivt, kommer Subsonic automatiskt att starta spelaren n\u00e4r du klickar p\u00e5 "Spela" \
+ i spellistan. Annars m\u00e5ste du starta spelaren manuellt.</p>
+helppopup.dynamicip.title = Dynamisk IP address
+helppopup.dynamicip.text = <p>St\u00e4ng av detta val om spelaren har en fast IP address.</p>
+
+# wap/index.jsp
+wap.index.missing = Ingen musik funnen
+wap.index.playlist = Spellista
+wap.index.search = S\u00f6k
+wap.index.settings = Inst\u00e4llningar
+
+# wap/browse.jsp
+wap.browse.playone = Spela l\u00e5t
+wap.browse.playall = Spela alla
+wap.browse.addone = L\u00e4ggtill
+wap.browse.addall = L\u00e4ggtill alla
+wap.browse.downloadone = Ladda ner l\u00e5t
+wap.browse.downloadall = Ladda ner alla
+
+# wap/playlist.jsp
+wap.playlist.title = Spellista
+wap.playlist.noplayer = Ingen spelare vald
+wap.playlist.clear = Rensa
+wap.playlist.load = Ladda
+wap.playlist.random = Blanda
+wap.playlist.play = Spela p\u00e5 telefonen
+
+# wap/search.jsp
+wap.search.title = S\u00f6k
+
+# wap/searchResult.jsp
+wap.searchresult.index = S\u00f6k indexet uppdateras, prova senare.
+
+# wap/settings.jsp
+wap.settings.selectplayer = V\u00e4lj spelare
+wap.settings.allplayers = Alla
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_CN.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_CN.properties
new file mode 100644
index 00000000..d05f405e
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_CN.properties
@@ -0,0 +1,94 @@
+#
+# Simplified Chinese localization.
+# Author: Neil Gao (neilgaojh@hotmail.com)
+#
+
+common.home = \u9996\u9875
+common.back = \u540e\u9000
+common.help = \u5e2e\u52a9
+common.play = \u64ad\u653e
+common.add = \u6dfb\u52a0
+common.download = \u4e0b\u8f7d
+common.close = \u5173\u95ed
+common.refresh = \u5237\u65b0
+common.next = \u4e0b\u4e00\u9996
+common.previous = \u4e0a\u4e00\u9996
+common.more = \u66f4\u591a
+common.ok = \u786e\u5b9a
+common.save = \u4fdd\u5b58
+common.create = \u521b\u5efa
+common.delete = \u5220\u9664
+common.unknown = (\u672a\u77e5)
+common.default = (\u9ed8\u8ba4)
+
+# login.jsp
+login.username = \u7528\u6237\u540d
+login.password = \u5bc6\u7801
+login.login = \u767b\u5f55
+login.remember = \u8bb0\u4f4f\u6211
+login.logout = \u60a8\u73b0\u5728\u5df2\u7ecf\u9000\u51fa.
+login.error = \u9519\u8bef\u7684\u7528\u6237\u540d\u6216\u5bc6\u7801.
+
+# top.jsp
+top.home = \u9996\u9875
+top.now_playing =\u64ad\u653e
+top.settings = \u8bbe\u7f6e
+top.status = \u72b6\u6001
+top.more = \u66f4\u591a
+top.help = \u5e2e\u52a9
+top.search = \u641c\u7d22
+top.upgrade = <b>Note!</b> A new version is available.<br>Download {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">here</a>.
+top.missing = \u6ca1\u6709\u53d1\u73b0\u97f3\u4e50\uff0c\u8bf7\u4fee\u6539\u8bbe\u7f6e.
+top.logout = \u9000\u51fa {0}
+
+# playlist.jsp
+playlist.stop = \u505c\u6b62
+playlist.start = \u64ad\u653e
+playlist.clear = \u6e05\u9664
+playlist.shuffle = Shuffle
+playlist.repeat_on = \u91cd\u653e\u662f\u6253\u5f00\u7684
+playlist.repeat_off = \u91cd\u653e\u662f\u5173\u95ed\u7684
+playlist.undo = \u64a4\u6d88
+playlist.load = \u8f7d\u5165
+playlist.save = \u4fdd\u5b58
+playlist.remove = \u5220\u9664
+playlist.up = Up
+playlist.down = Down
+playlist.empty = \u64ad\u653e\u5217\u8868\u4e3a\u7a7a
+
+# home.jsp
+home.random.title = \u968f\u673a
+home.newest.title = \u6700\u65b0
+home.highest.title = \u6700\u9ad8\u8bc4\u4ef7
+home.frequent.title = \u64ad\u653e\u9891\u7387\u6700\u9ad8
+home.recent.title = \u6700\u8fd1\u64ad\u653e\u7684
+home.users.title = \u7528\u6237
+home.random.text = \u968f\u673a\u7684\u6b4c\u96c6
+home.newest.text = \u6700\u8fd1\u6dfb\u52a0\u6216\u4fee\u6539\u7684\u6b4c\u96c6
+home.highest.text = \u8bc4\u4ef7\u6700\u9ad8\u7684\u6b4c\u96c6
+home.frequent.text = \u64ad\u653e\u9891\u7387\u6700\u9ad8\u7684\u6b4c\u96c6
+home.recent.text = \u6700\u8fd1\u64ad\u653e\u7684\u6b4c\u96c6
+home.users.text = \u7528\u6237\u7edf\u8ba1
+home.scan = The music folder is currently being scanned. All features are not yet available.
+home.listsize = \u6bcf\u9875 {0} \u6b4c\u96c6
+home.albums = \u6b4c\u96c6 {0} - {1}
+home.playcount = Played {0} times
+home.lastplayed = Played {0}
+home.created = \u4fee\u6539 {0}
+home.chart.total = \u5168\u90e8 (MB)
+home.chart.stream = Streamed (MB)
+home.chart.download = \u4e0b\u8f7d (MB)
+home.chart.upload = \u4e0a\u4f20 (MB)
+
+# settingsHeader.jsp
+settingsheader.title = \u8bbe\u7f6e
+settingsheader.general = \u4e00\u822c
+settingsheader.personal = \u5916\u89c2
+settingsheader.musicFolder = \u97f3\u4e50\u6587\u4ef6\u5939
+settingsheader.internetRadio = \u7f51\u4e0a\u7535\u89c6\u020f\u6536\u97f3\u673as
+settingsheader.player = \u6f14\u5531\u8005
+settingsheader.transcoding = \u8f6c\u7801
+settingsheader.user = \u7528\u6237
+settingsheader.search = \u641c\u7d22
+settingsheader.password = \u5bc6\u7801
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_TW.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_TW.properties
new file mode 100644
index 00000000..4a3193fd
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_zh_TW.properties
@@ -0,0 +1,677 @@
+#
+# Taiwan localization.
+# Author: Cheng Jen Li
+# chengjen.li@gmail.com
+#
+
+common.home = \u9996\u9801
+common.back = \u56de\u5fa9
+common.help = \u5354\u52a9
+common.play = \u64ad\u653e
+common.add = \u65b0\u589e
+common.download = \u4e0b\u8f09
+common.close = \u95dc\u9589
+common.refresh = \u5237\u65b0
+common.next = \u4e0b\u9801
+common.previous = \u4e0a\u9801
+common.more = \u66f4\u591a
+common.ok = \u78ba\u5b9a
+common.cancel = \u53d6\u6d88
+common.save = \u5132\u5b58
+common.create = \u5efa\u7acb
+common.delete = \u522a\u9664
+common.unknown = (\u672a\u77e5)
+common.default = (\u9810\u8a2d\u503c)
+
+# login.jsp
+login.username = \u5e33\u865f
+login.password = \u5bc6\u78bc
+login.login = \u767b\u5165
+login.remember = \u8a18\u5f97\u6211
+login.logout = \u60a8\u5df2\u7d93\u767b\u51fa.
+login.error = \u5e33\u865f\u6216\u662f\u5bc6\u78bc\u932f\u8aa4.
+login.insecure = {0} \u672a\u53d7\u4fdd\u8b77. \u8acb\u5148\u4ee5\u4f7f\u7528\u8005\u53ca\u5bc6\u78bc "admin"\u767b\u5165, \u6216\u9ede\u64ca <a href="login.view?user=admin&amp;password=admin">\u9019\u88e1</a>. \u4e26\u4e14\u7acb\u523b\u8b8a\u66f4\u60a8\u7684\u5bc6\u78bc.
+
+# accessDenied.jsp
+accessDenied.title = \u62d2\u7d55\u5b58\u53d6
+accessDenied.text = \u62b1\u6b49\uff0c\u60a8\u7121\u6b0a\u57f7\u884c\u6240\u8acb\u6c42\u7684\u64cd\u4f5c.
+
+# top.jsp
+top.home = \u9996\u9801
+top.now_playing = \u64ad\u653e
+top.settings = \u8a2d\u5b9a
+top.status = \u72c0\u614b
+top.podcast = \u64ad\u5ba2
+top.more = \u5176\u4ed6
+top.help = \u95dc\u65bc
+top.search = \u641c\u5c0b
+top.upgrade = <b>\u6ce8\u610f!</b> \u6709\u65b0\u7248\u672c\u63d0\u4f9b.<br>\u4e0b\u8f09 {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u9019\u88e1</a>.
+top.missing = \u627e\u4e0d\u5230\u97f3\u6a02\u593e\uff0c\u8acb\u91cd\u65b0\u8a2d\u5b9a.
+top.logout =\u767b\u51fa{0}
+
+# left.jsp
+left.statistics = \u97f3\u6a02\u76d2\u6709 \
+ {0}&nbsp;\u4f4d\u6b4c\u624b<br>\
+ {1}&nbsp;\u5f35\u5c08\u8f2f<br>\
+ {2}&nbsp;\u9996\u6b4c\u66f2<br>\
+ {3} (\u7d04 {4} \u5c0f\u6642)
+left.shortcut = \u6377\u5f91
+left.radio = \u7dda\u4e0a\u96fb\u8996/\u6536\u97f3\u6a5f
+left.allfolders = \u5168\u90e8
+
+# playlist.jsp
+playlist.stop = \u505c\u6b62
+playlist.start = \u64ad\u653e
+playlist.confirmclear = \u78ba\u5b9a\u522a\u9664\u9ede\u64ad\u6e05\u55ae?
+playlist.clear = \u6e05\u9664
+playlist.shuffle = \u96a8\u8208\u64ad\u653e
+playlist.repeat_on = \u91cd\u64ad
+playlist.repeat_off = \u4e0d\u91cd\u64ad
+playlist.undo = \u53d6\u6d88
+playlist.settings = \u8a2d\u5b9a
+playlist.more = \u5176\u4ed6...
+playlist.more.playlist = \u9ede\u64ad\u6e05\u55ae
+playlist.more.sortbytrack = \u97f3\u8ecc\u6392\u5e8f
+playlist.more.sortbyartist = \u6b4c\u624b\u6392\u5e8f
+playlist.more.sortbyalbum = \u5c08\u8f2f\u6392\u5e8f
+playlist.more.selection = \u9078\u6b4c
+playlist.more.selectall = \u5168\u9078
+playlist.more.selectnone = \u5168\u4e0d\u9078
+playlist.getflash = \u53d6\u5f97Flash\u64a5\u653e\u5668
+playlist.load = \u8f09\u5165
+playlist.save = \u5132\u5b58
+playlist.append = \u52a0\u5165\u6e05\u55ae
+playlist.remove = \u79fb\u9664
+playlist.up = \u4e0a
+playlist.down = \u4e0b
+playlist.empty = \u7a7a\u7684\u9ede\u64ad\u6e05\u55ae
+
+# status.jsp
+status.title = \u72c0\u614b
+status.type = \u5f62\u5f0f
+status.stream = \u4e32\u6d41
+status.download = \u4e0b\u8f09
+status.upload = \u4e0a\u50b3
+status.player = \u64a5\u653e\u5668
+status.user = \u4f7f\u7528\u8005
+status.current = \u76ee\u524d\u64ad\u653e
+status.transmitted = \u50b3\u8f38
+status.bitrate = Bitrate (Kbps)
+
+# search.jsp
+search.title = \u641c\u5c0b
+search.search = \u641c\u5c0b
+search.index = \u6b63\u5728\u5efa\u7acb\u7d22\u5f15\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66!
+search.hits.none = \u627e\u4e0d\u5230.
+
+# gettingStarted.jsp
+gettingStarted.title = \u521d\u6b21\u4f7f\u7528
+gettingStarted.text = <p>\u6b61\u8fce\u4f7f\u7528Subsonic!<br>\u8acb\u4f9d\u7167\u4e0b\u9762\u7684\u6b65\u9a5f\u8a2d\u5b9a.<br> \
+ \u9ede\u64ca\u5de5\u5177\u5217\u4e0a\u7684"\u9996\u9801"\uff0c\u5c31\u80fd\u96a8\u6642\u56de\u4f86\u9019\u88e1\uff01.</p> \
+ <p>\u9700\u8981\u66f4\u591a\u8a0a\u606f\u8acb\u53c3\u8003<a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\u5165\u9580\u6307\u5357</b></a></p>
+gettingStarted.step1.title = \u8b8a\u66f4\u7ba1\u7406\u54e1\u7684\u5bc6\u78bc.
+gettingStarted.step1.text = \u8acb\u4fee\u6539\u9810\u8a2d\u7684\u7ba1\u7406\u54e1\u5bc6\u78bc\uff0c\u4ee5\u78ba\u4fdd\u4f3a\u670d\u5668\u7684\u5b89\u5168\u3002\
+ \u4e5f\u53ef\u4ee5\u5efa\u7acb\u65b0\u7684\u4f7f\u7528\u8005\u4e26\u7d66\u4e88\u4e0d\u540c\u7684\u6b0a\u9650
+gettingStarted.step2.title = \u8a2d\u5b9a\u97f3\u6a02\u593e.
+gettingStarted.step2.text = \u8a2d\u5b9a\u60a8\u97f3\u6a02\u6240\u653e\u7f6e\u7684\u8cc7\u6599\u593e\u3002
+gettingStarted.step3.title = \u914d\u7f6e\u7db2\u8def\u8a2d\u5b9a.
+gettingStarted.step3.text = \u5982\u679c\u60a8\u8981\u900f\u904e\u7db2\u969b\u7db2\u8def\u6216\u662f\u8207\u60a8\u7684\u670b\u53cb\u3001\u5bb6\u4eba\u5206\u4eab\u3002\
+ \u4e26\u53d6\u5f97\u60a8\u7684\u5c08\u5c6c\u7db2\u5740\u50cf\u662f<em>yourname</em>.subsonic.org.
+gettingStarted.hide = \u4e0b\u6b21\u4e0d\u986f\u793a\u672c\u9801\uff01
+gettingStarted.hidealert = \u5982\u679c\u4e0b\u6b21\u9084\u9700\u8981\u986f\u793a\u63d0\u793a,\u8acb\u5f9e \u8a2d\u5b9a->\u4e00\u822c \u4e2d\u52fe\u9078.
+
+# home.jsp
+home.random.title = \u96a8\u8208\u64ad\u653e
+home.newest.title = \u6700\u65b0\u97f3\u6a02
+home.highest.title = \u559c\u597d\u7a0b\u5ea6
+home.frequent.title = \u9ede\u64ad\u7387
+home.recent.title = \u6700\u8fd1\u64ad\u653e
+home.users.title = \u4f7f\u7528\u8005
+home.random.text = \u96a8\u8208\u9078\u64ad\u5c08\u8f2f
+home.newest.text = \u6700\u65b0\u5c08\u8f2f
+home.highest.text = \u8a55\u50f9\u9ad8\u7684\u5c08\u8f2f
+home.frequent.text = \u6700\u591a\u9ede\u64ad\u5c08\u8f2f
+home.recent.text =\u6700\u8fd1\u64ad\u653e\u5c08\u8f2f
+home.users.text = \u4f7f\u7528\u8005\u7d71\u8a08
+home.scan = \u97f3\u6a02\u593e\u5df2\u7d93\u6383\u63cf. \u6240\u6709\u529f\u80fd\u5c1a\u672a\u958b\u653e.
+home.listsize = \u6bcf\u9801 {0} \u5f35\u5c08\u8f2f
+home.albums = \u5c08\u8f2f {0} - {1}
+home.playcount = \u64ad\u653e\u4e86 {0} \u9996\u6b4c
+home.lastplayed = \u64ad\u653e {0}
+home.created = \u5efa\u7acb {0}
+home.chart.total = \u7e3d\u8a08 (MB)
+home.chart.stream = \u4e32\u6d41 (MB)
+home.chart.download = \u5df2\u4e0b\u8f09(MB)
+home.chart.upload = \u5df2\u4e0a\u50b3 (MB)
+
+# more.jsp
+more.title = \u5176\u4ed6
+more.random.title = \u96a8\u8208\u64ad\u653e
+more.random.text = \u5efa\u7acb\u96a8\u8208\u6e05\u55ae
+more.random.songs = {0} \u9996
+more.random.auto = \u7576\u9ede\u64ad\u6e05\u55ae\u64ad\u653e\u5b8c\u7562\u6642\u7e7c\u7e8c\u64ad\u653e\u66f4\u591a\u7684\u96a8\u8208\u6b4c\u66f2 .
+more.random.ok = \u78ba\u5b9a
+more.random.genre = \u66f2\u98a8
+more.random.anygenre = \u4efb\u610f
+more.random.year = \u5e74\u4efd
+more.random.anyyear = \u4efb\u610f
+more.random.folder = \u97f3\u6a02\u593e
+more.random.anyfolder = \u4efb\u610f
+more.apps.title = Subsonic Apps
+more.apps.text = <p>\u652f\u63f4 <b>iPhone</b>, \
+ <b>Android</b> \u548c <b>AIR</b>\u7684<a href="http://subsonic.org/pages/apps.jsp" target="_blank">Subsonic apps</a>.</p>
+more.mobile.title = \u79fb\u52d5\u8a2d\u5099
+more.mobile.text = <p>\u53ef\u4ee5\u7531WAP\u96fb\u8a71\u6216PDA\u4f7f\u7528 {0} .<br> \
+ \u7d93\u7531\u624b\u6a5f\u8f38\u5165\u9019\u6a23\u7684\u7db2\u5740 <b>http://yourhostname/wap</b></p> \
+ <p>\u7576\u7136\u60a8\u624b\u6a5f\u5fc5\u9808\u8981\u6709\u4e0a\u7db2\u529f\u80fd!.</p>
+more.podcast.title = \u64ad\u5ba2
+more.podcast.text = <p>\u5132\u5b58\u9ede\u64ad\u6e05\u55ae\u7576\u6210\u64ad\u5ba2.<br>\
+ \u7d93\u7531\u9019\u500b\u7db2\u5740\u53ef\u4ee5\u6536\u807d: <b>http://yourhostname/podcast</b>, \
+ \u6216\u662f <b><a href="podcast.view?suffix=.rss">\u9ede\u6211</a>.</b></p>
+more.upload.title = \u4e0a\u50b3\u6a94\u6848
+more.upload.source = \u9078\u64c7\u6a94\u6848
+more.upload.target = \u4e0a\u50b3\u5230
+more.upload.browse = \u9078\u64c7
+more.upload.ok = \u4e0a\u50b3
+more.upload.unzip = \u81ea\u52d5\u89e3\u58d3\u7e2ezip\u6a94.
+more.upload.progress = % \u5b8c\u6210. \u8acb\u7a0d\u5019...
+
+# upload.jsp
+upload.title = \u6a94\u6848\u4e0a\u50b3\u4e2d......
+upload.success = \u4e0a\u50b3\u6210\u529f <b>{0}</b>
+upload.empty = \u6c92\u6709\u4e0a\u50b3\u7684\u6a94\u6848\u5594.
+upload.failed = \u6a94\u6848\u4e0a\u50b3\u5931\u6557\u56e0\u70ba:<br><b>"{0}"</b>
+upload.unzipped = \u89e3\u58d3\u7e2e {0}
+
+# help.jsp
+help.title = \u95dc\u65bc {0}
+help.upgrade = <b>\u6ce8\u610f!</b> \u5df2\u7d93\u6709\u65b0\u7684\u7248\u672c\u4e86\uff0c\u4e0b\u8f09\u65b0\u7248 {0} {1} \
+ <a href="#" onclick="window.open(''http://subsonic.org/'')">\u5728\u9019</a>.
+help.version.title = \u7248\u672c
+help.builddate.title = \u65e5\u671f
+help.server.title = \u4f3a\u670d\u5668
+help.license.title = \u6388\u6b0a
+help.license.text = {0} \u662f\u4ee5 <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank">GPL</a> \u578b\u5f0f\u767c\u4f48\u6388\u6b0a\u7684\u81ea\u7531\u8edf\u9ad4. <br>\
+ {0} \u4f7f\u7528 <a href="http://subsonic.org/pages/libraries.jsp" target="_blank">\u7b2c\u4e09\u65b9\u6388\u6b0a</a>.
+help.homepage.title = \u9996\u9801
+help.forum.title = \u8ad6\u58c7
+help.shop.title = \u5546\u54c1
+help.contact.title = \u806f\u7e6b
+help.contact.text = {0} \u7531 Sindre Mehus \u958b\u767c\u53ca\u7dad\u8b77 \
+ (<a href="mailto:sindre@activeobjects.no">sindre@activeobjects.no</a>). \
+ \u5982\u679c\u60a8\u6709\u4efb\u4f55\u7591\u554f\uff0c\u610f\u898b\u6216\u5efa\u8b70\u6539\u5584\uff0c\u8acb\u5230 \
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic\u8ad6\u58c7</a>.
+help.donate = {0} \u662f\u514d\u8cbb\u4f7f\u7528\u7684\uff0c\u4f46\u4e5f\u5e0c\u671b\u85c9\u7531\u60a8\u7684<b><a href="donate.view?">\u8d0a\u52a9</a></b>\u7d66\u4e88\u6211\u5011\u652f\u6301\u8207\u9f13\u52f5 .
+help.log = \u8a18\u9304
+help.logfile = \u5b8c\u6574\u7684\u7d00\u9304\u5b58\u653e\u5728 {0}.
+
+# settingsHeader.jsp
+settingsheader.title = \u8a2d\u5b9a
+settingsheader.general = \u4e00\u822c
+settingsheader.advanced = \u9032\u968e
+settingsheader.personal = \u500b\u4eba\u5316
+settingsheader.musicFolder = \u97f3\u6a02\u593e
+settingsheader.internetRadio = \u7dda\u4e0a\u96fb\u8996/\u6536\u97f3\u6a5f
+settingsheader.podcast = \u64ad\u5ba2
+settingsheader.player = \u64ad\u653e\u5668
+settingsheader.network = \u7db2\u8def
+settingsheader.transcoding = \u8f49\u6a94
+settingsheader.user = \u4f7f\u7528\u8005
+settingsheader.search = \u641c\u5c0b
+settingsheader.coverArt = \u5c08\u8f2f\u5c01\u9762
+settingsheader.password = \u5bc6\u78bc
+
+# generalSettings.jsp
+generalsettings.playlistfolder = \u9ede\u64ad\u6e05\u55ae\u8cc7\u6599\u593e
+generalsettings.musicmask = \u97f3\u6a02\u7684\u9644\u5c6c\u6a94\u540d:
+generalsettings.videomask = \u8996\u8a0a\u7684\u9644\u5c6c\u6a94\u540d:
+generalsettings.coverartmask = \u5c08\u8f2f\u5c01\u9762\u9644\u5c6c\u6a94\u540d
+generalsettings.index = \u7d22\u5f15
+generalsettings.ignoredarticles = \u5ffd\u7565\u7684\u6b4c\u624b\u540d\u7a31
+generalsettings.shortcuts = \u6377\u5f91
+generalsettings.showgettingstarted = \u5728\u9996\u9801\u986f\u793a "\u521d\u6b21\u4f7f\u7528"
+generalsettings.welcometitle = \u6b61\u8fce\u6a19\u984c
+generalsettings.welcomesubtitle = \u6b61\u8fce\u6b21\u6a19\u984c
+generalsettings.welcomemessage = \u6b61\u8fce\u8a0a\u606f
+generalsettings.loginmessage = \u767b\u5165\u8a0a\u606f
+generalsettings.language = Default language
+generalsettings.theme = \u9810\u8a2d\u4f48\u666f\u4e3b\u984c
+
+# advancedSettings.jsp
+advancedsettings.downsamplecommand = \u964d\u983b\u6307\u4ee4
+advancedsettings.coverartlimit = \u5c01\u9762\u9650\u5236<br><div class="detail">(0 = \u4e0d\u9650)</div>
+advancedsettings.downloadlimit = \u4e0b\u8f09\u9650\u5236 (Kbps)<br><div class="detail">(0 = \u4e0d\u9650)</div>
+advancedsettings.uploadlimit = \u4e0a\u50b3\u9650\u5236 (Kbps)<br><div class="detail">(0 = \u4e0d\u9650)</div>
+advancedsettings.streamport = \u975eSSL \u4e32\u6d41Port<br><div class="detail">(0 = \u53d6\u6d88)</div>
+advancedsettings.ldapenabled = \u555f\u52d5LDAP\u9a57\u8b49
+advancedsettings.ldapurl = LDAP\u7db2\u5740
+advancedsettings.ldapsearchfilter = LDAP\u641c\u5c0b\u904e\u6ffe
+advancedsettings.ldapmanagerdn = LDAP \u7ba1\u7406\u8005DN<br><div class="detail">(\u53ef\u9078\u64c7)</div>
+advancedsettings.ldapmanagerpassword = \u5bc6\u78bc
+advancedsettings.ldapautoshadowing = \u81ea\u52d5\u5728{0}\u5efa\u7acb\u4f7f\u7528\u8005
+
+# personalSettings.jsp
+personalsettings.title = {0}\u7684\u500b\u4eba\u5316\u8a2d\u5b9a
+personalsettings.language = Language
+personalsettings.theme = \u4f48\u666f\u4e3b\u984c
+personalsettings.display = \u986f\u793a
+personalsettings.browse = \u700f\u89bd
+personalsettings.playlist = \u9ede\u64ad\u6e05\u55ae
+personalsettings.tracknumber = \u97f3\u8ecc #
+personalsettings.artist = \u6b4c\u624b
+personalsettings.album = \u5c08\u8f2f
+personalsettings.genre = \u66f2\u98a8\u578b\u614b
+personalsettings.year = \u767c\u884c
+personalsettings.bitrate = Bit rate
+personalsettings.duration = \u6642\u6548
+personalsettings.format = \u683c\u5f0f
+personalsettings.filesize = \u6587\u4ef6\u5927\u5c0f
+personalsettings.captioncutoff = Caption cutoff
+personalsettings.partymode = \u5bb4\u6703\u6a21\u5f0f
+personalsettings.shownowplaying = \u5176\u4ed6\u4eba\u5728\u807d\u4ec0\u9ebc
+personalsettings.nowplayingallowed = \u8b93\u5225\u4eba\u770b\u6211\u5728\u807d\u4ec0\u9ebc
+personalsettings.showchat = \u986f\u793a\u4ea4\u8ac7\u8a0a\u606f
+personalsettings.finalversionnotification = \u63d0\u793a\u65b0\u7248\u672c
+personalsettings.betaversionnotification = \u63d0\u793a\u65b0\u7684\u6e2c\u8a66\u7248
+personalsettings.lastfmenabled = \u767b\u9304\u6211\u5728 <a href="http://last.fm/" target="_blank">Last.fm</a>\u7684\u5e33\u865f
+personalsettings.lastfmusername = Last.fm \u5e33\u865f
+personalsettings.lastfmpassword = Last.fm \u5bc6\u78bc
+personalsettings.avatar.title = \u500b\u4eba\u5716\u793a
+personalsettings.avatar.none = \u4e0d\u7528\u5716\u793a
+personalsettings.avatar.custom = \u81ea\u8a02\u5f71\u50cf
+personalsettings.avatar.changecustom = \u8b8a\u66f4\u81ea\u8a02\u5716\u793a
+personalsettings.avatar.upload = \u4e0a\u50b3
+personalsettings.avatar.courtesy = Icons courtesy of <a href="http://www.afterglow.ie/" target="_blank">Afterglow</a>, \
+ <a href="http://www.aha-soft.com/" target="_blank">Aha-Soft</a>, \
+ <a href="http://www.icons-land.com/" target="_blank">Icons-Land</a>, and \
+ <a href="http://www.iconshock.com/" target="_blank">Iconshock</a>
+
+# avatarUploadResult.jsp
+avataruploadresult.title = \u8b8a\u66f4\u500b\u4eba\u5716\u793a
+avataruploadresult.success = \u6210\u529f\u4e0a\u50b3\u500b\u4eba\u5716\u793a "{0}".
+avataruploadresult.failure = \u7121\u6cd5\u4e0a\u50b3\u500b\u4eba\u5716\u793a. \u8acb\u53c3\u95b1 <a href="help.view?">\u8a18\u9304</a>.
+
+# passwordSettings.jsp
+passwordsettings.title = \u8b8a\u66f4\u5bc6\u78bc {0}
+
+# musicFolderSettings.jsp
+musicfoldersettings.path = \u97f3\u6a02\u593e
+musicfoldersettings.name = \u540d\u7a31
+musicfoldersettings.enabled = \u4f7f\u7528
+musicfoldersettings.add = \u65b0\u589e\u97f3\u6a02\u593e
+musicfoldersettings.nopath = \u5e6b\u97f3\u6a02\u593e\u547d\u540d\u5427.
+
+# networkSettings.jsp
+networksettings.text = \u4ee5\u4e0b\u8a2d\u5b9a\u8b93\u60a8\u900f\u904e\u7db2\u969b\u7db2\u8def\u5b58\u53d6Subsonic\u4f3a\u670d\u5668.<br> \
+ \u6709\u4efb\u4f55\u554f\u984c, \u8acb\u53c3\u8003 <a href="http://subsonic.org/pages/getting-started.jsp" target="_blank"><b>\u521d\u6b21\u4f7f\u7528</b></a>\u624b\u518a.
+networksettings.portforwardingenabled = \u81ea\u52d5\u914d\u7f6e\u8def\u7531\u5668\u5b58\u53d6Subsonic\u4f3a\u670d\u5668 (UPnP port \u8f49\u767c).
+networksettings.portforwardinghelp = \u5982\u679c\u8def\u7531\u7121\u6cd5\u81ea\u52d5\u914d\u7f6e\uff0c\u8acb\u4f7f\u7528\u624b\u52d5\u914d\u7f6e.<br> \
+ \u4f9d\u7167 <a href="http://portforward.com/" target="_blank">portforward.com</a> \u7684\u8aaa\u660e<br>\
+ \u4f60\u5fc5\u9808\u8f49\u767cPort:{0}\u5230\u96fb\u8166\u4e0a\u904b\u884c\u7684Subsonic\u4f3a\u670d\u5668.
+networksettings.urlredirectionenabled = \u7528\u7c21\u55ae\u597d\u8a18\u7684\u7db2\u5740\u9023\u7dda\u5230\u60a8\u7684\u4f3a\u670d\u5668.
+networksettings.status = \u72c0\u614b:
+networksettings.trialexpired =\u5be6\u7528\u671f\u6eff {0}. \u8acb <b><a href="donate.view?">\u8d0a\u52a9</a></b> \u6c38\u4e45\u555f\u7528\u9019\u500b\u529f\u80fd.
+networksettings.trialnotexpired = \u63d0\u4f9b\u60a8\u5728{0}\u4e4b\u524d\u8a66\u7528. \u4e4b\u5f8c\u60a8\u53ef\u4ee5\u900f\u904e <b><a href="donate.view?">\u8d0a\u52a9</a></b>\u4f86\u6c38\u4e45\u4f7f\u7528.
+
+# transcodingSettings.jsp
+transcodingsettings.name = \u8f49\u6a94\u540d\u7a31
+transcodingsettings.sourceformat = \u539f\u59cb\u6a94
+transcodingsettings.targetformat = \u8f49\u63db\u6210
+transcodingsettings.step1 = \u6b65\u9a5f\u4e00
+transcodingsettings.step2 = \u6b65\u9a5f\u4e8c
+transcodingsettings.step3 = \u6b65\u9a5f\u4e09
+transcodingsettings.defaultactive = \u9810\u8a2d
+transcodingsettings.enabled = \u555f\u7528
+transcodingsettings.add = \u65b0\u589e\u8f49\u78bc\u5668
+transcodingsettings.noname = \u8acb\u6307\u5b9a\u4e00\u500b\u540d\u7a31.
+transcodingsettings.nosourceformat = \u8acb\u6307\u5b9a\u8f49\u63db\u683c\u5f0f.
+transcodingsettings.notargetformat = \u8acb\u6307\u5b9a\u8f49\u63db\u683c\u5f0f.
+transcodingsettings.nostep1 = \u8acb\u81f3\u5c11\u6307\u5b9a\u4e00\u500b\u8f49\u63db\u6b65\u9a5f.
+transcodingsettings.info = <p class="detail">(%s = \u6a94\u6848\u5c07\u88ab\u8f49\u63db, %b = \u64a5\u653e\u5668\u7684\u6700\u5927\u50b3\u8f38\u7387)</p> \
+ <p>\u8f49\u6a94\u904e\u7a0b\u662f\u7531\u4e00\u500b\u97f3\u6a02\u683c\u5f0f\u7ba1\u63db\u6210\u53e6\u4e00\u7a2e\u683c\u5f0f. {1}\u7684\u8f49\u6a94 \
+ \u5f15\u64ce\u5c07\u975e\u4e32\u6d41\u683c\u5f0f\u8f49\u63db\u6210\u4e32\u6d41. \u76f4\u63a5\u8f49\u6a94\u4e26\u4e14\u4e0d\u9700\u8981\u78c1\u789f\u904b\u4f5c<p/> \
+ <p>\u5be6\u969b\u7684\u5c08\u63db\u6771\u505a\u901a\u5e38\u662f\u900f\u904e\u7b2c\u4e09\u65b9\u8edf\u9ad4\u4ee5\u547d\u4ee4\u5217\u65b9\u5f0f\u8f49\u63db\uff0c\u7a0b\u5f0f\u5b89\u88dd\u5728 {0}. </p>\
+ <p>\u800cWindows\u7cfb\u7d71\u7684\u8f49\u63db\u7a0b\u5f0f\u53ef\u4ee5\u5728<a target="_blank" href="http://subsonic.org/pages/transcoding.jsp"><b>\u9019\u88e1\u627e\u5230</b></a>. \
+ \u60a8\u4e5f\u80fd\u4ee5\u81ea\u5b9a\u7684\u8f49\u63db\uff0c\u53ea\u8981\u6eff\u8db3\u4e0b\u9762\u8981\u6c42\uff1a\
+ <ul> \
+ <li>\u5fc5\u9808\u4ee5\u4e00\u500b\u547d\u4ee4\u884c\u6307\u4ee4\u754c\u9762.</li> \
+ <li>\u5fc5\u9808\u80fd\u5920\u5c07\u8f38\u51fa\u767c\u9001\u5230\u6a19\u6e96\u8f38\u51fa(Stdout).</li> \
+ <li>\u5982\u679c\u4f7f\u7528\u6b65\u9a5f2\u62163\uff0c\u5b83\u5fc5\u9808\u80fd\u5920\u5f9e\u6a19\u6e96\u8f38\u5165(Stdin)\u8b80\u53d6.</li> \
+ </ul> \
+ </p> \
+ <p> \u8acb\u6ce8\u610f\uff0c\u8f49\u63db\u7684\u7a0b\u5e8f\u7684\u7531\u64a5\u653e\u5668\u8a2d\u5b9a\u4e2d\u52fe\u9078\u3002\u5982\u679c"\u9810\u8a2d"\u662f\u5df2\u52fe\u9078\u7684\uff0c\u90a3\u9ebc\u8f49\u63db\u6703\u81ea\u52d5\u5728\u65b0\u64ad\u653e\u5668\u4e2d\u4f7f\u7528\u3002</p>
+
+# internetRadioSettings.jsp
+internetradiosettings.streamurl = \u4e32\u6d41\u7db2\u5740
+internetradiosettings.homepageurl = \u9996\u9801
+internetradiosettings.name = \u540d\u7a31
+internetradiosettings.enabled = \u63a1\u7528
+internetradiosettings.add = \u52a0\u5165\u7dda\u4e0a\u96fb\u8996/\u6536\u97f3\u6a5f
+internetradiosettings.nourl = \u8acb\u6307\u5b9a\u7db2\u5740\u3002
+internetradiosettings.noname = \u8acb\u6307\u5b9a\u540d\u7a31\u3002
+
+# podcastSettings.jsp
+podcastsettings.update = \u6aa2\u67e5\u65b0\u7684\u6536\u85cf
+podcastsettings.keep = \u4fdd\u6301
+podcastsettings.keep.all = \u6240\u6709\u7684\u6536\u85cf
+podcastsettings.keep.one = \u6700\u65b0\u7684\u6536\u85cf\u96c6
+podcastsettings.keep.many = \u6700\u8fd1 {0} \u7684\u6536\u85cf
+podcastsettings.download = \u5982\u679c\u6709\u6700\u65b0\u7684\u6536\u85cf
+podcastsettings.download.all = \u5168\u90e8\u4e0b\u8f09
+podcastsettings.download.one = \u4e0b\u8f09\u6700\u65b0\u7684
+podcastsettings.download.many = \u4e0b\u8f09\u6700\u8fd1{0}\u7684\u6536\u85cf
+podcastsettings.download.none = \u4e0d\u505a
+podcastsettings.interval.manually = \u624b\u52d5
+podcastsettings.interval.hourly = \u6bcf\u5c0f\u6642
+podcastsettings.interval.daily = \u6bcf\u5929
+podcastsettings.interval.weekly = \u6bcf\u9031
+podcastsettings.folder = \u5132\u5b58\u64ad\u5ba2\u5728
+
+# playerSettings.jsp
+playersettings.noplayers = \u6c92\u6709\u64a5\u653e\u5668.
+playersettings.type = \u5f62\u614b
+playersettings.lastseen = \u4e0a\u6b21\u767b\u5165
+playersettings.title = \u9078\u64c7\u64a5\u653e\u5668
+
+playersettings.technology.web.title = \u7db2\u9801\u64ad\u653e
+playersettings.technology.external.title = \u76f4\u63a5\u7528\u5916\u90e8\u64ad\u653e\u5668\u64ad\u653e
+playersettings.technology.external_with_playlist.title = \u4ee5\u9ede\u64ad\u6e05\u55ae\u5728\u5916\u90e8\u64a5\u653e\u5668\u64ad\u653e
+playersettings.technology.jukebox.title = \u9ede\u64ad\u6a5f\u6a21\u5f0f
+playersettings.technology.web.text = \u76f4\u63a5\u5728\u7db2\u9801\u4e2d\u7684Flash\u64ad\u653e\u5668\u64ad\u653e.
+playersettings.technology.external.text = \u5728\u60a8\u5e38\u7528\u7684\u64a5\u653e\u5668\u4e2d\u64ad\u653e\uff0c\u4f8b\u5982:WinAmp\u3001Windows Media Player\u3001iTunes.
+playersettings.technology.external_with_playlist.text = \u5982\u540c\u4e0a\u9762\u7684\u9078\u9805\uff0c\u4f46\u662f\u9ede\u64ad\u6e05\u55ae\u7531\u5ba2\u6236\u7aef\u7ba1\u7406\uff0c\u800c\u4e0d\u662f\u4f3a\u670d\u5668<br>\
+ \u56e0\u6b64\u53ef\u80fd\u7684\u60c5\u6cc1\u4e0b\u53ef\u4ee5\u76f4\u63a5\u8df3\u807d\u4e0b\u4e00\u9996\u6b4c\u66f2\u3002
+playersettings.technology.jukebox.text = \u76f4\u63a5\u5728Subsonic\u4f3a\u670d\u5668\u4e2d\u64ad\u653e. (\u9650\u5df2\u6388\u6b0a\u7684\u7528\u6236).
+playersettings.name = \u64a5\u653e\u5668\u540d\u7a31
+playersettings.coverartsize = \u5c08\u8f2f\u5c01\u9762\u5927\u5c0f
+playersettings.maxbitrate = \u6700\u5927\u50b3\u8f38\u7387
+playersettings.coverart.off = \u4e0d\u986f\u793a
+playersettings.coverart.small = \u5c0f
+playersettings.coverart.medium = \u4e2d
+playersettings.coverart.large = \u5927
+playersettings.nolame = <em>\u6ce8\u610f:</em> LAME\u5c1a\u672a\u5b89\u88dd.<br>\u9ede\u64ca\u3010\u5354\u52a9\u3011.
+playersettings.autocontrol = \u81ea\u52d5\u64ad\u653e
+playersettings.dynamicip = \u64a5\u653e\u5668\u4f7f\u7528\u52d5\u614bIP\u4f4d\u5740
+playersettings.transcodings = \u555f\u52d5\u7684\u8f49\u78bc\u7a0b\u5f0f
+playersettings.ok = \u5132\u5b58
+playersettings.forget = \u522a\u9664\u64a5\u653e\u5668
+playersettings.clone = \u8907\u88fd\u64a5\u653e\u5668
+
+# userSettings.jsp
+usersettings.title = \u9078\u64c7\u4f7f\u7528\u8005
+usersettings.newuser = \u65b0\u4f7f\u7528\u8005
+usersettings.admin = \u7ba1\u7406\u54e1\u6b0a\u9650
+usersettings.settings = \u53ef\u4ee5\u8b8a\u66f4\u8a2d\u5b9a\u53ca\u5bc6\u78bc
+usersettings.stream = \u53ef\u4ee5\u64ad\u653e\u6a94\u6848
+usersettings.jukebox = \u53ef\u4ee5\u7528\u9ede\u64a5\u6a5f\u6a21\u5f0f
+usersettings.download = \u53ef\u4ee5\u4e0b\u8f09\u6a94\u6848
+usersettings.upload = \u53ef\u4ee5\u4e0a\u50b3\u6a94\u6848
+usersettings.playlist= \u53ef\u4ee5\u65b0\u589e\u3001\u522a\u9664\u9ede\u64ad\u6e05\u55ae
+usersettings.coverart = \u53ef\u4ee5\u66f4\u63db\u5c08\u8f2f\u5c01\u9762\u53ca\u6a19\u7c64
+usersettings.comment= \u53ef\u4ee5\u5efa\u7acb\u6216\u7de8\u8f2f\u8a55\u8ad6\u53ca\u8a55\u5206
+usersettings.podcast= \u53ef\u4ee5\u7ba1\u7406\u64ad\u5ba2
+usersettings.username = \u4f7f\u7528\u8005
+usersettings.changepassword = \u8b8a\u66f4\u5bc6\u78bc
+usersettings.password = \u5bc6\u78bc
+usersettings.newpassword = \u65b0\u5bc6\u78bc
+usersettings.confirmpassword = \u518d\u6b21\u78ba\u8a8d\u5bc6\u78bc
+usersettings.delete = \u522a\u9664\u4f7f\u7528\u8005
+usersettings.ldap = \u5728LDAP\u9a57\u8b49\u7528\u6236
+usersettings.nousername = \u7f3a\u5c11\u4f7f\u7528\u8005\u540d\u7a31.
+usersettings.useralreadyexists = \u4f7f\u7528\u8005\u540d\u7a31\u5df2\u7d93\u88ab\u4f7f\u7528\u4e86\uff01.
+usersettings.nopassword = \u5fc5\u9808\u8a2d\u5b9a\u5bc6\u78bc.
+usersettings.wrongpassword = \u5169\u6b21\u8f38\u5165\u5bc6\u78bc\u4e0d\u540c.
+usersettings.ldapdisabled = LDAP\u9a57\u8b49\u6c92\u6709\u555f\u52d5. \u8acb\u5148\u5230\u9032\u968e\u8a2d\u5b9a.
+usersettings.passwordnotsupportedforldap = \u7121\u6cd5\u8a2d\u5b9a\u6216\u8b8a\u66f4LDAP\u9a57\u8b49\u7528\u6236\u7684\u5bc6\u78bc.
+usersettings.ok = \u4f7f\u7528\u8005 {0}\u7684\u5bc6\u78bc\u5df2\u7d93\u8b8a\u66f4.
+
+# musicFolderSettings.jsp
+musicfoldersettings.interval.never = \u5f9e\u4e0d
+musicfoldersettings.interval.one = \u6bcf\u5929
+musicfoldersettings.interval.many = \u6bcf\u9694 {0} \u5929
+musicfoldersettings.hour = \u5728 {0}:00
+
+# coverArtSettings.jsp
+coverartsettings.auto = \u7576\u641c\u5c0b\u7d22\u5f15\u5efa\u7acb\u6642\uff0c\u81ea\u52d5\u4e0b\u8f09\u907a\u5931\u7684\u5c08\u8f2f\u5c01\u9762.
+coverartsettings.manual = \u6b63\u5728\u4e0b\u8f09\u5c08\u8f2f\u5c01\u9762.
+coverartsettings.missing = {0} \u7684 {1} \u5c08\u8f2f\u5c01\u9762\u907a\u5931.
+coverartsettings.running = \u6b63\u5728\u4e0b\u8f09\u5c08\u8f2f\u5c01\u9762. \u8996\u60a8\u7684\u5a92\u9ad4\u8cc7\u6599\u5eab\u5927\u5c0f\uff0c\u6240\u9700\u7684\u6642\u9593\u4e0d\u7b49\u3002
+coverartsettings.albumList = \u5217\u51fa\u907a\u5931\u7684\u5c08\u8f2f\u5c01\u9762
+
+# main.jsp
+main.up = \u4e0a\u4e00\u9801
+main.playall = \u5168\u90e8\u64ad\u653e
+main.playrandom = \u96a8\u8208\u64ad\u653e
+main.addall = \u5168\u90e8\u52a0\u5230\u9ede\u64ad\u6e05\u55ae\u4e2d
+main.tags = \u7de8\u8f2f\u6a19\u7c64
+main.playcount = \u9ede\u64ad {0} \u6b21.
+main.lastplayed =\u4e0a\u6b21\u9ede\u64ad\u5728 {0}.
+main.comment = \u8a55\u8ad6
+main.wiki = <table class="detail">\
+ <tr><td style="padding-right:1em">__text__</td><td>Bold text </td><td style="padding-left:3em;padding-right:1em">\\\\ </td><td>Line break</td></tr>\
+ <tr><td style="padding-right:1em">~~text~~</td><td>Italic text </td><td style="padding-left:3em;padding-right:1em">(empty line) </td><td>New paragraph</td></tr>\
+ <tr><td style="padding-right:1em">* text </td><td>List item </td><td style="padding-left:3em;padding-right:1em">http://foo.com/ </td><td>Link</td></tr>\
+ <tr><td style="padding-right:1em">1. text </td><td>Enumerated list item</td><td style="padding-left:3em;padding-right:1em">{link:Foo|http://foo.com}</td><td>Named link</td></tr>\
+ </table>
+main.donate = <a href="{0}" style="text-decoration:underline">\u8d0a\u52a9</a> \u53ef\u4ee5 {1}!<br>(\u79fb\u9664\u6b64\u8655\u7684\u5ee3\u544a)
+main.nowplaying =\u76ee\u524d\u64a5\u653e
+main.lyrics = \u6b4c\u8a5e
+main.minutesago = \u5206\u9418\u524d
+main.chat = \u4ea4\u8ac7
+main.message = \u5beb\u5728\u9019\u88e1
+main.clearchat = \u6e05\u9664
+
+# rating.jsp
+rating.rating = \u7b49\u7d1a
+rating.clearrating = \u6e05\u9664\u7b49\u7d1a
+
+# coverArt.jsp
+coverart.change = \u8b8a\u66f4
+coverart.zoom = \u653e\u5927
+
+# allmusic.jsp
+allmusic.text = \u5728 allmusic.com \u641c\u5c0b\u5c08\u8f2f <em>{0}</em> - \u8acb\u7a0d\u5019.
+
+# changeCoverArt.jsp
+changecoverart.title = \u8b8a\u66f4\u5c08\u8f2f\u5c01\u9762
+changecoverart.address = \u6216\u662f\u8f38\u5165\u5716\u5f62\u7684\u4f4d\u5740
+changecoverart.artist = \u6b4c\u624b
+changecoverart.album = \u5c08\u8f2f
+changecoverart.searchdiscogs = \u641c\u5c0b Discogs
+changecoverart.wait = \u8acb\u7a0d\u5019...
+changecoverart.success = \u5716\u5f62\u4e0b\u8f09\u6210\u529f.
+changecoverart.error = \u7121\u6cd5\u4e0b\u8f09\u5716\u5f62.
+changecoverart.noimagesfound = \u627e\u4e0d\u5230\u5716\u5f62\u6a94.
+
+# changeCoverArtConfirm.jsp
+changeCoverArtConfirm.failed = \u7121\u6cd5\u4fee\u6539\u5c08\u8f2f\u5c01\u9762:<br><b>"{0}"</b>
+
+# editTags.jsp
+edittags.title = \u7de8\u8f2f\u6a19\u7c64
+edittags.file = \u6a94\u6848
+edittags.track = \u97f3\u8ecc
+edittags.songtitle = \u66f2\u540d
+edittags.artist = \u6b4c\u624b
+edittags.album = \u5c08\u8f2f
+edittags.year = \u767c\u884c
+edittags.genre = \u66f2\u98a8
+edittags.status = \u72c0\u614b
+edittags.suggest = \u5efa\u8b70
+edittags.reset = \u91cd\u7f6e
+edittags.suggest.short = S
+edittags.reset.short = R
+edittags.set = Set
+edittags.working = Working
+edittags.updated = Updated
+edittags.skipped = \u7565\u904e
+edittags.error = \u932f\u8aa4
+
+# donate.jsp
+donate.title = \u8d0a\u52a9
+donate.invalidlicense = \u6388\u6b0a\u5bc6\u9470\u7121\u6548.
+donate.amount = \u8d0a\u52a9 {0}
+
+donate.textbefore = <p>\u611f\u8b1d\u60a8\u7684\u8d0a\u52a9\u652f\u6301 {0} \u8a08\u756b! \
+ \u8d0a\u52a9\u8005\u53ef\u4ee5\u4f7f\u7528\u9032\u968e\u529f\u80fd\uff0c\u4f8b\u5982:</p> \
+ <ul> \
+ <li>\u7121\u9650\u5236\u7684\u4f7f\u7528 Subsonic \u63d0\u4f9b\u7d66iPhone, Android \u548c AIR\u7684. <a href="http://subsonic.org/pages/apps.jsp" target="blank">\u61c9\u7528\u7a0b\u5f0f</a></li> \
+ <li>\u60a8\u7684\u500b\u4eba\u5316\u7db2\u5740: <em>yourname</em>.subsonic.org (see <a href="networkSettings.view">Settings &gt; Network</a>).</li> \
+ <li>\u7121\u5ee3\u544a\u7684\u7db2\u9801\u4ecb\u9762.</li> \
+ <li>\u5176\u4ed6\u9678\u7e8c\u63a8\u51fa\u7684\u65b0\u529f\u80fd.</li> \
+ </ul> \
+ <p> \
+ \u8d0a\u52a9\u8005\u6703\u6536\u5230\u4e00\u500b\u76ee\u524d\u5230\u672a\u4f86\u6240\u6709{0}\u65b0\u7248\u672c\u7684\u6388\u6b0a\u5bc6\u9470</p> \
+ <p>\u5efa\u8b70\u60a8\u8d0a\u52a9<b>&euro;20</b>, \u60a8\u4e5f\u53ef\u4ee5\u8003\u616e\u5176\u4ed6\u91d1\u984d:</p>
+donate.textafter = <p>\u9ede\u64ca PayPal\u53ef\u4ee5\u4f7f\u7528\u4fe1\u7528\u5361\u652f\u4ed8 \
+ \u5982\u679c\u60a8\u6709PayPal\u5e33\u865f.\u60a8\u5c07\u5728\u5e7e\u5206\u9418\u5f8c\u7531email\u6536\u5230\u60a8\u7684\u6388\u6b0a\u5bc6\u9470</p> \
+ <p>\u5982\u679c\u6709\u4efb\u4f55\u554f\u984c\u8acb\u4f86\u4fe1\
+ <a href="mailto:subsonic_donation@activeobjects.no">subsonic_donation@activeobjects.no</a>.</p>
+donate.licensed = \u6b64\u526f\u672c\u7684 {2}\u662f {0} \u6388\u6b0a\u5728{1}\u3002\u611f\u8b1d\u60a8\u7684\u652f\u6301\uff01
+donate.register = \u7576\u60a8\u6536\u5230\u60a8\u7684\u6388\u6b0a\u5bc6\u9470\uff0c\u8acb\u5728\u767b\u9304\u5728\u4e0b\u9762.
+donate.register.email = \u96fb\u5b50\u90f5\u4ef6
+donate.register.license = \u6388\u6b0a
+
+# podcastReceiver.jsp
+podcastreceiver.title = \u64ad\u5ba2\u63a5\u6536\u5668
+podcastreceiver.expandall = \u986f\u793a\u6536\u85cf\u96c6
+podcastreceiver.collapseall = \u96b1\u85cf\u6536\u85cf\u96c6
+podcastreceiver.status.new = \u65b0\u7684
+podcastreceiver.status.downloading = \u4e0b\u8f09\u4e2d
+podcastreceiver.status.completed = \u5b8c\u6210
+podcastreceiver.status.error = \u932f\u8aa4
+podcastreceiver.status.deleted = \u5df2\u522a\u9664
+podcastreceiver.status.skipped = \u7565\u904e
+podcastreceiver.downloadselected= \u4e0b\u8f09\u5df2\u9078\u7684
+podcastreceiver.deleteselected= \u522a\u9664\u5df2\u9078\u64c7
+podcastreceiver.confirmdelete= \u771f\u7684\u8981\u522a\u9664\u64ad\u5ba2?
+podcastreceiver.check = \u6838\u5c0d\u65b0\u7684\u6536\u85cf\u96c6
+podcastreceiver.refresh = \u5237\u65b0\u672c\u9801
+podcastreceiver.settings = \u64ad\u5ba2\u8a2d\u5b9a
+podcastreceiver.subscribe = \u8a02\u95b1\u64ad\u5ba2
+
+# lyrics.jsp
+lyrics.title = \u6b4c\u8a5e
+lyrics.artist = \u6b4c\u624b
+lyrics.song = \u6b4c\u66f2
+lyrics.search = \u641c\u5c0b
+lyrics.wait = \u641c\u5c0b\u6b4c\u8a5e\u4e2d, \u8acb\u7a0d\u5019...
+lyrics.courtesy = (\u6b4c\u8a5e\u7531<a href="http://www.chartlyrics.com/" target="_blank">chartlyrics.com</a>\u63d0\u4f9b)
+lyrics.nolyricsfound = \u627e\u4e0d\u5230\u6b4c\u8a5e.
+
+# helpPopup.jsp
+helppopup.title = {0} \u5354\u52a9
+helppopup.cover.title = \u5c08\u8f2f\u5c01\u9762\u5927\u5c0f
+helppopup.cover.text = <p>\u7531\u60a8\u6307\u5b9a\u986f\u793a\u5c08\u8f2f\u5c01\u9762\u7684\u5927\u5c0f\uff0c\u4e5f\u53ef\u4ee5\u5b8c\u5168\u95dc\u9589.</p>
+helppopup.transcode.title = \u6700\u5927\u50b3\u8f38\u7387
+helppopup.transcode.text = <p>\u5982\u679c\u60a8\u7684\u64ad\u653e\u983b\u5bec\u6709\u9650\uff0c\u53ef\u4ee5\u904e\u8a2d\u5b9a\u6700\u5927\u50b3\u8f38\u7387\u4f86\u6539\u5584. \
+ \u4f8b\u5982,\u5982\u679cmp3\u539f\u672c\u7684\u58d3\u7e2e\u6bd4\u4f8b\u70ba256Kbps(kilobits per second), \u5982\u679c\u8a2d\u5b9a\u6700\u5927\u50b3\u8f38\u7387\u70ba128\
+ \u6703\u4f7f{0}\u81ea\u52d5\u5c07\u539f\u672c 256Kbps\u7684\u97f3\u6a02\u8abf\u964d\u6210 128 Kbps.</p> \
+ <p>\u9019\u500b\u9078\u9805\u5fc5\u9808\u4e8b\u5148\u5b89\u88ddLAME. LAME<a target="_blank" href="http://lame.sourceforge.net/">(http://lame.sourceforge.net)</a> \
+ \u662f\u63d0\u4f9bmp3\u7de8\u78bc\u7684\u4e00\u500b\u958b\u653e\u8edf\u9ad4. \u53ef\u4ee5\u5728<a target="_blank" href="http://subsonic.org/pages/transcoding.jsp">\u9019\u88e1\u4e0b\u8f09</a>. \
+ \u8acb\u78ba\u5b9a\u5c07\u5b83\u5b89\u88dd\u5728 SUBSONIC_HOME/transcode \u8cc7\u6599\u593e\u4e2d.</p>
+helppopup.playlistfolder.title = \u9ede\u64ad\u6e05\u55ae\u8cc7\u6599\u593e
+helppopup.playlistfolder.text = <p>\u5b58\u653e\u9ede\u64ad\u6e05\u55ae\u7684\u8cc7\u6599\u593e.</p>
+helppopup.musicmask.title = \u97f3\u6a02\u7684\u9644\u5c6c\u6a94\u540d
+helppopup.musicmask.text = <p>\u6307\u5b9a\u97f3\u6a02\u6a94\u6848\u7684\u9644\u5c6c\u6a94\u540d</p>
+helppopup.videomask.title = \u8996\u8a0a\u6a94\u7684\u9644\u5c6c\u6a94\u540d
+helppopup.videomask.text = <p>\u6307\u5b9a\u8996\u8a0a\u6a94\u6848\u7684\u8ca0\u6578\u6a94\u540d</p>
+helppopup.coverartmask.title = \u5c08\u8f2f\u5c01\u9762\u9644\u5c6c\u6a94\u540d
+helppopup.coverartmask.text = <p>\u8207\u97f3\u6a02\u593e\u653e\u5728\u4e00\u8d77\u7684\u5c08\u8f2f\u5c01\u9762\uff0c\u5148\u6307\u5b9a\u5c08\u8f2f\u5c01\u9762\u6a94\u6848\u7684\u9644\u5c6c\u6a94\u540d.</p>
+helppopup.downsamplecommand.title = \u964d\u983b\u6307\u4ee4
+helppopup.downsamplecommand.text = <p>\u8b93\u60a8\u8a2d\u5b9a\u8abf\u964d\u64ad\u653e\u97f3\u6a02\u6642\u964d\u4f4e\u53d6\u6a23\u983b\u7387\u7684\u6307\u4ee4.</p>\
+ <p>(%s = \u8981\u964d\u983b\u7684\u6a94\u6848, %b = \u64a5\u653e\u5668\u7684\u6700\u5927\u50b3\u8f38\u7387)</p>
+helppopup.index.title = \u5206\u985e\u6aa2\u7d22
+helppopup.index.text = <p>\u8a2d\u5b9a\u97f3\u6a02\u6aa2\u7d22\u7684\u65b9\u5f0f\uff0c\uff08\u5c31\u5982\u540c\u73fe\u5728\u5728\u87a2\u5e55\u5de6\u4e0a\u65b9\u7684\u5206\u985e\uff09. \u5728\u97f3\u6a02\u593e\u88e1\u7684\u6a94\u6848\u5f88\u5bb9\u6613\u5206\u985e\u6aa2\u7d22</p> \
+ <p>\u901a\u5e38\u4ee5\u7a7a\u767d\u4f86\u5206\u9694\u6aa2\u7d22\u9805\u76ee. \u4e00\u822c\u4f86\u8aaa\uff0c\u4e00\u500b\u5b57\u6bcd\uff08\u570b\u5b57\uff09\u70ba\u4e00\u500b\u9805\u76ee, \
+ \u4f46\u4e5f\u53ef\u4ee5\u591a\u500b\u5b57\u6bcd\uff08\u570b\u5b57\uff09\u653e\u5728\u540c\u4e00\u5206\u985e\u9805\u76ee\u4e2d. \u4f8b\u5982\uff1a\u3105(\u5305\u9b91),\u53ef\u4ee5\u5c07 <em>\u5305\u9b91</em>\u8996\u70ba\u540c\u4e00\u5206\u985e\
+ <p>\u800c\u7121\u6cd5\u6b78\u985e\u7684\u6a94\u6848\uff0c\u5c07\u5168\u90e8\u6b78\u65bc "#"\u9805\u76ee\u4e2d.</p>
+helppopup.ignoredarticles.title = \u5ffd\u7565\u5b57\u9996
+helppopup.ignoredarticles.text = <p>\u6709\u4e9b\u5b57\u9996(\u4f8b\u5982"The") \u901a\u5e38\u5728\u66f2\u76ee\u6b78\u985e\u4e2d\u61c9\u8a72\u5ffd\u7565\uff0c\u8acb\u5728\u6b64\u8655\u8a2d\u5b9a.</p>
+helppopup.shortcuts.title = \u6377\u5f91
+helppopup.shortcuts.text = <p>\u4ee5\u7a7a\u767d\u4f86\u5206\u9694\u5728\u6700\u4e0a\u5c64\u76ee\u9304\u4e2d\u5efa\u7acb\u6377\u5f91\u6aa2\u7d22. \u7528\u5f15\u865f\u4f86\u5206\u7d44\u5b57\u5143, \u4f8b\u5982:</p> \
+ <p><em>New Incoming "Sound tracks"</em></p>
+helppopup.language.title = Language
+helppopup.language.text = <p>\u5728\u9019\u88e1\u9078\u64c7\u6240\u63d0\u4f9b\u7684\u986f\u793a\u8a9e\u8a00.</p>
+helppopup.visibility.title = \u986f\u793a\u9805\u76ee
+helppopup.visibility.text = <p>\u9078\u64c7\u5728\u700f\u89bd\u6216\u662f\u5728\u9ede\u64ad\u6e05\u55ae\u4e2d\uff0c\u986f\u793a\u97f3\u6a02\u7684\u5404\u7a2e\u8a73\u7d30\u8cc7\u8a0a</p>
+helppopup.partymode.title = \u5bb4\u6703\u6a21\u5f0f
+helppopup.partymode.text = <p>\u4f7f\u7528\u5bb4\u6703\u6a21\u5f0f\u7c21\u5316\u4f7f\u7528\u8005\u754c\u9762\uff0c\u63d0\u4f9b\u7d66\u7121\u7d93\u9a57\u7684\u4f7f\u7528\u8005\u64cd\u4f5c\
+ \u4e5f\u53ef\u4ee5\u907f\u514d\u56e0\u70ba\u610f\u5916\u64cd\u4f5c\u800c\u5f04\u4e82\u4e86\u9ede\u64ad\u6e05\u55ae.</p>
+helppopup.theme.title = \u4f48\u666f\u4e3b\u984c
+helppopup.theme.text = <p>\u8b93\u60a8\u53ef\u4ee5\u9078\u64c7\u4e0d\u540c\u7684\u4f48\u666f\u4e3b\u984c. \u4f48\u666f\u4e3b\u984c\u900f\u904e\u4e0d\u540c\u7684\u984f\u8272\u3001\u5b57\u9ad4\u3001\u5716\u50cf\u7b49\uff0c\u5e36\u7d66\u60a8\u4e0d\u540c\u7684{0}\u611f\u53d7.</p>
+helppopup.welcomemessage.title = \u6b61\u8fce\u6a19\u984c
+helppopup.welcomemessage.text = <p>\u5728\u9996\u9801\u4e2d\u986f\u793a\u7684\u6a19\u984c.</p>
+helppopup.loginmessage.title = \u767b\u5165\u8a0a\u606f
+helppopup.loginmessage.text = <p>\u5728\u767b\u5165\u756b\u9762\u986f\u793a\u7684\u63d0\u793a\u8a0a\u606f.</p>
+helppopup.coverartlimit.title = \u5c08\u8f2f\u5c01\u9762\u9650\u5236
+helppopup.coverartlimit.text = <p>\u5728\u4e00\u9801\u756b\u9762\u4e2d\u986f\u793a\u5c08\u8f2f\u5c01\u9762\u7684\u6700\u5927\u6578\u91cf.</p>
+helppopup.downloadlimit.title = \u4e0b\u8f09\u9650\u5236
+helppopup.downloadlimit.text = <p>\u4e0b\u8f09\u6a94\u6848\u6642\u7684\u983b\u5bec\u9650\u5236.</p>
+helppopup.uploadlimit.title = \u4e0a\u50b3\u9650\u5236
+helppopup.uploadlimit.text = <p>\u4e0a\u50b3\u6a94\u6848\u6642\u7684\u983b\u5bec\u9650\u5236.</p>
+helppopup.streamport.title = \u975e-SSL \u4e32\u6d41 Port
+helppopup.streamport.text = <p>\u672c\u9078\u9805\u53ea\u662f\u7528\u65bc\u5728\u4f3a\u670d\u5668\u4e0a\u7684 {0} \u4f7f\u7528 SSL (HTTPS)\u50b3\u8f38\u5354\u5b9a.</p><p>\u6709\u4e9b\u64ad\u653e\u5668 \
+ (\u4f8b\u5982 Winamp) \u4e26\u672a\u652f\u63f4\u7d93\u7531SSL\u7684\u4e32\u6d41\u5354\u5b9a. \u5982\u679c\u60a8\u4e0d\u7d93\u7531SSL\u50b3\u9001\u4e32\u6d41\uff0c\u8acb\u6307\u5b9a\u901a\u8a0aPORT(\u901a\u5e38\u662f80\u62164040)\
+ \u8acb\u6ce8\u610f\uff0c\u4e32\u6d41\u97f3\u6a02\u4e26\u4e0d\u6703\u88ab\u52a0\u5bc6.</p>
+helppopup.ldap.title = LDAP \u9a57\u8b49
+helppopup.ldap.text = <p>\u4f7f\u7528\u8005\u53ef\u4ee5\u7d93\u7531\u5916\u90e8\u7684LDAP\u4f3a\u670d\u5668\u9a57\u8b49 (\u5305\u62ecWindows Active Directory). \
+ \u7576\u8a2d\u5b9a\u4f7f\u7528LDAP\u4f7f\u7528\u8005\u767b\u5165 {0}, \u5e33\u865f\u53ca\u5bc6\u78bc\u5c31\u7531\u5916\u90e8\u4f3a\u670d\u5668\u9a57\u8b49, \u800c\u4e0d\u662f\u7531{0}\u4f86\u9a57\u8b49.</p>
+helppopup.ldapurl.title = LDAP \u4f4d\u5740
+helppopup.ldapurl.text = <p>\u8a2d\u5b9a LDAP \u4f3a\u670d\u5668\u7684\u4f4d\u5740. \u5354\u5b9a\u61c9\u8a72\u662f <em>ldap://</em> \u6216 <em>ldaps://</em> \
+ (\u7d93\u7531SSL\u7684LDAP\u5354\u5b9a). \u8acb\u53c3\u8003<a href="http://java.sun.com/products/jndi/tutorial/ldap/misc/url.html" target="_blank">\u9019\u88e1</a> \
+ \u6709\u66f4\u591a\u8a73\u7d30\u8aaa\u660e.</p>
+helppopup.ldapsearchfilter.title = LDAP \u641c\u5c0b\u904e\u6ffe
+helppopup.ldapsearchfilter.text = <p>\u8490\u5c0bLDAP\u7528\u6236\u6642\uff0c\u5728\u9019\u88e1\u8a2d\u5b9a\u904e\u6ffe\u8868\u793a\u5f0f \
+ (\u5b9a\u7fa9\u5728<a href="http://www.ietf.org/rfc/rfc2254.txt" target="_blank">RFC 2254</a>). \
+ The pattern "'{0'}" is replaced by the username, \u4f8b\u5982: \
+ <ul>\
+ <li>(uid='{0'}) - this would search for a username match on the uid attribute.</li> \
+ <li>(sAMAccountName='{0'}) - typically used for authentication in Microsoft Active Directory.</li> \
+ </ul></p>
+helppopup.ldapmanagerdn.title = LDAP \u7ba1\u7406\u8005 DN
+helppopup.ldapmanagerdn.text = <p>\u5982\u679c LDAP \u4f3a\u670d\u5668\u4e0d\u652f\u63f4\u533f\u540d\u4f7f\u7528\u8005\u9023\u7dda\uff0c\u60a8\u5fc5\u9808\u6307\u5b9a DN \
+ (<em>Distinguished Name</em>)\u53caLDAP\u4f7f\u7528\u8005\u7684\u5bc6\u78bc.</p>
+helppopup.ldapautoshadowing.title = \u5728 {0} \u81ea\u52d5\u5efa\u7acb LDAP \u5e33\u865f
+helppopup.ldapautoshadowing.text = <p>\u8a2d\u5b9a\u9019\u500b\u9078\u9805, \u5247LDAP \u7528\u6236\u4e0d\u5fc5\u5728\u767b\u5165\u4e4b\u524d\u624b\u52d5\u5efa\u7acb{0}\u7684\u5e33\u865f. </p> \
+ <p>\u6ce8\u610f! \u9019\u610f\u5473\u6240\u6709\u5728LDAP\u4e0a\u7684\u4f7f\u7528\u8005\u90fd\u53ef\u4ee5\u767b\u5165\u60a8\u7684{0}, \
+ \u4e5f\u8a31\u9019\u4e0d\u662f\u60a8\u60f3\u8981\u7684\u529f\u80fd.</p>
+helppopup.playername.title = \u64a5\u653e\u5668\u540d\u7a31
+helppopup.playername.text = <p>\u8b93\u4f60\u5e6b\u64ad\u653e\u5668\u6307\u5b9a\u4e00\u500b\u5bb9\u6613\u8a18\u4f4f\u7684\u540d\u5b57\uff0c\u5982\u201c\u5de5\u4f5c\u5ba4\u201d\u6216\u201c\u5ba2\u5ef3\u201d</p>
+helppopup.autocontrol.title = \u63a7\u5236\u64ad\u653e\u5668\u81ea\u52d5\u64ad\u653e
+helppopup.autocontrol.text = <p>\u8a2d\u5b9a\u9019\u500b\u9078\u9805, {0} \u5728\u60a8\u958b\u59cb\u64ad\u653e\u6642\uff0c\u6703\u81ea\u52d5\u555f\u52d5\u64ad\u653e\u5668\
+ \u5426\u5247\u60a8\u5fc5\u9808\u81ea\u5df1\u555f\u52d5\u4e26\u4e14\u9023\u63a5\u60a8\u7684\u64a5\u653e\u5668.</p>
+helppopup.dynamicip.title = \u52d5\u614b IP \u4f4d\u5740
+helppopup.dynamicip.text = <p>\u95dc\u9589\u6b64\u9078\u9805\uff0c\u5982\u679c\u60a8\u4f7f\u7528\u975c\u614bIP\u4f4d\u5740.</p>
+
+# wap/index.jsp
+wap.index.missing = \u627e\u4e0d\u5230\u97f3\u6a02
+wap.index.playlist = \u9ede\u64ad\u6e05\u55ae
+wap.index.search = \u641c\u5c0b
+wap.index.settings = \u8a2d\u5b9a
+
+# wap/browse.jsp
+wap.browse.playone = \u64ad\u653e\u6b4c\u66f2
+wap.browse.playall = \u5168\u90e8\u64ad\u653e
+wap.browse.addone = \u52a0\u5165\u6b4c\u66f2
+wap.browse.addall = \u5168\u90e8\u52a0\u5165
+wap.browse.downloadone = \u4e0b\u8f09\u6b4c\u66f2
+wap.browse.downloadall = \u5168\u90e8\u4e0b\u8f09
+
+# wap/playlist.jsp
+wap.playlist.title =\u9ede\u64ad\u6e05\u55ae
+wap.playlist.noplayer = \u9023\u63a5\u4e0d\u5230\u64ad\u653e\u5668
+wap.playlist.clear = \u6e05\u9664
+wap.playlist.load = \u8f09\u5165
+wap.playlist.random = \u96a8\u8208
+wap.playlist.play = \u5728\u96fb\u8a71\u4e2d\u64ad\u9001
+
+# wap/search.jsp
+wap.search.title = \u641c\u5c0b
+
+# wap/searchResult.jsp
+wap.searchresult.index = \u6b63\u5728\u5efa\u7acb\u641c\u5c0b\u7d22\u5f15\u3002\u8acb\u7a0d\u5f8c\u518d\u8a66.
+
+# wap/settings.jsp
+wap.settings.selectplayer = \u9078\u64c7\u64ad\u653e\u5668
+wap.settings.allplayers = \u5168\u90e8
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/locales.txt b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/locales.txt
new file mode 100644
index 00000000..ff290ac9
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/locales.txt
@@ -0,0 +1,83 @@
+# List of available locales.
+# Author: Sindre Mehus
+
+# English
+en
+
+# English (United Kingdom)
+en_GB
+
+# English (United States)
+en_US
+
+# French
+fr
+
+# Spanish
+es
+
+# Catalan
+ca
+
+# Portuguese
+pt
+
+# German
+de
+
+# Italian
+it
+
+# Greek
+el
+
+# Russian
+ru
+
+# Slovenian
+sl
+
+# Macedonian
+mk
+
+# Polish
+pl
+
+# Bulgarian
+bg
+
+# Czech
+cs
+
+# Simplified Chinese
+zh_CN
+
+# Traditional Chinese
+zh_TW
+
+# Japanese
+ja_JP
+
+# Korean
+ko
+
+# Dutch
+nl
+
+# Norwegian
+no
+
+# Swedish
+sv
+
+# Danish
+da
+
+# Finnish
+fi
+
+# Icelandic
+is
+
+# Estonian
+et
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/2010.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/2010.properties
new file mode 100644
index 00000000..60355388
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/2010.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "2010" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/2010.css
+
+# Flash Player colours
+backgroundColor = 222222
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/2010/add.png
+clearRatingImage = icons/2010/clear_rating.png
+currentImage = icons/2010/now_playing.png
+donateImage = icons/2010/donate.png
+downImage = icons/2010/down.png
+downloadImage = icons/2010/download.png
+faviconImage = icons/2010/favicon.ico
+helpImage = icons/2010/help.png
+helpPopupImage = icons/2010/help_small.png
+homeImage = icons/2010/home.png
+logImage = icons/2010/log.png
+logoImage = icons/2010/logo.png
+moreImage = icons/2010/more.png
+nowPlayingImage = icons/2010/playing.png
+paypalImage = icons/2010/paypal.gif
+playImage = icons/2010/play.png
+podcastImage = icons/2010/podcast_small.png
+podcastLargeImage = icons/2010/podcast.png
+randomImage = icons/2010/random.png
+ratingOnImage = icons/2010/rating_on.png
+ratingOffImage = icons/2010/rating_off.png
+ratingHalfImage = icons/2010/rating_half.png
+removeImage = icons/2010/remove.png
+searchImage = icons/2010/search.png
+settingsImage = icons/2010/settings.png
+statusImage = icons/2010/status.png
+upImage = icons/2010/up.png
+uploadImage = icons/2010/upload.png
+volumeImage = icons/2010/now_playing.png
+wapImage = icons/2010/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/barents.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/barents.properties
new file mode 100644
index 00000000..896d1d3d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/barents.properties
@@ -0,0 +1,12 @@
+
+# Definition of the "Barents Sea" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/barents.css
+
+backgroundColor = 000843
+textColor = CFD1D6
+detailColor = CFD1D6
+linkColor = 97D9FF
+
+logoImage = icons/subsonic_white.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/black.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/black.properties
new file mode 100644
index 00000000..b3998a01
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/black.properties
@@ -0,0 +1,12 @@
+
+# Definition of the "Back In Black" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/black.css
+
+backgroundColor = 333333
+textColor = DDDDDD
+detailColor = 696969
+linkColor = BAD9F2
+
+logoImage = icons/subsonic_white.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/buuftheme.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/buuftheme.properties
new file mode 100644
index 00000000..bb51b133
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/buuftheme.properties
@@ -0,0 +1,48 @@
+
+# Definition of the "BUUF Theme" theme.
+# Author: Fractal Systems
+# BUUF artwork: Based on icons by Paul Davey aka Mattahan. All rights reserved.
+
+# Include the css
+styleSheet = style/buuftheme.css
+
+# Flash Player colors
+backgroundColor = AD845A
+textColor = 000000
+detailColor = 333333
+linkColor = 656569
+
+# Set the images for theme.
+addImage = icons/buuftheme/add.png
+androidImage = icons/buuftheme/android.png
+clearRatingImage = icons/buuftheme/clear_rating.png
+currentImage = icons/buuftheme/now_playing.png
+donateImage = icons/buuftheme/donate.png
+downImage = icons/buuftheme/down.png
+downloadImage = icons/buuftheme/download.png
+faviconImage = icons/buuftheme/favicon.ico
+gplImage = icons/buuftheme/gpl.png
+helpImage = icons/buuftheme/help.png
+helpPopupImage = icons/buuftheme/help_small.png
+homeImage = icons/buuftheme/home.png
+logImage = icons/buuftheme/log.png
+logoImage = icons/buuftheme/logo.png
+moreImage = icons/buuftheme/more.png
+nowPlayingImage = icons/buuftheme/playing.png
+paypalImage = icons/buuftheme/paypal.gif
+playImage = icons/buuftheme/play.png
+podcastImage = icons/buuftheme/podcast_small.png
+podcastLargeImage = icons/buuftheme/podcast.png
+randomImage = icons/buuftheme/random.png
+ratingOnImage = icons/buuftheme/rating_on.png
+ratingOffImage = icons/buuftheme/rating_off.png
+ratingHalfImage = icons/buuftheme/rating_half.png
+removeImage = icons/buuftheme/remove.png
+searchImage = icons/buuftheme/search.png
+settingsImage = icons/buuftheme/settings.png
+statusImage = icons/buuftheme/status.png
+upImage = icons/buuftheme/up.png
+uploadImage = icons/buuftheme/upload.png
+volumeImage = icons/buuftheme/now_playing.png
+wapImage = icons/buuftheme/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/coolandclean.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/coolandclean.properties
new file mode 100644
index 00000000..c19a8201
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/coolandclean.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "Cool and Clean" theme.
+# Author: Dan Eriksen
+
+# Include the css
+styleSheet = style/coolandclean.css
+
+# Flash Player colours
+backgroundColor = D1D9E1
+textColor = 000000
+detailColor = 333333
+linkColor = 656569
+
+# Set the images for theme.
+addImage = icons/coolandclean/add.png
+clearRatingImage = icons/coolandclean/clear_rating.png
+currentImage = icons/coolandclean/now_playing.png
+donateImage = icons/coolandclean/donate.png
+downImage = icons/coolandclean/down.png
+downloadImage = icons/coolandclean/download.png
+faviconImage = icons/coolandclean/favicon.ico
+helpImage = icons/coolandclean/help.png
+helpPopupImage = icons/coolandclean/help_small.png
+homeImage = icons/coolandclean/home.png
+logImage = icons/coolandclean/log.png
+logoImage = icons/coolandclean/logo.png
+moreImage = icons/coolandclean/more.png
+nowPlayingImage = icons/coolandclean/playing.png
+paypalImage = icons/coolandclean/paypal.gif
+playImage = icons/coolandclean/play.png
+podcastImage = icons/coolandclean/podcast_small.png
+podcastLargeImage = icons/coolandclean/podcast.png
+randomImage = icons/coolandclean/random.png
+ratingOnImage = icons/coolandclean/rating_on.png
+ratingOffImage = icons/coolandclean/rating_off.png
+ratingHalfImage = icons/coolandclean/rating_half.png
+removeImage = icons/coolandclean/remove.png
+searchImage = icons/coolandclean/search.png
+settingsImage = icons/coolandclean/settings.png
+statusImage = icons/coolandclean/status.png
+upImage = icons/coolandclean/up.png
+uploadImage = icons/coolandclean/upload.png
+volumeImage = icons/coolandclean/now_playing.png
+wapImage = icons/coolandclean/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/default.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/default.properties
new file mode 100644
index 00000000..8835eee6
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/default.properties
@@ -0,0 +1,53 @@
+
+# Definition of the "Subsonic Default" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/default.css
+
+backgroundColor = EFEFEF
+textColor = 000000
+detailColor = 696969
+linkColor = 006699
+
+addImage = icons/add.gif
+androidImage = icons/android.png
+clearRatingImage = icons/clearRating.png
+currentImage = icons/current.gif
+donateImage = icons/donate.png
+donateSmallImage = icons/donate_small.png
+downImage = icons/down.gif
+downloadImage = icons/download.gif
+faviconImage = icons/favicon.ico
+helpImage = icons/help.png
+helpPopupImage = icons/help_small.png
+homeImage = icons/home.png
+logImage = icons/log.png
+logoImage = icons/subsonic_black.png
+moreImage = icons/more.png
+nowPlayingImage = icons/now_playing.png
+paypalImage = icons/paypal.gif
+playImage = icons/play.gif
+podcastImage = icons/podcast.png
+podcastLargeImage = icons/podcast_large.png
+randomImage = icons/random.png
+ratingOnImage = icons/ratingOn.png
+ratingOffImage = icons/ratingOff.png
+ratingHalfImage = icons/ratingHalf.png
+removeImage = icons/remove.gif
+scanningImage = icons/spinner.gif
+searchImage = icons/search.png
+settingsImage = icons/settings.png
+shareFacebookImage = icons/share_facebook.png
+shareTwitterImage = icons/share_twitter.png
+shareGooglePlusImage = icons/share_googleplus.png
+starredImage = icons/starred.png
+statusImage = icons/status.png
+upImage = icons/up.gif
+uploadImage = icons/upload.gif
+volumeImage = icons/current.gif
+wapImage = icons/wap.png
+errorImage = icons/error.png
+
+
+
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/denim.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/denim.properties
new file mode 100644
index 00000000..77332de2
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/denim.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "Denim" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/denim.css
+
+# Flash Player colours
+backgroundColor = 456993
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/denim/add.png
+clearRatingImage = icons/denim/clear_rating.png
+currentImage = icons/denim/now_playing.png
+donateImage = icons/denim/donate.png
+downImage = icons/denim/down.png
+downloadImage = icons/denim/download.png
+faviconImage = icons/denim/favicon.ico
+helpImage = icons/denim/help.png
+helpPopupImage = icons/denim/help_small.png
+homeImage = icons/denim/home.png
+logImage = icons/denim/log.png
+logoImage = icons/denim/logo.png
+moreImage = icons/denim/more.png
+nowPlayingImage = icons/denim/playing.png
+paypalImage = icons/denim/paypal.gif
+playImage = icons/denim/play.png
+podcastImage = icons/denim/podcast_small.png
+podcastLargeImage = icons/denim/podcast.png
+randomImage = icons/denim/random.png
+ratingOnImage = icons/denim/rating_on.png
+ratingOffImage = icons/denim/rating_off.png
+ratingHalfImage = icons/denim/rating_half.png
+removeImage = icons/denim/remove.png
+searchImage = icons/denim/search.png
+settingsImage = icons/denim/settings.png
+statusImage = icons/denim/status.png
+upImage = icons/denim/up.png
+uploadImage = icons/denim/upload.png
+volumeImage = icons/denim/now_playing.png
+wapImage = icons/denim/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove.properties
new file mode 100644
index 00000000..05c240a7
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "groove" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/groove.css
+
+# Flash Player colours
+backgroundColor = 333333
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/groove/add.png
+clearRatingImage = icons/groove/clear_rating.png
+currentImage = icons/groove/now_playing.png
+donateImage = icons/groove/donate.png
+downImage = icons/groove/down.png
+downloadImage = icons/groove/download.png
+faviconImage = icons/groove/favicon.ico
+helpImage = icons/groove/help.png
+helpPopupImage = icons/groove/help_small.png
+homeImage = icons/groove/home.png
+logImage = icons/groove/log.png
+logoImage = icons/groove/logo.png
+moreImage = icons/groove/more.png
+nowPlayingImage = icons/groove/playing.png
+paypalImage = icons/groove/paypal.gif
+playImage = icons/groove/play.png
+podcastImage = icons/groove/podcast_small.png
+podcastLargeImage = icons/groove/podcast.png
+randomImage = icons/groove/random.png
+ratingOnImage = icons/groove/rating_on.png
+ratingOffImage = icons/groove/rating_off.png
+ratingHalfImage = icons/groove/rating_half.png
+removeImage = icons/groove/remove.png
+searchImage = icons/groove/search.png
+settingsImage = icons/groove/settings.png
+statusImage = icons/groove/status.png
+upImage = icons/groove/up.png
+uploadImage = icons/groove/upload.png
+volumeImage = icons/groove/now_playing.png
+wapImage = icons/groove/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove_simple.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove_simple.properties
new file mode 100644
index 00000000..38ff584b
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/groove_simple.properties
@@ -0,0 +1,45 @@
+groove
+# Definition of the "groove" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/groove_simple.css
+
+# Flash Player colours
+backgroundColor = 333333
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/groove/add.png
+clearRatingImage = icons/groove/clear_rating.png
+currentImage = icons/groove/now_playing.png
+donateImage = icons/groove/donate.png
+downImage = icons/groove/down.png
+downloadImage = icons/groove/download.png
+faviconImage = icons/groove/favicon.ico
+helpImage = icons/groove/help.png
+helpPopupImage = icons/groove/help_small.png
+homeImage = icons/groove/home.png
+logImage = icons/groove/log.png
+logoImage = icons/groove/logo.png
+moreImage = icons/groove/more.png
+nowPlayingImage = icons/groove/playing.png
+paypalImage = icons/groove/paypal.gif
+playImage = icons/groove/play.png
+podcastImage = icons/groove/podcast_small.png
+podcastLargeImage = icons/groove/podcast.png
+randomImage = icons/groove/random.png
+ratingOnImage = icons/groove/rating_on.png
+ratingOffImage = icons/groove/rating_off.png
+ratingHalfImage = icons/groove/rating_half.png
+removeImage = icons/groove/remove.png
+searchImage = icons/groove/search.png
+settingsImage = icons/groove/settings.png
+statusImage = icons/groove/status.png
+upImage = icons/groove/up.png
+uploadImage = icons/groove/upload.png
+volumeImage = icons/groove/now_playing.png
+wapImage = icons/groove/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd1080.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd1080.properties
new file mode 100644
index 00000000..dcf488b1
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd1080.properties
@@ -0,0 +1,12 @@
+
+# Definition of the "HD-1080" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/hd1080.css
+
+backgroundColor = 000843
+textColor = CFD1D6
+detailColor = CFD1D6
+linkColor = 97D9FF
+
+logoImage = icons/subsonic_white.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd720.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd720.properties
new file mode 100644
index 00000000..3b5e9625
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd720.properties
@@ -0,0 +1,12 @@
+
+# Definition of the "HD-720" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/hd720.css
+
+backgroundColor = 000843
+textColor = CFD1D6
+detailColor = CFD1D6
+linkColor = 97D9FF
+
+logoImage = icons/subsonic_white.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd768.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd768.properties
new file mode 100644
index 00000000..a761c58f
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hd768.properties
@@ -0,0 +1,12 @@
+
+# Definition of the "HD-768" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/hd768.css
+
+backgroundColor = 000843
+textColor = CFD1D6
+detailColor = CFD1D6
+linkColor = 97D9FF
+
+logoImage = icons/subsonic_white.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hicon.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hicon.properties
new file mode 100644
index 00000000..05bb7a5b
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hicon.properties
@@ -0,0 +1,20 @@
+
+# Definition of the "High Contrast" theme.
+# Author: Jeebs (Fisher Evans)
+
+styleSheet = style/hicon.css
+
+backgroundColor = ffffff
+textColor = 5c5c5c
+detailColor = 5c5c5c
+linkColor = black
+
+logoImage = icons/hicon/subsonic.png
+nowPlayingImage = icons/hicon/now_playing.png
+helpImage = icons/hicon/help.png
+moreImage = icons/hicon/more.png
+statusImage = icons/hicon/status.png
+settingsImage = icons/hicon/settings.png
+homeImage = icons/hicon/home.png
+podcastLargeImage = icons/hicon/podcast_large.png
+faviconImage = icons/hicon/favicon.ico
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hiconi.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hiconi.properties
new file mode 100644
index 00000000..5eb8b8a1
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hiconi.properties
@@ -0,0 +1,20 @@
+
+# Definition of the "High Contrast (Inverted)" theme.
+# Author: Jeebs (Fisher Evans)
+
+styleSheet = style/hiconi.css
+
+backgroundColor = 000000
+textColor = a3a3a3
+detailColor = a3a3a3
+linkColor = white
+
+logoImage = icons/hiconi/subsonic.png
+nowPlayingImage = icons/hiconi/now_playing.png
+helpImage = icons/hiconi/help.png
+moreImage = icons/hiconi/more.png
+statusImage = icons/hiconi/status.png
+settingsImage = icons/hiconi/settings.png
+homeImage = icons/hiconi/home.png
+podcastLargeImage = icons/hiconi/podcast_large.png
+faviconImage = icons/hiconi/favicon.ico
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hitech.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hitech.properties
new file mode 100644
index 00000000..166d86c8
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/hitech.properties
@@ -0,0 +1,20 @@
+
+# Definition of the "High-Tech" theme.
+# Author: Jeebs (Fisher Evans)
+
+styleSheet = style/hitech.css
+
+backgroundColor = 090a09
+textColor = d2f1d5
+detailColor = d2f1d5
+linkColor = 00d61b
+
+logoImage = icons/hitech/subsonic.png
+nowPlayingImage = icons/hitech/now_playing.png
+helpImage = icons/hitech/help.png
+moreImage = icons/hitech/more.png
+statusImage = icons/hitech/status.png
+settingsImage = icons/hitech/settings.png
+homeImage = icons/hitech/home.png
+podcastLargeImage = icons/hitech/podcast_large.png
+faviconImage = icons/hitech/favicon.ico
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnight.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnight.properties
new file mode 100644
index 00000000..9f8743b0
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnight.properties
@@ -0,0 +1,12 @@
+
+# Definition of the "2 Minutes To Midnight" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/midnight.css
+
+backgroundColor = 656569
+textColor = DDDDDD
+detailColor = DDDDDD
+linkColor = BAD9F2
+
+logoImage = icons/subsonic_white.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnightfun.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnightfun.properties
new file mode 100644
index 00000000..54b6c7b9
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/midnightfun.properties
@@ -0,0 +1,45 @@
+
+# Definition of the http://www.midnightfun.co.uk/ "MidnightFun" theme.
+# Author: Don Pearson http://www.midnightfun.co.uk/
+
+# Include the css
+styleSheet = style/midnightfun.css
+
+# Flash Player colours
+backgroundColor = AAAAAA
+detailColor = 696969
+linkColor = 006699
+textColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/midnightfun/midnightfun_add.png
+clearRatingImage = icons/midnightfun/midnightfun_clear_Rating.png
+currentImage = icons/midnightfun/midnightfun_Now_Playing.png
+donateImage = icons/midnightfun/midnightfun_donate.png
+downImage = icons/midnightfun/midnightfun_down.png
+downloadImage = icons/midnightfun/midnightfun_download.png
+faviconImage = icons/midnightfun/favicon.ico
+helpImage = icons/midnightfun/midnightfun_help.png
+helpPopupImage = icons/midnightfun/midnightfun_help_small.png
+homeImage = icons/midnightfun/midnightfun_home.png
+logImage = icons/midnightfun/midnightfun_log.png
+logoImage = icons/midnightfun/midnightfun_logo.png
+moreImage = icons/midnightfun/midnightfun_more.png
+nowPlayingImage = icons/midnightfun/midnightfun_playing.png
+paypalImage = icons/midnightfun/midnightfun_paypal.gif
+playImage = icons/midnightfun/midnightfun_play.png
+podcastImage = icons/midnightfun/midnightfun_podcast_small.png
+podcastLargeImage = icons/midnightfun/midnightfun_podcast.png
+randomImage = icons/midnightfun/midnightfun_random.png
+ratingOnImage = icons/midnightfun/ratingOn.png
+ratingOffImage = icons/midnightfun/ratingOff.png
+ratingHalfImage = icons/midnightfun/ratingHalf.png
+removeImage = icons/midnightfun/midnightfun_remove.png
+searchImage = icons/midnightfun/midnightfun_search.png
+settingsImage = icons/midnightfun/midnightfun_settings.png
+statusImage = icons/midnightfun/midnightfun_status.png
+upImage = icons/midnightfun/midnightfun_up.png
+uploadImage = icons/midnightfun/midnightfun_upload.png
+volumeImage = icons/midnightfun/midnightfun_Now_Playing.png
+wapImage = icons/midnightfun/midnightfun_phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome.properties
new file mode 100644
index 00000000..2be2112d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome.properties
@@ -0,0 +1,11 @@
+# Definition of the "Monochrome" theme.
+# Author: David D.
+
+styleSheet = style/monochrome.css
+
+backgroundColor = EEEEEE
+textColor = 333333
+detailColor = AAAAAA
+linkColor = 006699
+
+logoImage = icons/monochrome/subdot.png
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome_black.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome_black.properties
new file mode 100644
index 00000000..d815477d
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/monochrome_black.properties
@@ -0,0 +1,12 @@
+# Definition of the "Monochrome (Black)" theme.
+# Author: David D.
+
+styleSheet = style/monochrome_black.css
+
+backgroundColor = 333333
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+logoImage = icons/monochrome/subdot.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/pinkpanther.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/pinkpanther.properties
new file mode 100644
index 00000000..5a157bb6
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/pinkpanther.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "PinkPanther" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/pinkpanther.css
+
+# Flash Player colours
+backgroundColor = 402c31
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/pinkpanther/add.png
+clearRatingImage = icons/pinkpanther/clear_rating.png
+currentImage = icons/pinkpanther/now_playing.png
+donateImage = icons/pinkpanther/donate.png
+downImage = icons/pinkpanther/down.png
+downloadImage = icons/pinkpanther/download.png
+faviconImage = icons/pinkpanther/favicon.ico
+helpImage = icons/pinkpanther/help.png
+helpPopupImage = icons/pinkpanther/help_small.png
+homeImage = icons/pinkpanther/home.png
+logImage = icons/pinkpanther/log.png
+logoImage = icons/pinkpanther/logo.png
+moreImage = icons/pinkpanther/more.png
+nowPlayingImage = icons/pinkpanther/playing.png
+paypalImage = icons/pinkpanther/paypal.gif
+playImage = icons/pinkpanther/play.png
+podcastImage = icons/pinkpanther/podcast_small.png
+podcastLargeImage = icons/pinkpanther/podcast.png
+randomImage = icons/pinkpanther/random.png
+ratingOnImage = icons/pinkpanther/rating_on.png
+ratingOffImage = icons/pinkpanther/rating_off.png
+ratingHalfImage = icons/pinkpanther/rating_half.png
+removeImage = icons/pinkpanther/remove.png
+searchImage = icons/pinkpanther/search.png
+settingsImage = icons/pinkpanther/settings.png
+statusImage = icons/pinkpanther/status.png
+upImage = icons/pinkpanther/up.png
+uploadImage = icons/pinkpanther/upload.png
+volumeImage = icons/pinkpanther/now_playing.png
+wapImage = icons/pinkpanther/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/ripserver.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/ripserver.properties
new file mode 100644
index 00000000..5d1e261c
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/ripserver.properties
@@ -0,0 +1,43 @@
+
+# Definition of the "Ripserver" theme.
+# Author: Ralph Hill / Sindre Mehus
+
+styleSheet = style/ripserver.css
+
+backgroundColor = FFFFFF
+linkColor = 0076B6
+
+addImage = icons/ripserver/add.gif
+clearRatingImage = icons/ripserver/clearRating.png
+currentImage = icons/ripserver/current.gif
+donateImage = icons/ripserver/donate.png
+downImage = icons/ripserver/down.gif
+downloadImage = icons/ripserver/download.gif
+faviconImage = icons/ripserver/favicon.ico
+helpImage = icons/ripserver/help.png
+helpPopupImage = icons/ripserver/help_small.png
+homeImage = icons/ripserver/home.png
+logImage = icons/ripserver/log.png
+logoImage = icons/ripserver/subsonic_black.png
+moreImage = icons/ripserver/more.png
+nowPlayingImage = icons/ripserver/now_playing.png
+paypalImage = icons/ripserver/paypal.gif
+playImage = icons/ripserver/play.gif
+podcastImage = icons/ripserver/podcast.png
+podcastLargeImage = icons/ripserver/podcast_large.png
+randomImage = icons/ripserver/random.png
+ratingOnImage = icons/ripserver/ratingOn.png
+ratingOffImage = icons/ripserver/ratingOff.png
+ratingHalfImage = icons/ripserver/ratingHalf.png
+removeImage = icons/ripserver/remove.gif
+searchImage = icons/ripserver/search.png
+settingsImage = icons/ripserver/settings.png
+statusImage = icons/ripserver/status.png
+upImage = icons/ripserver/up.gif
+uploadImage = icons/ripserver/upload.gif
+volumeImage = icons/ripserver/current.gif
+wapImage = icons/ripserver/wap.png
+
+
+
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sandstorm.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sandstorm.properties
new file mode 100644
index 00000000..bddad3c1
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sandstorm.properties
@@ -0,0 +1,10 @@
+
+# Definition of the "Sandstorm" theme.
+# Author: Sindre Mehus
+
+styleSheet = style/sandstorm.css
+
+backgroundColor = F5F5D0
+textColor = 000000
+detailColor = 696969
+linkColor = 057368
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/simplify.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/simplify.properties
new file mode 100644
index 00000000..4d9d8ef6
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/simplify.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "Simplify" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/simplify.css
+
+# Flash Player colours
+backgroundColor = 1b1b1b
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/simplify/add.png
+clearRatingImage = icons/simplify/clear_rating.png
+currentImage = icons/simplify/now_playing.png
+donateImage = icons/simplify/donate.png
+downImage = icons/simplify/down.png
+downloadImage = icons/simplify/download.png
+faviconImage = icons/simplify/favicon.ico
+helpImage = icons/simplify/help.png
+helpPopupImage = icons/simplify/help_small.png
+homeImage = icons/simplify/home.png
+logImage = icons/simplify/log.png
+logoImage = icons/simplify/logo.png
+moreImage = icons/simplify/more.png
+nowPlayingImage = icons/simplify/playing.png
+paypalImage = icons/simplify/paypal.gif
+playImage = icons/simplify/play.png
+podcastImage = icons/simplify/podcast_small.png
+podcastLargeImage = icons/simplify/podcast.png
+randomImage = icons/simplify/random.png
+ratingOnImage = icons/simplify/rating_on.png
+ratingOffImage = icons/simplify/rating_off.png
+ratingHalfImage = icons/simplify/rating_half.png
+removeImage = icons/simplify/remove.png
+searchImage = icons/simplify/search.png
+settingsImage = icons/simplify/settings.png
+statusImage = icons/simplify/status.png
+upImage = icons/simplify/up.png
+uploadImage = icons/simplify/upload.png
+volumeImage = icons/simplify/now_playing.png
+wapImage = icons/simplify/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/slick.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/slick.properties
new file mode 100644
index 00000000..18b04eac
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/slick.properties
@@ -0,0 +1,20 @@
+
+# Definition of the "Slick" theme.
+# Author: Jeebs (Fisher Evans)
+
+styleSheet = style/slick.css
+
+backgroundColor = ffffff
+textColor = 1e3127
+detailColor = 1e3127
+linkColor = 1f5e59
+
+logoImage = icons/slick/subsonic.png
+nowPlayingImage = icons/slick/now_playing.png
+helpImage = icons/slick/help.png
+moreImage = icons/slick/more.png
+statusImage = icons/slick/status.png
+settingsImage = icons/slick/settings.png
+homeImage = icons/slick/home.png
+podcastLargeImage = icons/slick/podcast_large.png
+faviconImage = icons/slick/favicon.ico
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic.properties
new file mode 100644
index 00000000..96cc0f57
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic.properties
@@ -0,0 +1,44 @@
+# Definition of the "Sonic" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/sonic.css
+
+# Flash Player colours
+backgroundColor = e7e7e7
+textColor = 696969
+detailColor = 333333
+linkColor = 333333
+
+# Set the images for theme.
+addImage = icons/sonic/add.png
+clearRatingImage = icons/sonic/clear_rating.png
+currentImage = icons/sonic/now_playing.png
+donateImage = icons/sonic/donate.png
+downImage = icons/sonic/down.png
+downloadImage = icons/sonic/download.png
+faviconImage = icons/sonic/favicon.ico
+helpImage = icons/sonic/help.png
+helpPopupImage = icons/sonic/help_small.png
+homeImage = icons/sonic/home.png
+logImage = icons/sonic/log.png
+logoImage = icons/sonic/logo.png
+moreImage = icons/sonic/more.png
+nowPlayingImage = icons/sonic/playing.png
+paypalImage = icons/sonic/paypal.gif
+playImage = icons/sonic/play.png
+podcastImage = icons/sonic/podcast_small.png
+podcastLargeImage = icons/sonic/podcast.png
+randomImage = icons/sonic/random.png
+ratingOnImage = icons/sonic/rating_on.png
+ratingOffImage = icons/sonic/rating_off.png
+ratingHalfImage = icons/sonic/rating_half.png
+removeImage = icons/sonic/remove.png
+searchImage = icons/sonic/search.png
+settingsImage = icons/sonic/settings.png
+statusImage = icons/sonic/status.png
+upImage = icons/sonic/up.png
+uploadImage = icons/sonic/upload.png
+volumeImage = icons/sonic/now_playing.png
+wapImage = icons/sonic/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_blue.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_blue.properties
new file mode 100644
index 00000000..e2473613
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_blue.properties
@@ -0,0 +1,45 @@
+
+# Definition of the "Sonic Blue" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/sonic_blue.css
+
+# Flash Player colours
+backgroundColor = 456993
+textColor = CCCCCC
+detailColor = AAAAAA
+linkColor = FFFFFF
+
+# Set the images for theme.
+addImage = icons/sonic_blue/add.png
+clearRatingImage = icons/sonic_blue/clear_rating.png
+currentImage = icons/sonic_blue/now_playing.png
+donateImage = icons/sonic_blue/donate.png
+downImage = icons/sonic_blue/down.png
+downloadImage = icons/sonic_blue/download.png
+faviconImage = icons/sonic_blue/favicon.ico
+helpImage = icons/sonic_blue/help.png
+helpPopupImage = icons/sonic_blue/help_small.png
+homeImage = icons/sonic_blue/home.png
+logImage = icons/sonic_blue/log.png
+logoImage = icons/sonic_blue/logo.png
+moreImage = icons/sonic_blue/more.png
+nowPlayingImage = icons/sonic_blue/playing.png
+paypalImage = icons/sonic_blue/paypal.gif
+playImage = icons/sonic_blue/play.png
+podcastImage = icons/sonic_blue/podcast_small.png
+podcastLargeImage = icons/sonic_blue/podcast.png
+randomImage = icons/sonic_blue/random.png
+ratingOnImage = icons/sonic_blue/rating_on.png
+ratingOffImage = icons/sonic_blue/rating_off.png
+ratingHalfImage = icons/sonic_blue/rating_half.png
+removeImage = icons/sonic_blue/remove.png
+searchImage = icons/sonic_blue/search.png
+settingsImage = icons/sonic_blue/settings.png
+statusImage = icons/sonic_blue/status.png
+upImage = icons/sonic_blue/up.png
+uploadImage = icons/sonic_blue/upload.png
+volumeImage = icons/sonic_blue/now_playing.png
+wapImage = icons/sonic_blue/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_white.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_white.properties
new file mode 100644
index 00000000..d0c86fda
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/sonic_white.properties
@@ -0,0 +1,44 @@
+# Definition of the "Sonic White" theme.
+# Author: Thomas Bruce Dyrud
+
+# Include the css
+styleSheet = style/sonic_white.css
+
+# Flash Player colours
+backgroundColor = FFFFFF
+textColor = 696969
+detailColor = 333333
+linkColor = 333333
+
+# Set the images for theme.
+addImage = icons/sonic_white/add.png
+clearRatingImage = icons/sonic_white/clear_rating.png
+currentImage = icons/sonic_white/now_playing.png
+donateImage = icons/sonic_white/donate.png
+downImage = icons/sonic_white/down.png
+downloadImage = icons/sonic_white/download.png
+faviconImage = icons/sonic_white/favicon.ico
+helpImage = icons/sonic_white/help.png
+helpPopupImage = icons/sonic_white/help_small.png
+homeImage = icons/sonic_white/home.png
+logImage = icons/sonic_white/log.png
+logoImage = icons/sonic_white/logo.png
+moreImage = icons/sonic_white/more.png
+nowPlayingImage = icons/sonic_white/playing.png
+paypalImage = icons/sonic_white/paypal.gif
+playImage = icons/sonic_white/play.png
+podcastImage = icons/sonic_white/podcast_small.png
+podcastLargeImage = icons/sonic_white/podcast.png
+randomImage = icons/sonic_white/random.png
+ratingOnImage = icons/sonic_white/rating_on.png
+ratingOffImage = icons/sonic_white/rating_off.png
+ratingHalfImage = icons/sonic_white/rating_half.png
+removeImage = icons/sonic_white/remove.png
+searchImage = icons/sonic_white/search.png
+settingsImage = icons/sonic_white/settings.png
+statusImage = icons/sonic_white/status.png
+upImage = icons/sonic_white/up.png
+uploadImage = icons/sonic_white/upload.png
+volumeImage = icons/sonic_white/now_playing.png
+wapImage = icons/sonic_white/phone.png
+
diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/themes.txt b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/themes.txt
new file mode 100644
index 00000000..652096f1
--- /dev/null
+++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/theme/themes.txt
@@ -0,0 +1,56 @@
+# List of available themes.
+# Author: Sindre Mehus
+
+# The original Subsonic theme.
+default "Subsonic Default"
+
+# Theme with dark colors.
+midnight "2 Minutes To Midnight"
+
+# Theme with even darker colors.
+black "Back In Black"
+
+# Theme with sandy colors.
+sandstorm "Sandstorm"
+
+# Similar to HD, but with normal fonts.
+barents "Barents Sea"
+
+# Themes designed for large HDTV screens.
+hd720 "HD-720"
+hd768 "HD-768"
+hd1080 "HD-1080"
+
+# Default theme when pre-installed on Ripserver NAS.
+ripserver "Ripserver"
+
+# Midnight fun theme, courtesy of Don Pearson http://www.midnightfun.co.uk/
+midnightfun "Midnight Fun"
+
+# Theme by Dan Eriksen
+coolandclean "Cool and Clean"
+
+# Theme by David D.
+monochrome "Monochrome"
+
+# Theme by David D.
+monochrome_black "Monochrome (Black)"
+
+# Themes by Thomas Bruce Dyrud
+groove "Groove"
+groove_simple "Groove (Simple)"
+simplify "Simplify"
+pinkpanther "PinkPanther"
+denim "Denim"
+sonic "Sonic"
+sonic_blue "Sonic (Blue)"
+sonic_white "Sonic (White)"
+
+# Themes by Jeebs (Fisher Evans)
+slick "Slick"
+hicon "High Contrast"
+hiconi "High Contrast (Inverted)"
+hitech "High-Tech"
+
+# BUUF Theme by Fractal Systems - Based on icons by Paul Davey aka Mattahan. All rights reserved.
+buuftheme "BUUF Theme"
diff --git a/subsonic-main/src/main/webapp/WEB-INF/applicationContext-cache.xml b/subsonic-main/src/main/webapp/WEB-INF/applicationContext-cache.xml
new file mode 100644
index 00000000..a8692d08
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/applicationContext-cache.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+
+ <bean id="cacheFactory" class="net.sourceforge.subsonic.cache.CacheFactory"/>
+
+ <bean id="userCache" factory-bean="cacheFactory" factory-method="getCache">
+ <constructor-arg value="userCache"/>
+ </bean>
+
+ <bean id="mediaFileMemoryCache" factory-bean="cacheFactory" factory-method="getCache">
+ <constructor-arg value="mediaFileMemoryCache"/>
+ </bean>
+
+ <bean id="musicFileMemoryCache" factory-bean="cacheFactory" factory-method="getCache">
+ <constructor-arg value="musicFileMemoryCache"/>
+ </bean>
+
+</beans>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/applicationContext-security.xml b/subsonic-main/src/main/webapp/WEB-INF/applicationContext-security.xml
new file mode 100644
index 00000000..8cf5e9c2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/applicationContext-security.xml
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
+ <property name="filterInvocationDefinitionSource">
+ <value>
+ PATTERN_TYPE_APACHE_ANT
+ /wap**=httpSessionContextIntegrationFilter,logoutFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,basicExceptionTranslationFilter,filterInvocationInterceptor
+ /podcastReceiver**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
+ /podcast**=httpSessionContextIntegrationFilter,logoutFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,basicExceptionTranslationFilter,filterInvocationInterceptor
+ /rest/**=httpSessionContextIntegrationFilter,logoutFilter,basicProcessingFilter,restRequestParameterProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,basicExceptionTranslationFilter,filterInvocationInterceptor
+ /**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
+ </value>
+ </property>
+ </bean>
+
+ <bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>
+
+ <bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
+ <constructor-arg value="/login.view?logout"/>
+ <!-- URL redirected to after logout -->
+ <constructor-arg>
+ <list>
+ <ref bean="rememberMeServices"/>
+ <bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
+ </list>
+ </constructor-arg>
+ </bean>
+
+ <bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
+ <property name="authenticationManager" ref="authenticationManager"/>
+ <property name="authenticationFailureUrl" value="/login.view?error"/>
+ <property name="defaultTargetUrl" value="/"/>
+ <property name="alwaysUseDefaultTargetUrl" value="true"/>
+ <property name="filterProcessesUrl" value="/j_acegi_security_check"/>
+ <property name="rememberMeServices" ref="rememberMeServices"/>
+ </bean>
+
+ <bean id="basicProcessingFilter" class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
+ <property name="authenticationManager" ref="authenticationManager"/>
+ <property name="authenticationEntryPoint" ref="basicProcessingFilterEntryPoint"/>
+ </bean>
+
+ <bean id="restRequestParameterProcessingFilter" class="net.sourceforge.subsonic.security.RESTRequestParameterProcessingFilter">
+ <property name="authenticationManager" ref="authenticationManager"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+
+ <bean id="basicProcessingFilterEntryPoint" class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
+ <property name="realmName" value="Subsonic"/>
+ </bean>
+
+ <bean id="securityContextHolderAwareRequestFilter" class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"/>
+
+ <bean id="rememberMeProcessingFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
+ <property name="authenticationManager" ref="authenticationManager"/>
+ <property name="rememberMeServices" ref="rememberMeServices"/>
+ </bean>
+
+ <bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
+ <property name="key" value="subsonic"/>
+ <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
+ </bean>
+
+ <bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
+ <property name="authenticationEntryPoint">
+ <bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
+ <property name="loginFormUrl" value="/login.view?"/>
+ <property name="forceHttps" value="false"/>
+ </bean>
+ </property>
+ <property name="accessDeniedHandler">
+ <bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
+ <property name="errorPage" value="/accessDenied.view"/>
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="basicExceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
+ <property name="authenticationEntryPoint">
+ <bean class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
+ <property name="realmName" value="Subsonic"/>
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
+ <property name="authenticationManager" ref="authenticationManager"/>
+ <property name="alwaysReauthenticate" value="true"/>
+ <property name="accessDecisionManager" ref="accessDecisionManager"/>
+ <property name="objectDefinitionSource">
+ <value>
+ PATTERN_TYPE_APACHE_ANT
+
+ /login.view=IS_AUTHENTICATED_ANONYMOUSLY
+ /recover.view=IS_AUTHENTICATED_ANONYMOUSLY
+ /accessDenied.view=IS_AUTHENTICATED_ANONYMOUSLY
+ /videoPlayer.view=IS_AUTHENTICATED_ANONYMOUSLY
+ /coverArt.view=IS_AUTHENTICATED_ANONYMOUSLY
+ /stream/**=IS_AUTHENTICATED_ANONYMOUSLY
+ /share/**=IS_AUTHENTICATED_ANONYMOUSLY
+ /style/**=IS_AUTHENTICATED_ANONYMOUSLY
+ /icons/**=IS_AUTHENTICATED_ANONYMOUSLY
+ /flash/**=IS_AUTHENTICATED_ANONYMOUSLY
+ /script/**=IS_AUTHENTICATED_ANONYMOUSLY
+ /crossdomain.xml=IS_AUTHENTICATED_ANONYMOUSLY
+
+ /personalSettings.view=ROLE_SETTINGS
+ /passwordSettings.view=ROLE_SETTINGS
+ /playerSettings.view=ROLE_SETTINGS
+ /shareSettings.view=ROLE_SETTINGS
+
+ /generalSettings.view=ROLE_ADMIN
+ /advancedSettings.view=ROLE_ADMIN
+ /userSettings.view=ROLE_ADMIN
+ /musicFolderSettings.view=ROLE_ADMIN
+ /networkSettings.view=ROLE_ADMIN
+ /transcodingSettings.view=ROLE_ADMIN
+ /internetRadioSettings.view=ROLE_ADMIN
+ /podcastSettings.view=ROLE_ADMIN
+ /db.view=ROLE_ADMIN
+
+ /deletePlaylist.view=ROLE_PLAYLIST
+ /savePlaylist.view=ROLE_PLAYLIST
+
+ /download.view=ROLE_DOWNLOAD
+
+ /upload.view=ROLE_UPLOAD
+
+ /createShare.view=ROLE_SHARE
+
+ /changeCoverArt.view=ROLE_COVERART
+ /editTags.view=ROLE_COVERART
+
+ /setMusicFileInfo.view=ROLE_COMMENT
+
+ /podcastReceiverAdmin.view=ROLE_PODCAST
+
+ /**=IS_AUTHENTICATED_REMEMBERED
+ </value>
+ </property>
+ </bean>
+
+ <bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
+ <property name="allowIfAllAbstainDecisions" value="false"/>
+ <property name="decisionVoters">
+ <list>
+ <bean class="org.acegisecurity.vote.RoleVoter"/>
+ <bean class="org.acegisecurity.vote.AuthenticatedVoter"/>
+ </list>
+ </property>
+ </bean>
+
+ <bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
+ <property name="userDetailsService" ref="securityService"/>
+ <property name="tokenValiditySeconds" value="31536000"/>
+ <!-- One year -->
+ <property name="key" value="subsonic"/>
+ </bean>
+
+ <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
+ <property name="providers">
+ <list>
+ <ref local="daoAuthenticationProvider"/>
+ <ref local="ldapAuthenticationProvider"/>
+ <bean class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
+ <property name="key" value="subsonic"/>
+ </bean>
+ <bean class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
+ <property name="key" value="subsonic"/>
+ </bean>
+ </list>
+ </property>
+ </bean>
+
+ <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
+ <property name="userDetailsService" ref="securityService"/>
+ <property name="userCache" ref="userCacheWrapper"/>
+ </bean>
+
+ <bean id="userCacheWrapper" class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
+ <property name="cache" ref="userCache"/>
+ </bean>
+
+ <bean id="ldapAuthenticationProvider" class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">
+ <constructor-arg ref="bindAuthenticator"/>
+ <constructor-arg ref="userDetailsServiceBasedAuthoritiesPopulator"/>
+ <property name="userCache" ref="userCacheWrapper"/>
+ </bean>
+
+ <bean id="bindAuthenticator" class="net.sourceforge.subsonic.ldap.SubsonicLdapBindAuthenticator">
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+
+ <bean id="userDetailsServiceBasedAuthoritiesPopulator"
+ class="net.sourceforge.subsonic.ldap.UserDetailsServiceBasedAuthoritiesPopulator">
+ <property name="userDetailsService" ref="securityService"/>
+ </bean>
+
+ <!-- Authorization of AJAX services. -->
+ <bean id="ajaxServiceInterceptor" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
+ <property name="authenticationManager" ref="authenticationManager"/>
+ <property name="accessDecisionManager" ref="accessDecisionManager"/>
+ <property name="objectDefinitionSource">
+ <value>
+ net.sourceforge.subsonic.ajax.TagService.setTags=ROLE_COVERART
+ net.sourceforge.subsonic.ajax.TransferService.getUploadInfo=ROLE_UPLOAD
+ </value>
+ </property>
+ </bean>
+
+ <bean id="ajaxTagServiceSecure" class="org.springframework.aop.framework.ProxyFactoryBean">
+ <property name="target" ref="ajaxTagService"/>
+ <property name="interceptorNames">
+ <list>
+ <idref local="ajaxServiceInterceptor"/>
+ </list>
+ </property>
+ </bean>
+
+ <bean id="ajaxTransferServiceSecure" class="org.springframework.aop.framework.ProxyFactoryBean">
+ <property name="target" ref="ajaxTransferService"/>
+ <property name="interceptorNames">
+ <list>
+ <idref local="ajaxServiceInterceptor"/>
+ </list>
+ </property>
+ </bean>
+
+</beans> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/applicationContext-service.xml b/subsonic-main/src/main/webapp/WEB-INF/applicationContext-service.xml
new file mode 100644
index 00000000..95f76359
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/applicationContext-service.xml
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <!-- DAO's -->
+
+ <bean id="playerDao" class="net.sourceforge.subsonic.dao.PlayerDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="mediaFileDao" class="net.sourceforge.subsonic.dao.MediaFileDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="artistDao" class="net.sourceforge.subsonic.dao.ArtistDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="albumDao" class="net.sourceforge.subsonic.dao.AlbumDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="playlistDao" class="net.sourceforge.subsonic.dao.PlaylistDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="internetRadioDao" class="net.sourceforge.subsonic.dao.InternetRadioDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="musicFileInfoDao" class="net.sourceforge.subsonic.dao.RatingDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="musicFolderDao" class="net.sourceforge.subsonic.dao.MusicFolderDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="userDao" class="net.sourceforge.subsonic.dao.UserDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="transcodingDao" class="net.sourceforge.subsonic.dao.TranscodingDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="podcastDao" class="net.sourceforge.subsonic.dao.PodcastDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="avatarDao" class="net.sourceforge.subsonic.dao.AvatarDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="shareDao" class="net.sourceforge.subsonic.dao.ShareDao">
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+
+ <bean id="daoHelper" class="net.sourceforge.subsonic.dao.DaoHelper"/>
+
+
+ <!-- Services -->
+
+ <bean id="mediaFileService" class="net.sourceforge.subsonic.service.MediaFileService">
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileMemoryCache" ref="mediaFileMemoryCache"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ <property name="albumDao" ref="albumDao"/>
+ <property name="metaDataParserFactory" ref="metaDataParserFactory"/>
+ </bean>
+
+ <bean id="securityService" class="net.sourceforge.subsonic.service.SecurityService">
+ <property name="settingsService" ref="settingsService"/>
+ <property name="userDao" ref="userDao"/>
+ <property name="userCache" ref="userCache"/>
+ </bean>
+
+ <bean id="settingsService" class="net.sourceforge.subsonic.service.SettingsService" init-method="init">
+ <property name="internetRadioDao" ref="internetRadioDao"/>
+ <property name="musicFolderDao" ref="musicFolderDao"/>
+ <property name="userDao" ref="userDao"/>
+ <property name="avatarDao" ref="avatarDao"/>
+ <property name="versionService" ref="versionService"/>
+ </bean>
+
+ <bean id="mediaScannerService" class="net.sourceforge.subsonic.service.MediaScannerService" init-method="init" depends-on="metaDataParserFactory">
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ <property name="artistDao" ref="artistDao"/>
+ <property name="albumDao" ref="albumDao"/>
+ <property name="searchService" ref="searchService"/>
+ </bean>
+
+ <bean id="searchService" class="net.sourceforge.subsonic.service.SearchService">
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="artistDao" ref="artistDao"/>
+ <property name="albumDao" ref="albumDao"/>
+ </bean>
+
+ <bean id="networkService" class="net.sourceforge.subsonic.service.NetworkService" init-method="init">
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+
+ <bean id="playerService" class="net.sourceforge.subsonic.service.PlayerService" init-method="init">
+ <property name="playerDao" ref="playerDao"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ </bean>
+
+ <bean id="playlistService" class="net.sourceforge.subsonic.service.PlaylistService" init-method="init">
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ <property name="playlistDao" ref="playlistDao"/>
+ </bean>
+
+ <bean id="versionService" class="net.sourceforge.subsonic.service.VersionService"/>
+
+ <bean id="statusService" class="net.sourceforge.subsonic.service.StatusService"/>
+
+ <bean id="musicInfoService" class="net.sourceforge.subsonic.service.RatingService">
+ <property name="ratingDao" ref="musicFileInfoDao"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+
+ <bean id="musicIndexService" class="net.sourceforge.subsonic.service.MusicIndexService">
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ </bean>
+
+ <bean id="audioScrobblerService" class="net.sourceforge.subsonic.service.AudioScrobblerService">
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+
+ <bean id="transcodingService" class="net.sourceforge.subsonic.service.TranscodingService">
+ <property name="transcodingDao" ref="transcodingDao"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="playerService" ref="playerService"/>
+ </bean>
+
+ <bean id="shareService" class="net.sourceforge.subsonic.service.ShareService">
+ <property name="shareDao" ref="shareDao"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+
+ <bean id="podcastService" class="net.sourceforge.subsonic.service.PodcastService" init-method="init">
+ <property name="podcastDao" ref="podcastDao"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+
+ <bean id="adService" class="net.sourceforge.subsonic.service.AdService">
+ <property name="adInterval" value="4"/>
+ </bean>
+
+ <bean id="jukeboxService" class="net.sourceforge.subsonic.service.JukeboxService">
+ <property name="statusService" ref="statusService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ <property name="audioScrobblerService" ref="audioScrobblerService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+
+ <bean id="metaDataParserFactory" class="net.sourceforge.subsonic.service.metadata.MetaDataParserFactory">
+ <property name="parsers">
+ <list>
+ <bean class="net.sourceforge.subsonic.service.metadata.JaudiotaggerParser"/>
+ <bean class="net.sourceforge.subsonic.service.metadata.FFmpegParser">
+ <property name="transcodingService" ref="transcodingService"/>
+ </bean>
+ <bean class="net.sourceforge.subsonic.service.metadata.DefaultMetaDataParser"/>
+ </list>
+ </property>
+ </bean>
+
+ <!-- AJAX services -->
+
+ <bean id="ajaxMultiService" class="net.sourceforge.subsonic.ajax.MultiService">
+ <property name="networkService" ref="networkService"/>
+ </bean>
+
+ <bean id="ajaxNowPlayingService" class="net.sourceforge.subsonic.ajax.NowPlayingService">
+ <property name="playerService" ref="playerService"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaScannerService" ref="mediaScannerService"/>
+ </bean>
+
+ <bean id="ajaxPlayQueueService" class="net.sourceforge.subsonic.ajax.PlayQueueService">
+ <property name="playerService" ref="playerService"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ <property name="jukeboxService" ref="jukeboxService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+
+ <bean id="ajaxPlaylistService" class="net.sourceforge.subsonic.ajax.PlaylistService">
+ <property name="playlistService" ref="playlistService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ </bean>
+
+ <bean id="ajaxLyricsService" class="net.sourceforge.subsonic.ajax.LyricsService"/>
+
+ <bean id="ajaxCoverArtService" class="net.sourceforge.subsonic.ajax.CoverArtService">
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+
+ <bean id="ajaxStarService" class="net.sourceforge.subsonic.ajax.StarService">
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ </bean>
+
+ <bean id="ajaxTagService" class="net.sourceforge.subsonic.ajax.TagService">
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="metaDataParserFactory" ref="metaDataParserFactory"/>
+ </bean>
+
+ <bean id="ajaxTransferService" class="net.sourceforge.subsonic.ajax.TransferService"/>
+
+ <bean id="ajaxChatService" class="net.sourceforge.subsonic.ajax.ChatService" init-method="init">
+ <property name="securityService" ref="securityService"/>
+ </bean>
+
+</beans>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/dwr.xml b/subsonic-main/src/main/webapp/WEB-INF/dwr.xml
new file mode 100644
index 00000000..e7ea3187
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/dwr.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 3.0//EN" "http://getahead.org/dwr/dwr30.dtd">
+
+<dwr>
+ <allow>
+
+ <create creator="spring" javascript="multiService">
+ <param name="beanName" value="ajaxMultiService"/>
+ </create>
+
+ <create creator="spring" javascript="nowPlayingService">
+ <param name="beanName" value="ajaxNowPlayingService"/>
+ </create>
+
+ <create creator="spring" javascript="playQueueService">
+ <param name="beanName" value="ajaxPlayQueueService"/>
+ </create>
+
+ <create creator="spring" javascript="playlistService">
+ <param name="beanName" value="ajaxPlaylistService"/>
+ </create>
+
+ <create creator="spring" javascript="lyricsService">
+ <param name="beanName" value="ajaxLyricsService"/>
+ </create>
+
+ <create creator="spring" javascript="coverArtService">
+ <param name="beanName" value="ajaxCoverArtService"/>
+ </create>
+
+ <create creator="spring" javascript="starService">
+ <param name="beanName" value="ajaxStarService"/>
+ </create>
+
+ <create creator="spring" javascript="tagService">
+ <param name="beanName" value="ajaxTagService"/>
+ </create>
+
+ <create creator="spring" javascript="transferService">
+ <param name="beanName" value="ajaxTransferService"/>
+ </create>
+
+ <create creator="spring" javascript="chatService">
+ <param name="beanName" value="ajaxChatService"/>
+ </create>
+
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.NetworkStatus"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.NowPlayingInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.ScanInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.PlayQueueInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.PlayQueueInfo$Entry"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.PlaylistInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.PlaylistInfo$Entry"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.domain.Playlist"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.UploadInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.LyricsInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.CoverArtInfo"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.ChatService$Message"/>
+ <convert converter="bean" match="net.sourceforge.subsonic.ajax.ChatService$Messages"/>
+
+ </allow>
+</dwr> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/accessDenied.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/accessDenied.jsp
new file mode 100644
index 00000000..78d2b910
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/accessDenied.jsp
@@ -0,0 +1,22 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+</head>
+
+<body class="mainframe bgcolor1">
+
+<h1>
+ <img src="<spring:theme code="errorImage"/>" alt=""/>
+ <fmt:message key="accessDenied.title"/>
+</h1>
+
+<p>
+ <fmt:message key="accessDenied.text"/>
+</p>
+
+<div class="back"><a href="javascript:history.go(-1)"><fmt:message key="common.back"/></a></div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp
new file mode 100644
index 00000000..9b3a95b1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/advancedSettings.jsp
@@ -0,0 +1,142 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+ function enableLdapFields() {
+ var checkbox = $("ldap");
+ var table = $("ldapTable");
+
+ if (checkbox && checkbox.checked) {
+ table.show();
+ } else {
+ table.hide();
+ }
+ }
+ </script>
+</head>
+
+<body class="mainframe bgcolor1" onload="enableLdapFields()">
+<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
+<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="advanced"/>
+</c:import>
+
+<form:form method="post" action="advancedSettings.view" commandName="command">
+
+ <table style="white-space:nowrap" class="indent">
+
+ <tr>
+ <td><fmt:message key="advancedsettings.downsamplecommand"/></td>
+ <td>
+ <form:input path="downsampleCommand" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="downsamplecommand"/></c:import>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">&nbsp;</td></tr>
+
+ <tr>
+ <td><fmt:message key="advancedsettings.coverartlimit"/></td>
+ <td>
+ <form:input path="coverArtLimit" size="8"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="coverartlimit"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="advancedsettings.downloadlimit"/></td>
+ <td>
+ <form:input path="downloadLimit" size="8"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="downloadlimit"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="advancedsettings.uploadlimit"/></td>
+ <td>
+ <form:input path="uploadLimit" size="8"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="uploadlimit"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="advancedsettings.streamport"/></td>
+ <td>
+ <form:input path="streamPort" size="8"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="streamport"/></c:import>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">&nbsp;</td></tr>
+
+ <tr>
+ <td colspan="2">
+ <form:checkbox path="ldapEnabled" id="ldap" cssClass="checkbox" onclick="javascript:enableLdapFields()"/>
+ <label for="ldap"><fmt:message key="advancedsettings.ldapenabled"/></label>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="ldap"/></c:import>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">
+ <table class="indent" id="ldapTable" style="padding-left:2em">
+ <tr>
+ <td><fmt:message key="advancedsettings.ldapurl"/></td>
+ <td colspan="3">
+ <form:input path="ldapUrl" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="ldapurl"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="advancedsettings.ldapsearchfilter"/></td>
+ <td colspan="3">
+ <form:input path="ldapSearchFilter" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="ldapsearchfilter"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="advancedsettings.ldapmanagerdn"/></td>
+ <td>
+ <form:input path="ldapManagerDn" size="20"/>
+ </td>
+ <td><fmt:message key="advancedsettings.ldapmanagerpassword"/></td>
+ <td>
+ <form:password path="ldapManagerPassword" size="20"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="ldapmanagerdn"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="5">
+ <form:checkbox path="ldapAutoShadowing" id="ldapAutoShadowing" cssClass="checkbox"/>
+ <label for="ldapAutoShadowing"><fmt:message key="advancedsettings.ldapautoshadowing"><fmt:param value="${command.brand}"/></fmt:message></label>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="ldapautoshadowing"/></c:import>
+ </td>
+ </tr>
+ </table>
+ </td></tr>
+
+ <tr>
+ <td colspan="2" style="padding-top:1.5em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </td>
+ </tr>
+
+ </table>
+</form:form>
+
+<c:if test="${command.reloadNeeded}">
+ <script language="javascript" type="text/javascript">
+ parent.frames.left.location.href="left.view?";
+ parent.frames.playQueue.location.href="playQueue.view?";
+ </script>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/allmusic.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/allmusic.jsp
new file mode 100644
index 00000000..1af611fb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/allmusic.jsp
@@ -0,0 +1,16 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+
+<body onload="document.allmusic.submit();" class="mainframe bgcolor1">
+<h2><fmt:message key="allmusic.text"><fmt:param value="${album}"/></fmt:message></h2>
+
+<form name="allmusic" action="http://www.allmusic.com/search" method="POST" accept-charset="iso-8859-1">
+ <input type="hidden" name="search_term" value="${album}"/>
+ <input type="hidden" name="search_type" value="album"/>
+</form>
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/avatarUploadResult.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/avatarUploadResult.jsp
new file mode 100644
index 00000000..6c3b1010
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/avatarUploadResult.jsp
@@ -0,0 +1,35 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<h1>
+ <img src="<spring:theme code="settingsImage"/>" alt=""/>
+ <fmt:message key="avataruploadresult.title"/>
+</h1>
+
+<c:choose>
+ <c:when test="${empty model.error}">
+ <p>
+ <fmt:message key="avataruploadresult.success"><fmt:param value="${model.avatar.name}"/></fmt:message>
+ <sub:url value="avatar.view" var="avatarUrl">
+ <sub:param name="username" value="${model.username}"/>
+ </sub:url>
+ <img src="${avatarUrl}" alt="${model.avatar.name}" width="${model.avatar.width}"
+ height="${model.avatar.height}" style="padding-left:2em"/>
+ </p>
+ </c:when>
+ <c:otherwise>
+ <p class="warning">
+ <fmt:message key="avataruploadresult.failure"/>
+ </p>
+ </c:otherwise>
+</c:choose>
+
+<div class="back"><a href="personalSettings.view?"><fmt:message key="common.back"/></a></div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/changeCoverArt.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/changeCoverArt.jsp
new file mode 100644
index 00000000..598f12ca
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/changeCoverArt.jsp
@@ -0,0 +1,206 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/coverArtService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+ <script type="text/javascript" src="https://www.google.com/jsapi"></script>
+
+ <script type="text/javascript" language="javascript">
+
+ dwr.engine.setErrorHandler(null);
+ google.load('search', '1');
+ var imageSearch;
+
+ function setImage(imageUrl) {
+ $("wait").show();
+ $("result").hide();
+ $("success").hide();
+ $("error").hide();
+ $("errorDetails").hide();
+ $("noImagesFound").hide();
+ var id = dwr.util.getValue("id");
+ coverArtService.setCoverArtImage(id, imageUrl, setImageComplete);
+ }
+
+ function setImageComplete(errorDetails) {
+ $("wait").hide();
+ if (errorDetails != null) {
+ dwr.util.setValue("errorDetails", "<br/>" + errorDetails, { escapeHtml:false });
+ $("error").show();
+ $("errorDetails").show();
+ } else {
+ $("success").show();
+ }
+ }
+
+ function searchComplete() {
+
+ $("wait").hide();
+
+ if (imageSearch.results && imageSearch.results.length > 0) {
+
+ var images = $("images");
+ images.innerHTML = "";
+
+ var results = imageSearch.results;
+ for (var i = 0; i < results.length; i++) {
+ var result = results[i];
+ var node = $("template").cloneNode(true);
+
+ var link = node.getElementsByClassName("search-result-link")[0];
+ link.href = "javascript:setImage('" + result.url + "');";
+
+ var thumbnail = node.getElementsByClassName("search-result-thumbnail")[0];
+ thumbnail.src = result.tbUrl;
+
+ var title = node.getElementsByClassName("search-result-title")[0];
+ title.innerHTML = result.contentNoFormatting.truncate(30);
+
+ var dimension = node.getElementsByClassName("search-result-dimension")[0];
+ dimension.innerHTML = result.width + " × " + result.height;
+
+ var url = node.getElementsByClassName("search-result-url")[0];
+ url.innerHTML = result.visibleUrl;
+
+ node.show();
+ images.appendChild(node);
+ }
+
+ $("result").show();
+
+ addPaginationLinks(imageSearch);
+
+ } else {
+ $("noImagesFound").show();
+ }
+ }
+
+ function addPaginationLinks() {
+
+ // To paginate search results, use the cursor function.
+ var cursor = imageSearch.cursor;
+ var curPage = cursor.currentPageIndex; // check what page the app is on
+ var pagesDiv = document.createElement("div");
+ for (var i = 0; i < cursor.pages.length; i++) {
+ var page = cursor.pages[i];
+ var label;
+ if (curPage == i) {
+ // If we are on the current page, then don"t make a link.
+ label = document.createElement("b");
+ } else {
+
+ // Create links to other pages using gotoPage() on the searcher.
+ label = document.createElement("a");
+ label.href = "javascript:imageSearch.gotoPage(" + i + ");";
+ }
+ label.innerHTML = page.label;
+ label.style.marginRight = "1em";
+ pagesDiv.appendChild(label);
+ }
+
+ // Create link to next page.
+ if (curPage < cursor.pages.length - 1) {
+ var next = document.createElement("a");
+ next.href = "javascript:imageSearch.gotoPage(" + (curPage + 1) + ");";
+ next.innerHTML = "<fmt:message key="common.next"/>";
+ next.style.marginLeft = "1em";
+ pagesDiv.appendChild(next);
+ }
+
+ var pages = $("pages");
+ pages.innerHTML = "";
+ pages.appendChild(pagesDiv);
+ }
+
+ function search() {
+
+ $("wait").show();
+ $("result").hide();
+ $("success").hide();
+ $("error").hide();
+ $("errorDetails").hide();
+ $("noImagesFound").hide();
+
+ var query = dwr.util.getValue("query");
+ imageSearch.execute(query);
+ }
+
+ function onLoad() {
+
+ imageSearch = new google.search.ImageSearch();
+ imageSearch.setSearchCompleteCallback(this, searchComplete, null);
+ imageSearch.setNoHtmlGeneration();
+ imageSearch.setResultSetSize(8);
+
+ google.search.Search.getBranding("branding");
+
+ $("template").hide();
+
+ search();
+ }
+ google.setOnLoadCallback(onLoad);
+
+
+ </script>
+</head>
+<body class="mainframe bgcolor1">
+<h1><fmt:message key="changecoverart.title"/></h1>
+<form action="javascript:search()">
+ <table class="indent"><tr>
+ <td><input id="query" name="query" size="70" type="text" value="${model.artist} ${model.album}" onclick="select()"/></td>
+ <td style="padding-left:0.5em"><input type="submit" value="<fmt:message key="changecoverart.search"/>"/></td>
+ </tr></table>
+</form>
+
+<form action="javascript:setImage(dwr.util.getValue('url'))">
+ <table><tr>
+ <input id="id" type="hidden" name="id" value="${model.id}"/>
+ <td><label for="url"><fmt:message key="changecoverart.address"/></label></td>
+ <td style="padding-left:0.5em"><input type="text" name="url" size="50" id="url" value="http://" onclick="select()"/></td>
+ <td style="padding-left:0.5em"><input type="submit" value="<fmt:message key="common.ok"/>"></td>
+ </tr></table>
+</form>
+<sub:url value="main.view" var="backUrl"><sub:param name="id" value="${model.id}"/></sub:url>
+<div style="padding-top:0.5em;padding-bottom:0.5em">
+ <div class="back"><a href="${backUrl}"><fmt:message key="common.back"/></a></div>
+</div>
+
+<h2 id="wait" style="display:none"><fmt:message key="changecoverart.wait"/></h2>
+<h2 id="noImagesFound" style="display:none"><fmt:message key="changecoverart.noimagesfound"/></h2>
+<h2 id="success" style="display:none"><fmt:message key="changecoverart.success"/></h2>
+<h2 id="error" style="display:none"><fmt:message key="changecoverart.error"/></h2>
+<div id="errorDetails" class="warning" style="display:none">
+</div>
+
+<div id="result">
+
+ <div id="pages" style="float:left;padding-left:0.5em;padding-top:0.5em">
+ </div>
+
+ <div id="branding" style="float:right;padding-right:1em;padding-top:0.5em">
+ </div>
+
+ <div style="clear:both;">
+ </div>
+
+ <div id="images" style="width:100%;padding-bottom:2em">
+ </div>
+
+ <div style="clear:both;">
+ </div>
+
+</div>
+
+<div id="template" style="float:left; height:190px; width:220px;padding:0.5em;position:relative">
+ <div style="position:absolute;bottom:0">
+ <a class="search-result-link"><img class="search-result-thumbnail" style="padding:1px; border:1px solid #021a40; background-color:white;"></a>
+ <div class="search-result-title"></div>
+ <div class="search-result-dimension detail"></div>
+ <div class="search-result-url detail"></div>
+ </div>
+</div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/coverArt.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/coverArt.jsp
new file mode 100644
index 00000000..a5030499
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/coverArt.jsp
@@ -0,0 +1,86 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%@ include file="include.jsp" %>
+
+<%--
+PARAMETERS
+ albumId: ID of album.
+ coverArtSize: Height and width of cover art.
+ albumName: Album name to display as caption and img alt.
+ showLink: Whether to make the cover art image link to the album page.
+ showZoom: Whether to display a link for zooming the cover art.
+ showChange: Whether to display a link for changing the cover art.
+ showCaption: Whether to display the album name as a caption below the image.
+ appearAfter: Fade in after this many milliseconds, or nil if no fading in should happen.
+--%>
+<c:choose>
+ <c:when test="${empty param.coverArtSize}">
+ <c:set var="size" value="auto"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="size" value="${param.coverArtSize + 8}px"/>
+ </c:otherwise>
+</c:choose>
+
+<c:set var="opacity" value="${empty param.appearAfter ? 1 : 0}"/>
+
+<div style="width:${size}; max-width:${size}; height:${size}; max-height:${size}" title="${param.albumName}">
+ <sub:url value="main.view" var="mainUrl">
+ <sub:param name="id" value="${param.albumId}"/>
+ </sub:url>
+
+ <sub:url value="/coverArt.view" var="coverArtUrl">
+ <c:if test="${not empty param.coverArtSize}">
+ <sub:param name="size" value="${param.coverArtSize}"/>
+ </c:if>
+ <sub:param name="id" value="${param.albumId}"/>
+ </sub:url>
+ <sub:url value="/coverArt.view" var="zoomCoverArtUrl">
+ <sub:param name="id" value="${param.albumId}"/>
+ </sub:url>
+
+ <str:randomString count="5" type="alphabet" var="divId"/>
+ <div class="outerpair1" id="${divId}" style="display:none">
+ <div class="outerpair2">
+ <div class="shadowbox">
+ <div class="innerbox">
+ <c:choose>
+ <c:when test="${param.showLink}"><a href="${mainUrl}" title="${param.albumName}"></c:when>
+ <c:when test="${param.showZoom}"><a href="${zoomCoverArtUrl}" rel="zoom" title="${param.albumName}"></c:when>
+ </c:choose>
+ <img src="${coverArtUrl}" alt="${param.albumName}">
+ <c:if test="${param.showLink or param.showZoom}"></a></c:if>
+ </div>
+ </div>
+ </div>
+ </div>
+ <c:if test="${not empty param.appearAfter}">
+ <script type="text/javascript">
+ if (window.addEventListener) {
+ window.addEventListener('load', function() {
+ setTimeout("$('#${divId}').fadeIn(500)", ${param.appearAfter});
+ }, false);
+ }
+ </script>
+ </c:if>
+</div>
+
+<div style="text-align:right; padding-right: 8px;">
+ <c:if test="${param.showChange}">
+ <sub:url value="/changeCoverArt.view" var="changeCoverArtUrl">
+ <sub:param name="id" value="${param.albumId}"/>
+ </sub:url>
+ <a class="detail" href="${changeCoverArtUrl}"><fmt:message key="coverart.change"/></a>
+ </c:if>
+
+ <c:if test="${param.showZoom and param.showChange}">
+ |
+ </c:if>
+
+ <c:if test="${param.showZoom}">
+ <a class="detail" rel="zoom" title="${param.albumName}" href="${zoomCoverArtUrl}"><fmt:message key="coverart.zoom"/></a>
+ </c:if>
+
+ <c:if test="${not param.showZoom and not param.showChange and param.showCaption}">
+ <span class="detail"><str:truncateNicely upper="17">${param.albumName}</str:truncateNicely></span>
+ </c:if>
+</div> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/createShare.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/createShare.jsp
new file mode 100644
index 00000000..3f739077
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/createShare.jsp
@@ -0,0 +1,51 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
+</head>
+<body class="mainframe bgcolor1">
+
+<h1><fmt:message key="share.title"/></h1>
+
+<c:choose>
+ <c:when test="${model.urlRedirectionEnabled}">
+ <fmt:message key="share.warning"/>
+ <p>
+ <a href="http://www.facebook.com/sharer.php?u=${model.playUrl}" target="_blank"><img src="<spring:theme code="shareFacebookImage"/>" alt=""></a>&nbsp;
+ <a href="http://www.facebook.com/sharer.php?u=${model.playUrl}" target="_blank"><fmt:message key="share.facebook"/></a>
+ </p>
+
+ <p>
+ <a href="http://twitter.com/?status=Listening to ${model.playUrl}" target="_blank"><img src="<spring:theme code="shareTwitterImage"/>" alt=""></a>&nbsp;
+ <a href="http://twitter.com/?status=Listening to ${model.playUrl}" target="_blank"><fmt:message key="share.twitter"/></a>
+ </p>
+ <p>
+ <g:plusone size="small" annotation="none" href="${model.playUrl}"></g:plusone>&nbsp;<fmt:message key="share.googleplus"/>
+ </p>
+ <p>
+ <fmt:message key="share.link">
+ <fmt:param>${model.playUrl}</fmt:param>
+ </fmt:message>
+ </p>
+ </c:when>
+ <c:otherwise>
+ <p>
+ <fmt:message key="share.disabled"/>
+ </p>
+ </c:otherwise>
+</c:choose>
+
+
+<div style="padding-top:1em">
+ <c:if test="${not empty model.dir}">
+ <sub:url value="main.view" var="backUrl"><sub:param name="path" value="${model.dir.path}"/></sub:url>
+ <div class="back" style="float:left;padding-right:10pt"><a href="${backUrl}"><fmt:message key="common.back"/></a></div>
+ </c:if>
+ <c:if test="${model.user.settingsRole}">
+ <div class="forward" style="float:left"><a href="shareSettings.view"><fmt:message key="share.manage"/></a></div>
+ </c:if>
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/db.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/db.jsp
new file mode 100644
index 00000000..52918ab1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/db.jsp
@@ -0,0 +1,45 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head><body class="mainframe bgcolor1">
+
+<h1>Database query</h1>
+
+<form method="post" action="db.view">
+ <textarea rows="10" cols="80" name="query" style="margin-top:1em">${model.query}</textarea>
+ <input type="submit" value="<fmt:message key="common.ok"/>">
+</form>
+
+<c:if test="${not empty model.result}">
+ <h1 style="margin-top:2em">Result</h1>
+
+ <table class="indent ruleTable">
+ <c:forEach items="${model.result}" var="row" varStatus="loopStatus">
+
+ <c:if test="${loopStatus.count == 1}">
+ <tr>
+ <c:forEach items="${row}" var="entry">
+ <td class="ruleTableHeader">${entry.key}</td>
+ </c:forEach>
+ </tr>
+ </c:if>
+ <tr>
+ <c:forEach items="${row}" var="entry">
+ <td class="ruleTableCell">${entry.value}</td>
+ </c:forEach>
+ </tr>
+ </c:forEach>
+
+ </table>
+</c:if>
+
+<c:if test="${not empty model.error}">
+ <h1 style="margin-top:2em">Error</h1>
+
+ <p class="warning">
+ ${model.error}
+ </p>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/donate.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/donate.jsp
new file mode 100644
index 00000000..77906cb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/donate.jsp
@@ -0,0 +1,147 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.DonateCommand"--%>
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<h1>
+ <img src="<spring:theme code="donateImage"/>" alt=""/>
+ <fmt:message key="donate.title"/>
+</h1>
+<c:if test="${not empty command.path}">
+ <sub:url value="main.view" var="backUrl">
+ <sub:param name="path" value="${command.path}"/>
+ </sub:url>
+ <div class="back"><a href="${backUrl}">
+ <fmt:message key="common.back"/>
+ </a></div>
+ <br/>
+</c:if>
+
+<c:url value="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E5RNJMDJ7C862" var="donate10Url"/>
+<c:url value="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CKRS9A4J99TFN" var="donate15Url"/>
+<c:url value="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=H79PAZLVHFT6E" var="donate20Url"/>
+<c:url value="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2TGXFN7AVREEN" var="donate25Url"/>
+<c:url value="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=BXJVAQALLFREC" var="donate30Url"/>
+<c:url value="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=M5PX55AC4ER9Y" var="donate50Url"/>
+
+<div style="width:50em; max-width:50em">
+
+<fmt:message key="donate.textbefore"><fmt:param value="${command.brand}"/></fmt:message>
+
+<table cellpadding="10">
+ <tr>
+ <td>
+ <table>
+ <tr>
+ <td><a href="${donate10Url}" target="_blank"><img src="<spring:theme code="paypalImage"/>" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td class="detail" style="text-align:center;"><fmt:message key="donate.amount"><fmt:param value="&euro;10"/></fmt:message></td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td><a href="${donate15Url}" target="_blank"><img src="<spring:theme code="paypalImage"/>" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td class="detail" style="text-align:center;"><fmt:message key="donate.amount"><fmt:param value="&euro;15"/></fmt:message></td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td><a href="${donate20Url}" target="_blank"><img src="<spring:theme code="paypalImage"/>" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td class="detail" style="text-align:center;"><fmt:message key="donate.amount"><fmt:param value="&euro;20"/></fmt:message></td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td><a href="${donate25Url}" target="_blank"><img src="<spring:theme code="paypalImage"/>" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td class="detail" style="text-align:center;"><fmt:message key="donate.amount"><fmt:param value="&euro;25"/></fmt:message></td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td><a href="${donate30Url}" target="_blank"><img src="<spring:theme code="paypalImage"/>" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td class="detail" style="text-align:center;"><fmt:message key="donate.amount"><fmt:param value="&euro;30"/></fmt:message></td>
+ </tr>
+ </table>
+ </td>
+ <td>
+ <table>
+ <tr>
+ <td><a href="${donate50Url}" target="_blank"><img src="<spring:theme code="paypalImage"/>" alt=""/></a> </td>
+ </tr>
+ <tr>
+ <td class="detail" style="text-align:center;"><fmt:message key="donate.amount"><fmt:param value="&euro;50"/></fmt:message></td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+</table>
+
+<fmt:message key="donate.textafter"/>
+
+<c:choose>
+ <c:when test="${command.licenseValid}">
+ <p>
+ <b>
+ <fmt:formatDate value="${command.licenseDate}" dateStyle="long" var="licenseDate"/>
+ <fmt:message key="donate.licensed">
+ <fmt:param value="${command.emailAddress}"/>
+ <fmt:param value="${licenseDate}"/>
+ <fmt:param value="${command.brand}"/>
+ </fmt:message>
+ </p>
+ </c:when>
+ <c:otherwise>
+
+ <p><fmt:message key="donate.register"/></p>
+
+ <form:form commandName="command" method="post" action="donate.view">
+ <form:hidden path="path"/>
+ <table>
+ <tr>
+ <td><fmt:message key="donate.register.email"/></td>
+ <td>
+ <form:input path="emailAddress" size="40"/>
+ </td>
+ </tr>
+ <tr>
+ <td><fmt:message key="donate.register.license"/></td>
+ <td>
+ <form:input path="license" size="40"/>
+ </td>
+ <td><input type="submit" value="<fmt:message key="common.ok"/>"/></td>
+ </tr>
+ <tr>
+ <td/>
+ <td class="warning"><form:errors path="license"/></td>
+ </tr>
+ </table>
+ </form:form>
+
+ <p><fmt:message key="donate.resend"/></p>
+
+ </c:otherwise>
+</c:choose>
+
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/editTags.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/editTags.jsp
new file mode 100644
index 00000000..7d95edd9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/editTags.jsp
@@ -0,0 +1,164 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/tagService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+</head>
+<body class="mainframe bgcolor1">
+
+<script type="text/javascript" language="javascript">
+ var index = 0;
+ var fileCount = ${fn:length(model.songs)};
+ function setArtist() {
+ var artist = dwr.util.getValue("artistAll");
+ for (i = 0; i < fileCount; i++) {
+ dwr.util.setValue("artist" + i, artist);
+ }
+ }
+ function setAlbum() {
+ var album = dwr.util.getValue("albumAll");
+ for (i = 0; i < fileCount; i++) {
+ dwr.util.setValue("album" + i, album);
+ }
+ }
+ function setYear() {
+ var year = dwr.util.getValue("yearAll");
+ for (i = 0; i < fileCount; i++) {
+ dwr.util.setValue("year" + i, year);
+ }
+ }
+ function setGenre() {
+ var genre = dwr.util.getValue("genreAll");
+ for (i = 0; i < fileCount; i++) {
+ dwr.util.setValue("genre" + i, genre);
+ }
+ }
+ function suggestTitle() {
+ for (i = 0; i < fileCount; i++) {
+ var title = dwr.util.getValue("suggestedTitle" + i);
+ dwr.util.setValue("title" + i, title);
+ }
+ }
+ function resetTitle() {
+ for (i = 0; i < fileCount; i++) {
+ var title = dwr.util.getValue("originalTitle" + i);
+ dwr.util.setValue("title" + i, title);
+ }
+ }
+ function suggestTrack() {
+ for (i = 0; i < fileCount; i++) {
+ var track = dwr.util.getValue("suggestedTrack" + i);
+ dwr.util.setValue("track" + i, track);
+ }
+ }
+ function resetTrack() {
+ for (i = 0; i < fileCount; i++) {
+ var track = dwr.util.getValue("originalTrack" + i);
+ dwr.util.setValue("track" + i, track);
+ }
+ }
+ function updateTags() {
+ document.getElementById("save").disabled = true;
+ index = 0;
+ dwr.util.setValue("errors", "");
+ for (i = 0; i < fileCount; i++) {
+ dwr.util.setValue("status" + i, "");
+ }
+ updateNextTag();
+ }
+ function updateNextTag() {
+ var id = dwr.util.getValue("id" + index);
+ var artist = dwr.util.getValue("artist" + index);
+ var track = dwr.util.getValue("track" + index);
+ var album = dwr.util.getValue("album" + index);
+ var title = dwr.util.getValue("title" + index);
+ var year = dwr.util.getValue("year" + index);
+ var genre = dwr.util.getValue("genre" + index);
+ dwr.util.setValue("status" + index, "<fmt:message key="edittags.working"/>");
+ tagService.setTags(id, track, artist, album, title, year, genre, setTagsCallback);
+ }
+ function setTagsCallback(result) {
+ var message;
+ if (result == "SKIPPED") {
+ message = "<fmt:message key="edittags.skipped"/>";
+ } else if (result == "UPDATED") {
+ message = "<b><fmt:message key="edittags.updated"/></b>";
+ } else {
+ message = "<div class='warning'><fmt:message key="edittags.error"/></div>"
+ var errors = dwr.util.getValue("errors");
+ errors += result + "<br/>";
+ dwr.util.setValue("errors", errors, { escapeHtml:false });
+ }
+ dwr.util.setValue("status" + index, message, { escapeHtml:false });
+ index++;
+ if (index < fileCount) {
+ updateNextTag();
+ } else {
+ document.getElementById("save").disabled = false;
+ }
+ }
+</script>
+
+<h1><fmt:message key="edittags.title"/></h1>
+<sub:url value="main.view" var="backUrl"><sub:param name="id" value="${model.id}"/></sub:url>
+<div class="back"><a href="${backUrl}"><fmt:message key="common.back"/></a></div>
+
+<table class="ruleTable indent">
+ <tr>
+ <th class="ruleTableHeader"><fmt:message key="edittags.file"/></th>
+ <th class="ruleTableHeader"><fmt:message key="edittags.track"/></th>
+ <th class="ruleTableHeader"><fmt:message key="edittags.songtitle"/></th>
+ <th class="ruleTableHeader"><fmt:message key="edittags.artist"/></th>
+ <th class="ruleTableHeader"><fmt:message key="edittags.album"/></th>
+ <th class="ruleTableHeader"><fmt:message key="edittags.year"/></th>
+ <th class="ruleTableHeader"><fmt:message key="edittags.genre"/></th>
+ <th class="ruleTableHeader" width="60pt"><fmt:message key="edittags.status"/></th>
+ </tr>
+ <tr>
+ <th class="ruleTableHeader"/>
+ <th class="ruleTableHeader"><a href="javascript:suggestTrack()"><fmt:message key="edittags.suggest.short"/></a> |
+ <a href="javascript:resetTrack()"><fmt:message key="edittags.reset.short"/></a></th>
+ <th class="ruleTableHeader"><a href="javascript:suggestTitle()"><fmt:message key="edittags.suggest"/></a> |
+ <a href="javascript:resetTitle()"><fmt:message key="edittags.reset"/></a></th>
+ <th class="ruleTableHeader" style="white-space: nowrap"><input type="text" name="artistAll" size="15" onkeypress="dwr.util.onReturn(event, setArtist)" value="${model.defaultArtist}"/>&nbsp;<a href="javascript:setArtist()"><fmt:message key="edittags.set"/></a></th>
+ <th class="ruleTableHeader" style="white-space: nowrap"><input type="text" name="albumAll" size="15" onkeypress="dwr.util.onReturn(event, setAlbum)" value="${model.defaultAlbum}"/>&nbsp;<a href="javascript:setAlbum()"><fmt:message key="edittags.set"/></a></th>
+ <th class="ruleTableHeader" style="white-space: nowrap"><input type="text" name="yearAll" size="5" onkeypress="dwr.util.onReturn(event, setYear)" value="${model.defaultYear}"/>&nbsp;<a href="javascript:setYear()"><fmt:message key="edittags.set"/></a></th>
+ <th class="ruleTableHeader" style="white-space: nowrap">
+ <select name="genreAll" style="width:7em">
+ <option value=""/>
+ <c:forEach items="${model.allGenres}" var="genre">
+ <option ${genre eq model.defaultGenre ? "selected" : ""} value="${genre}">${genre}</option>
+ </c:forEach>
+ </select>
+
+ <a href="javascript:setGenre()"><fmt:message key="edittags.set"/></a>
+ </th>
+ <th class="ruleTableHeader"/>
+ </tr>
+
+ <c:forEach items="${model.songs}" var="song" varStatus="loopStatus">
+ <tr>
+ <str:truncateNicely lower="25" upper="25" var="fileName">${song.fileName}</str:truncateNicely>
+ <input type="hidden" name="id${loopStatus.count - 1}" value="${song.id}"/>
+ <input type="hidden" name="suggestedTitle${loopStatus.count - 1}" value="${song.suggestedTitle}"/>
+ <input type="hidden" name="originalTitle${loopStatus.count - 1}" value="${song.title}"/>
+ <input type="hidden" name="suggestedTrack${loopStatus.count - 1}" value="${song.suggestedTrack}"/>
+ <input type="hidden" name="originalTrack${loopStatus.count - 1}" value="${song.track}"/>
+ <td class="ruleTableCell" title="${song.fileName}">${fileName}</td>
+ <td class="ruleTableCell"><input type="text" size="5" name="track${loopStatus.count - 1}" value="${song.track}"/></td>
+ <td class="ruleTableCell"><input type="text" size="30" name="title${loopStatus.count - 1}" value="${song.title}"/></td>
+ <td class="ruleTableCell"><input type="text" size="15" name="artist${loopStatus.count - 1}" value="${song.artist}"/></td>
+ <td class="ruleTableCell"><input type="text" size="15" name="album${loopStatus.count - 1}" value="${song.album}"/></td>
+ <td class="ruleTableCell"><input type="text" size="5" name="year${loopStatus.count - 1}" value="${song.year}"/></td>
+ <td class="ruleTableCell"><input type="text" name="genre${loopStatus.count - 1}" value="${song.genre}" style="width:7em"/></td>
+ <td class="ruleTableCell"><div id="status${loopStatus.count - 1}"/></td>
+ </tr>
+ </c:forEach>
+
+</table>
+
+<p><input type="submit" id="save" value="<fmt:message key="common.save"/>" onclick="javascript:updateTags()"/></p>
+<div class="warning" id="errors"/>
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/externalPlayer.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/externalPlayer.jsp
new file mode 100644
index 00000000..31077d85
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/externalPlayer.jsp
@@ -0,0 +1,99 @@
+<%--@elvariable id="model" type="java.util.Map"--%>
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/swfobject.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+
+ <sub:url value="/coverArt.view" var="coverArtUrl">
+ <c:if test="${not empty model.coverArt}">
+ <sub:param name="path" value="${model.coverArt.path}"/>
+ </c:if>
+ <sub:param name="size" value="200"/>
+ </sub:url>
+
+ <meta name="og:title" content="${fn:escapeXml(model.songs[0].artist)} &mdash; ${fn:escapeXml(model.songs[0].albumName)}"/>
+ <meta name="og:type" content="album"/>
+ <meta name="og:image" content="http://${model.redirectFrom}.subsonic.org${coverArtUrl}"/>
+
+ <script type="text/javascript">
+ function init() {
+ var flashvars = {
+ id:"player1",
+ screencolor:"000000",
+ frontcolor:"<spring:theme code="textColor"/>",
+ backcolor:"<spring:theme code="backgroundColor"/>",
+ stretching: "fill",
+ "playlist.position": "bottom",
+ "playlist.size": 200
+ };
+ var params = {
+ allowfullscreen:"true",
+ allowscriptaccess:"always"
+ };
+ var attributes = {
+ id:"player1",
+ name:"player1"
+ };
+ swfobject.embedSWF("<c:url value="/flash/jw-player-5.6.swf"/>", "placeholder", "500", "500", "9.0.0", false, flashvars, params, attributes);
+ }
+
+ function playerReady(thePlayer) {
+ var player = $("player1");
+ var list = new Array();
+
+ <c:forEach items="${model.songs}" var="song" varStatus="loopStatus">
+ <%--@elvariable id="song" type="net.sourceforge.subsonic.domain.MediaFile"--%>
+ <sub:url value="/stream" var="streamUrl">
+ <sub:param name="path" value="${song.path}"/>
+ <sub:param name="player" value="${model.player}"/>
+ </sub:url>
+ <sub:url value="/coverArt.view" var="coverUrl">
+ <sub:param name="size" value="500"/>
+ <c:if test="${not empty model.coverArts[loopStatus.count - 1]}">
+ <sub:param name="path" value="${model.coverArts[loopStatus.count - 1].path}"/>
+ </c:if>
+ </sub:url>
+
+ <!-- TODO: Use video provider for aac, m4a -->
+ list[${loopStatus.count - 1}] = {
+ file: "${streamUrl}",
+ image: "${coverUrl}",
+ title: "${fn:escapeXml(song.title)}",
+ provider: "${song.video ? "video" : "sound"}",
+ description: "${fn:escapeXml(song.artist)}"
+ };
+
+ <c:if test="${not empty song.durationSeconds}">
+ list[${loopStatus.count-1}].duration = ${song.durationSeconds};
+ </c:if>
+
+ </c:forEach>
+
+ player.sendEvent("LOAD", list);
+ player.sendEvent("PLAY");
+ }
+
+ </script>
+</head>
+
+<body class="mainframe bgcolor1" style="padding-top:2em" onload="init();">
+
+<div style="margin:auto;width:500px">
+ <h1 >${model.songs[0].artist}</h1>
+ <div style="float:left;padding-right:1.5em">
+ <h2 style="margin:0;">${model.songs[0].albumName}</h2>
+ </div>
+ <div class="detail" style="float:right">Streaming by <a href="http://subsonic.org/" target="_blank"><b>Subsonic</b></a></div>
+
+ <div style="clear:both;padding-top:1em">
+ <div id="placeholder">
+ <a href="http://www.adobe.com/go/getflashplayer" target="_blank"><fmt:message key="playlist.getflash"/></a>
+ </div>
+ </div>
+ <div style="padding-top: 2em">${fn:escapeXml(model.share.description)}</div>
+</div>
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/generalSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/generalSettings.jsp
new file mode 100644
index 00000000..f5cffe35
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/generalSettings.jsp
@@ -0,0 +1,165 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.GeneralSettingsCommand"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+</head>
+
+<body class="mainframe bgcolor1">
+<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
+<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="general"/>
+</c:import>
+
+<form:form method="post" action="generalSettings.view" commandName="command">
+
+ <table style="white-space:nowrap" class="indent">
+
+ <tr>
+ <td><fmt:message key="generalsettings.musicmask"/></td>
+ <td>
+ <form:input path="musicFileTypes" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="musicmask"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.videomask"/></td>
+ <td>
+ <form:input path="videoFileTypes" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="videomask"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.coverartmask"/></td>
+ <td>
+ <form:input path="coverArtFileTypes" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="coverartmask"/></c:import>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">&nbsp;</td></tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.index"/></td>
+ <td>
+ <form:input path="index" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="index"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.ignoredarticles"/></td>
+ <td>
+ <form:input path="ignoredArticles" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="ignoredarticles"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.shortcuts"/></td>
+ <td>
+ <form:input path="shortcuts" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="shortcuts"/></c:import>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">&nbsp;</td></tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.language"/></td>
+ <td>
+ <form:select path="localeIndex" cssStyle="width:15em">
+ <c:forEach items="${command.locales}" var="locale" varStatus="loopStatus">
+ <form:option value="${loopStatus.count - 1}" label="${locale}"/>
+ </c:forEach>
+ </form:select>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="language"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.theme"/></td>
+ <td>
+ <form:select path="themeIndex" cssStyle="width:15em">
+ <c:forEach items="${command.themes}" var="theme" varStatus="loopStatus">
+ <form:option value="${loopStatus.count - 1}" label="${theme.name}"/>
+ </c:forEach>
+ </form:select>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="theme"/></c:import>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">&nbsp;</td></tr>
+
+ <tr>
+ <td>
+ </td>
+ <td>
+ <form:checkbox path="sortAlbumsByYear" id="sortAlbumsByYear"/>
+ <label for="sortAlbumsByYear"><fmt:message key="generalsettings.sortalbumsbyyear"/></label>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ </td>
+ <td>
+ <form:checkbox path="gettingStartedEnabled" id="gettingStartedEnabled"/>
+ <label for="gettingStartedEnabled"><fmt:message key="generalsettings.showgettingstarted"/></label>
+ </td>
+ </tr>
+
+ <tr><td colspan="2">&nbsp;</td></tr>
+
+ <tr>
+ <td><fmt:message key="generalsettings.welcometitle"/></td>
+ <td>
+ <form:input path="welcomeTitle" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="welcomemessage"/></c:import>
+ </td>
+ </tr>
+ <tr>
+ <td><fmt:message key="generalsettings.welcomesubtitle"/></td>
+ <td>
+ <form:input path="welcomeSubtitle" size="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="welcomemessage"/></c:import>
+ </td>
+ </tr>
+ <tr>
+ <td style="vertical-align:top;"><fmt:message key="generalsettings.welcomemessage"/></td>
+ <td>
+ <form:textarea path="welcomeMessage" rows="5" cols="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="welcomemessage"/></c:import>
+ </td>
+ </tr>
+ <tr>
+ <td style="vertical-align:top;"><fmt:message key="generalsettings.loginmessage"/></td>
+ <td>
+ <form:textarea path="loginMessage" rows="5" cols="70"/>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="loginmessage"/></c:import>
+ <fmt:message key="main.wiki"/>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2" style="padding-top:1.5em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </td>
+ </tr>
+
+ </table>
+</form:form>
+
+<c:if test="${command.reloadNeeded}">
+ <script language="javascript" type="text/javascript">
+ parent.frames.left.location.href="left.view?";
+ parent.frames.playQueue.location.href="playQueue.view?";
+ </script>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/gettingStarted.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/gettingStarted.jsp
new file mode 100644
index 00000000..ad7b2a77
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/gettingStarted.jsp
@@ -0,0 +1,53 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" language="javascript">
+ function hideGettingStarted() {
+ alert("<fmt:message key="gettingStarted.hidealert"/>");
+ location.href = "gettingStarted.view?hide";
+ }
+ </script>
+</head>
+<body class="mainframe bgcolor1">
+
+<h1 style="padding-bottom:0.5em">
+ <img src="<spring:theme code="homeImage"/>" alt="">
+ <fmt:message key="gettingStarted.title"/>
+</h1>
+
+<fmt:message key="gettingStarted.text"/>
+
+<c:if test="${model.runningAsRoot}">
+ <h2 class="warning"><fmt:message key="gettingStarted.root"/></h2>
+</c:if>
+
+<table style="padding-top:1em;padding-bottom:2em;width:60%">
+ <tr>
+ <td style="font-size:26pt;padding:20pt">1</td>
+ <td>
+ <div style="font-size:14pt"><a href="userSettings.view?userIndex=0"><fmt:message key="gettingStarted.step1.title"/></a></div>
+ <div style="padding-top:5pt"><fmt:message key="gettingStarted.step1.text"/></div>
+ </td>
+ </tr>
+ <tr>
+ <td style="font-size:26pt;padding:20pt">2</td>
+ <td>
+ <div style="font-size:14pt"><a href="musicFolderSettings.view"><fmt:message key="gettingStarted.step2.title"/></a></div>
+ <div style="padding-top:5pt"><fmt:message key="gettingStarted.step2.text"/></div>
+ </td>
+ </tr>
+ <tr>
+ <td style="font-size:26pt;padding:20pt">3</td>
+ <td>
+ <div style="font-size:14pt"><a href="networkSettings.view"><fmt:message key="gettingStarted.step3.title"/></a></div>
+ <div style="padding-top:5pt"><fmt:message key="gettingStarted.step3.text"/></div>
+ </td>
+ </tr>
+
+</table>
+
+<div class="forward"><a href="javascript:hideGettingStarted()"><fmt:message key="gettingStarted.hide"/></a></div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/head.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/head.jsp
new file mode 100644
index 00000000..cedadd8d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/head.jsp
@@ -0,0 +1,11 @@
+<%@ include file="include.jsp" %>
+
+<!--[if lt IE 7.]>
+<script defer type="text/javascript" src="<c:url value="/script/pngfix.js"/>"></script>
+<![endif]-->
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<c:set var="styleSheet"><spring:theme code="styleSheet"/></c:set>
+<c:set var="faviconImage"><spring:theme code="faviconImage"/></c:set>
+<link rel="stylesheet" href="<c:url value="/${styleSheet}"/>" type="text/css">
+<link rel="shortcut icon" href="<c:url value="/${faviconImage}"/>" type="text/css">
+<title>Subsonic</title>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp
new file mode 100644
index 00000000..58421828
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/help.jsp
@@ -0,0 +1,70 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:choose>
+ <c:when test="${empty model.buildDate}">
+ <fmt:message key="common.unknown" var="buildDateString"/>
+ </c:when>
+ <c:otherwise>
+ <fmt:formatDate value="${model.buildDate}" dateStyle="long" var="buildDateString"/>
+ </c:otherwise>
+</c:choose>
+
+<c:choose>
+ <c:when test="${empty model.localVersion}">
+ <fmt:message key="common.unknown" var="versionString"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="versionString" value="${model.localVersion} (build ${model.buildNumber})"/>
+ </c:otherwise>
+</c:choose>
+
+<h1>
+ <img src="<spring:theme code="helpImage"/>" alt="">
+ <fmt:message key="help.title"><fmt:param value="${model.brand}"/></fmt:message>
+</h1>
+
+<c:if test="${model.newVersionAvailable}">
+ <p class="warning"><fmt:message key="help.upgrade"><fmt:param value="${model.brand}"/><fmt:param value="${model.latestVersion}"/></fmt:message></p>
+</c:if>
+
+<table width="75%" class="ruleTable indent">
+ <tr><td class="ruleTableHeader"><fmt:message key="help.version.title"/></td><td class="ruleTableCell">${versionString} &ndash; ${buildDateString}</td></tr>
+ <tr><td class="ruleTableHeader"><fmt:message key="help.server.title"/></td><td class="ruleTableCell">${model.serverInfo} (<sub:formatBytes bytes="${model.usedMemory}"/> / <sub:formatBytes bytes="${model.totalMemory}"/>)</td></tr>
+ <tr><td class="ruleTableHeader"><fmt:message key="help.license.title"/></td><td class="ruleTableCell">
+ <a href="http://www.gnu.org/copyleft/gpl.html" target="_blank"><img style="float:right;margin-left: 10px" alt="GPL 3.0" src="<c:url value="/icons/gpl.png"/>"></a>
+ <fmt:message key="help.license.text"><fmt:param value="${model.brand}"/></fmt:message></td></tr>
+ <tr><td class="ruleTableHeader"><fmt:message key="help.homepage.title"/></td><td class="ruleTableCell"><a target="_blank" href="http://www.subsonic.org/">subsonic.org</a></td></tr>
+ <tr><td class="ruleTableHeader"><fmt:message key="help.forum.title"/></td><td class="ruleTableCell"><a target="_blank" href="http://forum.subsonic.org/">forum.subsonic.org</a></td></tr>
+ <tr><td class="ruleTableHeader"><fmt:message key="help.contact.title"/></td><td class="ruleTableCell"><fmt:message key="help.contact.text"><fmt:param value="${model.brand}"/></fmt:message></td></tr>
+</table>
+
+<p></p>
+
+<table width="75%"><tr>
+ <td><a href="<c:url value="/donate.view"/>"><img src="<spring:theme code="paypalImage"/>" alt=""></a></td>
+ <td><fmt:message key="help.donate"><fmt:param value="${model.brand}"/></fmt:message></td>
+</tr></table>
+
+<h2><img src="<spring:theme code="logImage"/>" alt="">&nbsp;<fmt:message key="help.log"/></h2>
+
+<table cellpadding="2" class="log indent">
+ <c:forEach items="${model.logEntries}" var="entry">
+ <tr>
+ <td>[<fmt:formatDate value="${entry.date}" dateStyle="short" timeStyle="long" type="both"/>]</td>
+ <td>${entry.level}</td><td>${entry.category}</td><td>${entry.message}</td>
+ </tr>
+ </c:forEach>
+</table>
+
+<p><fmt:message key="help.logfile"><fmt:param value="${model.logFile}"/></fmt:message> </p>
+
+<div class="forward"><a href="help.view?"><fmt:message key="common.refresh"/></a></div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/helpToolTip.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/helpToolTip.jsp
new file mode 100644
index 00000000..04de8ad0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/helpToolTip.jsp
@@ -0,0 +1,18 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<%@ include file="include.jsp" %>
+
+<%--
+ Shows online help as a balloon tool tip.
+
+PARAMETERS
+ topic: Refers to a key in the resource bundle containing the text to display in the tool tip.
+--%>
+
+<spring:theme code="helpPopupImage" var="imageUrl"/>
+<fmt:message key="common.help" var="help"/>
+
+<div id="placeholder-${param.topic}" style="display:none">
+ <div style="font-weight:bold;"><fmt:message key="helppopup.${param.topic}.title"><fmt:param value="Subsonic"/></fmt:message></div>
+ <div><fmt:message key="helppopup.${param.topic}.text"><fmt:param value="Subsonic"/></fmt:message></div>
+</div>
+<img src="${imageUrl}" alt="${help}" title="${help}" onmouseover="TagToTip('placeholder-${param.topic}', BALLOON, true, ABOVE, true, OFFSETX, -17, PADDING, 8, WIDTH, -240, CLICKSTICKY, true, CLICKCLOSE, true)" onmouseout="UnTip()"/>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/home.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/home.jsp
new file mode 100644
index 00000000..8792d60d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/home.jsp
@@ -0,0 +1,189 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <%@ include file="jquery.jsp" %>
+ <link href="<c:url value="/style/shadow.css"/>" rel="stylesheet">
+
+ <script type="text/javascript" language="javascript">
+ function init() {
+ <c:if test="${model.listType eq 'random'}">
+ setTimeout("refresh()", 20000);
+ </c:if>
+ }
+
+ function refresh() {
+ top.main.location.href = top.main.location.href;
+ }
+ </script>
+</head>
+<body class="mainframe bgcolor1" onload="init();">
+<h1>
+ <img src="<spring:theme code="homeImage"/>" alt="">
+ ${model.welcomeTitle}
+</h1>
+
+<c:if test="${not empty model.welcomeSubtitle}">
+ <h2>${model.welcomeSubtitle}</h2>
+</c:if>
+
+<h2>
+ <c:forTokens items="random newest starred highest frequent recent alphabetical users" delims=" " var="cat" varStatus="loopStatus">
+ <c:if test="${loopStatus.count > 1}">&nbsp;|&nbsp;</c:if>
+ <sub:url var="url" value="home.view">
+ <sub:param name="listSize" value="${model.listSize}"/>
+ <sub:param name="listType" value="${cat}"/>
+ </sub:url>
+
+ <c:choose>
+ <c:when test="${model.listType eq cat}">
+ <span class="headerSelected"><fmt:message key="home.${cat}.title"/></span>
+ </c:when>
+ <c:otherwise>
+ <a href="${url}"><fmt:message key="home.${cat}.title"/></a>
+ </c:otherwise>
+ </c:choose>
+
+ </c:forTokens>
+</h2>
+
+<c:if test="${model.isIndexBeingCreated}">
+ <p class="warning"><fmt:message key="home.scan"/></p>
+</c:if>
+
+<h2><fmt:message key="home.${model.listType}.text"/></h2>
+
+<table width="100%">
+ <tr>
+ <td style="vertical-align:top;">
+<c:choose>
+<c:when test="${model.listType eq 'users'}">
+ <table>
+ <tr>
+ <th><fmt:message key="home.chart.total"/></th>
+ <th><fmt:message key="home.chart.stream"/></th>
+ </tr>
+ <tr>
+ <td><img src="<c:url value="/userChart.view"><c:param name="type" value="total"/></c:url>" alt=""></td>
+ <td><img src="<c:url value="/userChart.view"><c:param name="type" value="stream"/></c:url>" alt=""></td>
+ </tr>
+ <tr>
+ <th><fmt:message key="home.chart.download"/></th>
+ <th><fmt:message key="home.chart.upload"/></th>
+ </tr>
+ <tr>
+ <td><img src="<c:url value="/userChart.view"><c:param name="type" value="download"/></c:url>" alt=""></td>
+ <td><img src="<c:url value="/userChart.view"><c:param name="type" value="upload"/></c:url>" alt=""></td>
+ </tr>
+</table>
+
+</c:when>
+<c:otherwise>
+
+ <table>
+ <c:forEach items="${model.albums}" var="album" varStatus="loopStatus">
+ <c:if test="${loopStatus.count % 5 == 1}">
+ <tr>
+ </c:if>
+
+ <td style="vertical-align:top">
+ <table>
+ <tr><td>
+ <c:import url="coverArt.jsp">
+ <c:param name="albumId" value="${album.id}"/>
+ <c:param name="albumName" value="${album.albumTitle}"/>
+ <c:param name="coverArtSize" value="110"/>
+ <c:param name="showLink" value="true"/>
+ <c:param name="showZoom" value="false"/>
+ <c:param name="showChange" value="false"/>
+ <c:param name="appearAfter" value="${loopStatus.count * 30}"/>
+ </c:import>
+
+ <div class="detail">
+ <c:if test="${not empty album.playCount}">
+ <fmt:message key="home.playcount"><fmt:param value="${album.playCount}"/></fmt:message>
+ </c:if>
+ <c:if test="${not empty album.lastPlayed}">
+ <fmt:formatDate value="${album.lastPlayed}" dateStyle="short" var="lastPlayedDate"/>
+ <fmt:message key="home.lastplayed"><fmt:param value="${lastPlayedDate}"/></fmt:message>
+ </c:if>
+ <c:if test="${not empty album.created}">
+ <fmt:formatDate value="${album.created}" dateStyle="short" var="creationDate"/>
+ <fmt:message key="home.created"><fmt:param value="${creationDate}"/></fmt:message>
+ </c:if>
+ <c:if test="${not empty album.rating}">
+ <c:import url="rating.jsp">
+ <c:param name="readonly" value="true"/>
+ <c:param name="rating" value="${album.rating}"/>
+ </c:import>
+ </c:if>
+ </div>
+
+ <c:choose>
+ <c:when test="${empty album.artist and empty album.albumTitle}">
+ <div class="detail"><fmt:message key="common.unknown"/></div>
+ </c:when>
+ <c:otherwise>
+ <div class="detail"><em><str:truncateNicely lower="15" upper="15">${album.artist}</str:truncateNicely></em></div>
+ <div class="detail"><str:truncateNicely lower="15" upper="15">${album.albumTitle}</str:truncateNicely></div>
+ </c:otherwise>
+ </c:choose>
+
+ </td></tr>
+ </table>
+ </td>
+ <c:if test="${loopStatus.count % 5 == 0}">
+ </tr>
+ </c:if>
+ </c:forEach>
+ </table>
+
+<table>
+ <tr>
+ <td style="padding-right:1.5em">
+ <select name="listSize" onchange="location='home.view?listType=${model.listType}&amp;listOffset=${model.listOffset}&amp;listSize=' + options[selectedIndex].value;">
+ <c:forTokens items="5 10 15 20 30 40 50" delims=" " var="size">
+ <option ${size eq model.listSize ? "selected" : ""} value="${size}"><fmt:message key="home.listsize"><fmt:param value="${size}"/></fmt:message></option>
+ </c:forTokens>
+ </select>
+ </td>
+
+ <c:choose>
+ <c:when test="${model.listType eq 'random'}">
+ <td><div class="forward"><a href="home.view?listType=random&amp;listSize=${model.listSize}"><fmt:message key="common.more"/></a></div></td>
+ </c:when>
+
+ <c:otherwise>
+ <sub:url value="home.view" var="previousUrl">
+ <sub:param name="listType" value="${model.listType}"/>
+ <sub:param name="listOffset" value="${model.listOffset - model.listSize}"/>
+ <sub:param name="listSize" value="${model.listSize}"/>
+ </sub:url>
+ <sub:url value="home.view" var="nextUrl">
+ <sub:param name="listType" value="${model.listType}"/>
+ <sub:param name="listOffset" value="${model.listOffset + model.listSize}"/>
+ <sub:param name="listSize" value="${model.listSize}"/>
+ </sub:url>
+
+ <td style="padding-right:1.5em"><fmt:message key="home.albums"><fmt:param value="${model.listOffset + 1}"/><fmt:param value="${model.listOffset + model.listSize}"/></fmt:message></td>
+ <td style="padding-right:1.5em"><div class="back"><a href="${previousUrl}"><fmt:message key="common.previous"/></a></div></td>
+ <td><div class="forward"><a href="${nextUrl}"><fmt:message key="common.next"/></a></div></td>
+ </c:otherwise>
+ </c:choose>
+ </tr>
+ </table>
+</c:otherwise>
+</c:choose>
+ </td>
+ <c:if test="${not empty model.welcomeMessage}">
+ <td style="vertical-align:top;width:20em">
+ <div style="padding:0 1em 0 1em;border-left:1px solid #<spring:theme code="detailColor"/>">
+ <sub:wiki text="${model.welcomeMessage}"/>
+ </div>
+ </td>
+ </c:if>
+ </tr>
+ </table>
+
+</body></html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/importPlaylist.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/importPlaylist.jsp
new file mode 100644
index 00000000..4bc09b88
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/importPlaylist.jsp
@@ -0,0 +1,37 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<h1 style="padding-bottom:0.5em">
+ <fmt:message key="importPlaylist.title"/>
+</h1>
+
+<c:if test="${not empty model.playlist}">
+ <p>
+ <fmt:message key="importPlaylist.success"><fmt:param value="${model.playlist.name}"/></fmt:message>
+ <script type="text/javascript" language="javascript">
+ top.left.updatePlaylists();
+ </script>
+ </p>
+</c:if>
+
+<c:if test="${not empty model.error}">
+ <p class="warning">
+ <fmt:message key="importPlaylist.error"><fmt:param value="${model.error}"/></fmt:message>
+ </p>
+</c:if>
+
+<div style="padding-bottom: 0.25em">
+ <fmt:message key="importPlaylist.text"/>
+</div>
+<form method="post" enctype="multipart/form-data" action="importPlaylist.view">
+ <input type="file" id="file" name="file" size="40"/>
+ <input type="submit" value="<fmt:message key="common.ok"/>"/>
+</form>
+
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/include.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/include.jsp
new file mode 100644
index 00000000..41c0aa2d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/include.jsp
@@ -0,0 +1,8 @@
+<%@ page session="false"%>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+<%@ taglib prefix="sub" uri="http://subsonic.org/taglib/sub" %>
+<%@ taglib prefix="str" uri="http://jakarta.apache.org/taglibs/string-1.1" %>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/index.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/index.jsp
new file mode 100644
index 00000000..4ec7e2b0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/index.jsp
@@ -0,0 +1,26 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <link rel="alternate" type="application/rss+xml" title="Subsonic Podcast" href="podcast.view?suffix=.rss">
+</head>
+
+<frameset rows="70,*,0" border="0" framespacing="0" frameborder="0">
+ <frame name="upper" src="top.view?">
+ <frameset cols="15%,85%" border="0" framespacing="0" frameborder="0">
+ <frame name="left" src="left.view?" marginwidth="0" marginheight="0">
+
+ <frameset rows="70%,30%" border="0" framespacing="0" frameborder="0">
+ <frameset cols="*,${model.showRight ? 230 : 0}" border="0" framespacing="0" frameborder="0">
+ <frame name="main" src="nowPlaying.view?" marginwidth="0" marginheight="0">
+ <frame name="right" src="right.view?">
+ </frameset>
+ <frame name="playQueue" src="playQueue.view?">
+ </frameset>
+ </frameset>
+ <frame name="hidden" frameborder="0" noresize="noresize">
+
+</frameset>
+
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/internetRadioSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/internetRadioSettings.jsp
new file mode 100644
index 00000000..ba3fbe1b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/internetRadioSettings.jsp
@@ -0,0 +1,62 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="internetRadio"/>
+</c:import>
+
+<form method="post" action="internetRadioSettings.view">
+<table class="indent">
+ <tr>
+ <th><fmt:message key="internetradiosettings.name"/></th>
+ <th><fmt:message key="internetradiosettings.streamurl"/></th>
+ <th><fmt:message key="internetradiosettings.homepageurl"/></th>
+ <th style="padding-left:1em"><fmt:message key="internetradiosettings.enabled"/></th>
+ <th style="padding-left:1em"><fmt:message key="common.delete"/></th>
+ </tr>
+
+ <c:forEach items="${model.internetRadios}" var="radio">
+ <tr>
+ <td><input type="text" name="name[${radio.id}]" size="20" value="${radio.name}"/></td>
+ <td><input type="text" name="streamUrl[${radio.id}]" size="40" value="${radio.streamUrl}"/></td>
+ <td><input type="text" name="homepageUrl[${radio.id}]" size="40" value="${radio.homepageUrl}"/></td>
+ <td align="center" style="padding-left:1em"><input type="checkbox" ${radio.enabled ? "checked" : ""} name="enabled[${radio.id}]" class="checkbox"/></td>
+ <td align="center" style="padding-left:1em"><input type="checkbox" name="delete[${radio.id}]" class="checkbox"/></td>
+ </tr>
+ </c:forEach>
+
+ <tr>
+ <th colspan="5" align="left" style="padding-top:1em"><fmt:message key="internetradiosettings.add"/></th>
+ </tr>
+
+ <tr>
+ <td><input type="text" name="name" size="20"/></td>
+ <td><input type="text" name="streamUrl" size="40"/></td>
+ <td><input type="text" name="homepageUrl" size="40"/></td>
+ <td align="center" style="padding-left:1em"><input name="enabled" checked type="checkbox" class="checkbox"/></td>
+ <td/>
+ </tr>
+
+ <tr>
+ <td style="padding-top:1.5em" colspan="5">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </td>
+ </tr>
+</table>
+</form>
+
+
+<c:if test="${not empty model.error}">
+ <p class="warning"><fmt:message key="${model.error}"/></p>
+</c:if>
+
+<c:if test="${model.reload}">
+ <script language="javascript" type="text/javascript">parent.frames.left.location.href="left.view?"</script>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/jquery.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/jquery.jsp
new file mode 100644
index 00000000..094b9bcc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/jquery.jsp
@@ -0,0 +1,3 @@
+<link rel="stylesheet" href="<c:url value="/style/smoothness/jquery-ui-1.8.18.custom.css"/>" type="text/css">
+<script type="text/javascript" src="<c:url value='/script/jquery-1.7.1.min.js'/>"></script>
+<script type="text/javascript" src="<c:url value='/script/jquery-ui-1.8.18.custom.min.js'/>"></script>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp
new file mode 100644
index 00000000..d47c25f6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp
@@ -0,0 +1,168 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head>
+ <%@ include file="head.jsp" %>
+ <%@ include file="jquery.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/smooth-scroll.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/playlistService.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+
+ var playlists;
+
+ function init() {
+ dwr.engine.setErrorHandler(null);
+ updatePlaylists();
+ }
+
+ function updatePlaylists() {
+ playlistService.getReadablePlaylists(playlistCallback);
+ }
+
+ function createEmptyPlaylist() {
+ playlistService.createEmptyPlaylist(playlistCallback);
+ }
+
+ function playlistCallback(playlists) {
+ this.playlists = playlists;
+
+ $("#playlists").empty();
+ for (var i = 0; i < playlists.length; i++) {
+ var playlist = playlists[i];
+ $("<p class='dense'><a target='main' href='playlist.view?id=" +
+ playlist.id + "'>" + playlist.name + "&nbsp;(" + playlist.fileCount + ")</a></p>").appendTo("#playlists");
+ }
+ }
+ </script>
+</head>
+
+<body class="bgcolor2 leftframe" onload="init()">
+<a name="top"></a>
+
+<c:if test="${model.scanning}">
+ <div style="padding-bottom:1.0em">
+ <div class="warning"><fmt:message key="left.scanning"/></div>
+ <div class="forward"><a href="left.view"><fmt:message key="common.refresh"/></a></div>
+ </div>
+</c:if>
+
+<div style="padding-bottom:0.5em">
+ <c:forEach items="${model.indexes}" var="index">
+ <a href="#${index.index}" accesskey="${index.index}">${index.index}</a>
+ </c:forEach>
+</div>
+
+<c:if test="${model.statistics.songCount gt 0}">
+ <div class="detail">
+ <fmt:message key="left.statistics">
+ <fmt:param value="${model.statistics.artistCount}"/>
+ <fmt:param value="${model.statistics.albumCount}"/>
+ <fmt:param value="${model.statistics.songCount}"/>
+ <fmt:param value="${model.bytes}"/>
+ <fmt:param value="${model.hours}"/>
+ </fmt:message>
+ </div>
+</c:if>
+
+<c:if test="${fn:length(model.musicFolders) > 1}">
+ <div style="padding-top:1em">
+ <select name="musicFolderId" style="width:100%" onchange="location='left.view?musicFolderId=' + options[selectedIndex].value;" >
+ <option value="-1"><fmt:message key="left.allfolders"/></option>
+ <c:forEach items="${model.musicFolders}" var="musicFolder">
+ <option ${model.selectedMusicFolder.id == musicFolder.id ? "selected" : ""} value="${musicFolder.id}">${musicFolder.name}</option>
+ </c:forEach>
+ </select>
+ </div>
+</c:if>
+
+<c:if test="${not empty model.shortcuts}">
+ <h2 class="bgcolor1"><fmt:message key="left.shortcut"/></h2>
+ <c:forEach items="${model.shortcuts}" var="shortcut">
+ <p class="dense" style="padding-left:0.5em">
+ <sub:url value="main.view" var="mainUrl">
+ <sub:param name="id" value="${shortcut.id}"/>
+ </sub:url>
+ <a target="main" href="${mainUrl}">${shortcut.name}</a>
+ </p>
+ </c:forEach>
+</c:if>
+
+<h2 class="bgcolor1"><fmt:message key="left.playlists"/></h2>
+<div style='padding-left:0.5em'>
+ <div id="playlists"></div>
+ <div style="margin-top: 0.3em"><a href="javascript:noop()" onclick="createEmptyPlaylist()"><fmt:message key="left.createplaylist"/></a></div>
+ <div><a href="importPlaylist.view" target="main"><fmt:message key="left.importplaylist"/></a></div>
+</div>
+
+<c:if test="${not empty model.radios}">
+ <h2 class="bgcolor1"><fmt:message key="left.radio"/></h2>
+ <c:forEach items="${model.radios}" var="radio">
+ <p class="dense">
+ <a target="hidden" href="${radio.streamUrl}">
+ <img src="<spring:theme code="playImage"/>" alt="<fmt:message key="common.play"/>" title="<fmt:message key="common.play"/>"></a>
+ <c:choose>
+ <c:when test="${empty radio.homepageUrl}">
+ ${radio.name}
+ </c:when>
+ <c:otherwise>
+ <a target="main" href="${radio.homepageUrl}">${radio.name}</a>
+ </c:otherwise>
+ </c:choose>
+ </p>
+ </c:forEach>
+</c:if>
+
+<c:forEach items="${model.indexedArtists}" var="entry">
+ <table class="bgcolor1" style="width:100%;padding:0;margin:1em 0 0 0;border:0">
+ <tr style="padding:0;margin:0;border:0">
+ <th style="text-align:left;padding:0;margin:0;border:0"><a name="${entry.key.index}"></a>
+ <h2 style="padding:0;margin:0;border:0">${entry.key.index}</h2>
+ </th>
+ <th style="text-align:right;">
+ <a href="#top"><img src="<spring:theme code="upImage"/>" alt=""></a>
+ </th>
+ </tr>
+ </table>
+
+ <c:forEach items="${entry.value}" var="artist">
+ <p class="dense" style="padding-left:0.5em">
+ <span title="${artist.name}">
+ <sub:url value="main.view" var="mainUrl">
+ <c:forEach items="${artist.mediaFiles}" var="mediaFile">
+ <sub:param name="id" value="${mediaFile.id}"/>
+ </c:forEach>
+ </sub:url>
+ <a target="main" href="${mainUrl}"><str:truncateNicely upper="${model.captionCutoff}">${artist.name}</str:truncateNicely></a>
+ </span>
+ </p>
+ </c:forEach>
+</c:forEach>
+
+<div style="padding-top:1em"></div>
+
+<c:forEach items="${model.singleSongs}" var="song">
+ <p class="dense" style="padding-left:0.5em">
+ <span title="${song.title}">
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${song.id}"/>
+ <c:param name="playEnabled" value="${model.user.streamRole and not model.partyMode}"/>
+ <c:param name="addEnabled" value="${model.user.streamRole}"/>
+ <c:param name="downloadEnabled" value="${model.user.downloadRole and not model.partyMode}"/>
+ <c:param name="video" value="${song.video and model.player.web}"/>
+ </c:import>
+ <str:truncateNicely upper="${model.captionCutoff}">${song.title}</str:truncateNicely>
+ </span>
+ </p>
+</c:forEach>
+
+<div style="height:5em"></div>
+
+<div class="bgcolor2" style="opacity: 1.0; clear: both; position: fixed; bottom: 0; right: 0; left: 0;
+ padding: 0.25em 0.75em 0.25em 0.75em; border-top:1px solid black; max-width: 850px;">
+ <c:forEach items="${model.indexes}" var="index">
+ <a href="#${index.index}">${index.index}</a>
+ </c:forEach>
+</div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/login.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/login.jsp
new file mode 100644
index 00000000..0173ca8c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/login.jsp
@@ -0,0 +1,64 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript">
+ if (window != window.top) {
+ top.location.href = location.href;
+ }
+ </script>
+
+</head>
+<body class="mainframe bgcolor1" onload="document.getElementById('j_username').focus()">
+
+<form action="<c:url value="/j_acegi_security_check"/>" method="POST">
+ <div class="bgcolor2" align="center" style="border:1px solid black; padding:20px 50px 20px 50px; margin-top:100px">
+
+ <div style="margin-bottom:1em;max-width:50em;text-align:left;"><sub:wiki text="${model.loginMessage}"/></div>
+
+ <table>
+ <tr>
+ <td colspan="2" align="left" style="padding-bottom:10px">
+ <img src="<spring:theme code="logoImage"/>" alt="">
+ </td>
+ </tr>
+ <tr>
+ <td align="left" style="padding-right:10px"><fmt:message key="login.username"/></td>
+ <td align="left"><input type="text" id="j_username" name="j_username" style="width:12em" tabindex="1"></td>
+ </tr>
+
+ <tr>
+ <td align="left" style="padding-bottom:10px"><fmt:message key="login.password"/></td>
+ <td align="left" style="padding-bottom:10px"><input type="password" name="j_password" style="width:12em" tabindex="2"></td>
+ </tr>
+
+ <tr>
+ <td align="left"><input name="submit" type="submit" value="<fmt:message key="login.login"/>" tabindex="4"></td>
+ <td align="left" class="detail">
+ <input type="checkbox" name="_acegi_security_remember_me" id="remember" class="checkbox" tabindex="3">
+ <label for="remember"><fmt:message key="login.remember"/></label>
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td align="left" class="detail"><a href="recover.view"><fmt:message key="login.recover"/></a></td>
+ </tr>
+
+ <c:if test="${model.logout}">
+ <tr><td colspan="2" style="padding-top:10px"><b><fmt:message key="login.logout"/></b></td></tr>
+ </c:if>
+ <c:if test="${model.error}">
+ <tr><td colspan="2" style="padding-top:10px"><b class="warning"><fmt:message key="login.error"/></b></td></tr>
+ </c:if>
+
+ </table>
+
+ <c:if test="${model.insecure}">
+ <p><b class="warning"><fmt:message key="login.insecure"><fmt:param value="${model.brand}"/></fmt:message></b></p>
+ </c:if>
+
+ </div>
+</form>
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/lyrics.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/lyrics.jsp
new file mode 100644
index 00000000..82d5ce37
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/lyrics.jsp
@@ -0,0 +1,79 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <title><fmt:message key="lyrics.title"/></title>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/lyricsService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+
+ <script type="text/javascript" language="javascript">
+
+ dwr.engine.setErrorHandler(null);
+
+ function init() {
+ getLyrics('${model.artist}', '${model.song}');
+ }
+
+ function getLyrics(artist, song) {
+ $("wait").style.display = "inline";
+ $("lyrics").style.display = "none";
+ $("noLyricsFound").style.display = "none";
+ lyricsService.getLyrics(artist, song, getLyricsCallback);
+ }
+
+ function getLyricsCallback(lyricsInfo) {
+ dwr.util.setValue("lyricsHeader", lyricsInfo.artist + " - " + lyricsInfo.title);
+ var lyrics;
+ if (lyricsInfo.lyrics != null) {
+ lyrics = lyricsInfo.lyrics.replace(/\n/g, "<br>");
+ }
+ dwr.util.setValue("lyricsText", lyrics, { escapeHtml:false });
+ $("wait").style.display = "none";
+ if (lyrics != null) {
+ $("lyrics").style.display = "inline";
+ } else {
+ $("noLyricsFound").style.display = "inline";
+ }
+ }
+ </script>
+
+</head>
+<body class="mainframe bgcolor1" onload="init();">
+
+<table>
+ <tr>
+ <td><fmt:message key="lyrics.artist"/></td>
+ <td style="padding-left:0.50em"><input id="artist" type="text" size="40" value="${model.artist}" tabindex="1"/></td>
+ <td style="padding-left:0.75em"><input type="submit" value="<fmt:message key="lyrics.search"/>" style="width:6em"
+ onclick="getLyrics(dwr.util.getValue('artist'), dwr.util.getValue('song'))" tabindex="3"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="lyrics.song"/></td>
+ <td style="padding-left:0.50em"><input id="song" type="text" size="40" value="${model.song}" tabindex="2"/></td>
+ <td style="padding-left:0.75em"><input type="submit" value="<fmt:message key="common.close"/>" style="width:6em"
+ onclick="self.close()" tabindex="4"/></td>
+ </tr>
+</table>
+
+<hr/>
+<h2 id="wait"><fmt:message key="lyrics.wait"/></h2>
+<h2 id="noLyricsFound" style="display:none"><fmt:message key="lyrics.nolyricsfound"/></h2>
+
+<div id="lyrics" style="display:none;">
+ <h2 id="lyricsHeader" style="text-align:center;margin-bottom:1em"></h2>
+
+ <div id="lyricsText"></div>
+
+ <p class="detail" style="text-align:right">
+ <fmt:message key="lyrics.courtesy"/>
+ </p>
+</div>
+
+<hr/>
+<p style="text-align:center">
+ <a href="javascript:self.close()">[<fmt:message key="common.close"/>]</a>
+</p>
+
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/main.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/main.jsp
new file mode 100644
index 00000000..fbbd553d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/main.jsp
@@ -0,0 +1,479 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<%--@elvariable id="model" type="java.util.Map"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <%@ include file="jquery.jsp" %>
+ <link href="<c:url value="/style/shadow.css"/>" rel="stylesheet">
+ <c:if test="${not model.updateNowPlaying}">
+ <meta http-equiv="refresh" content="180;URL=nowPlaying.view?">
+ </c:if>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/starService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/playlistService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/fancyzoom/FancyZoom.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/fancyzoom/FancyZoomHTML.js"/>"></script>
+</head><body class="mainframe bgcolor1" onload="init();">
+
+<sub:url value="createShare.view" var="shareUrl">
+ <sub:param name="dir" value="${model.dir.path}"/>
+</sub:url>
+<sub:url value="download.view" var="downloadUrl">
+ <sub:param name="dir" value="${model.dir.path}"/>
+</sub:url>
+<sub:url value="appendPlaylist.view" var="appendPlaylistUrl">
+ <sub:param name="dir" value="${model.dir.path}"/>
+</sub:url>
+
+<script type="text/javascript" language="javascript">
+ function init() {
+ setupZoom('<c:url value="/"/>');
+
+ $("#dialog-select-playlist").dialog({resizable: true, height: 220, position: 'top', modal: true, autoOpen: false,
+ buttons: {
+ "<fmt:message key="common.cancel"/>": function() {
+ $(this).dialog("close");
+ }
+ }});
+ }
+
+ <!-- actionSelected() is invoked when the users selects from the "More actions..." combo box. -->
+ function actionSelected(id) {
+
+ if (id == "top") {
+ return;
+ } else if (id == "selectAll") {
+ selectAll(true);
+ } else if (id == "selectNone") {
+ selectAll(false);
+ } else if (id == "share") {
+ parent.frames.main.location.href = "${shareUrl}&" + getSelectedIndexes();
+ } else if (id == "download") {
+ location.href = "${downloadUrl}&" + getSelectedIndexes();
+ } else if (id == "appendPlaylist") {
+ onAppendPlaylist();
+ }
+ $("#moreActions").prop("selectedIndex", 0);
+ }
+
+ function getSelectedIndexes() {
+ var result = "";
+ for (var i = 0; i < ${fn:length(model.children)}; i++) {
+ var checkbox = $("#songIndex" + i);
+ if (checkbox != null && checkbox.is(":checked")) {
+ result += "i=" + i + "&";
+ }
+ }
+ return result;
+ }
+
+ function selectAll(b) {
+ for (var i = 0; i < ${fn:length(model.children)}; i++) {
+ var checkbox = $("#songIndex" + i);
+ if (checkbox != null) {
+ if (b) {
+ checkbox.attr("checked", "checked");
+ } else {
+ checkbox.removeAttr("checked");
+ }
+ }
+ }
+ }
+
+ function toggleStar(mediaFileId, imageId) {
+ if ($(imageId).attr("src").indexOf("<spring:theme code="ratingOnImage"/>") != -1) {
+ $(imageId).attr("src", "<spring:theme code="ratingOffImage"/>");
+ starService.unstar(mediaFileId);
+ }
+ else if ($(imageId).attr("src").indexOf("<spring:theme code="ratingOffImage"/>") != -1) {
+ $(imageId).attr("src", "<spring:theme code="ratingOnImage"/>");
+ starService.star(mediaFileId);
+ }
+ }
+
+ function onAppendPlaylist() {
+ playlistService.getWritablePlaylists(playlistCallback);
+ }
+ function playlistCallback(playlists) {
+ $("#dialog-select-playlist-list").empty();
+ for (var i = 0; i < playlists.length; i++) {
+ var playlist = playlists[i];
+ $("<p class='dense'><b><a href='#' onclick='appendPlaylist(" + playlist.id + ")'>" + playlist.name + "</a></b></p>").appendTo("#dialog-select-playlist-list");
+ }
+ $("#dialog-select-playlist").dialog("open");
+ }
+ function appendPlaylist(playlistId) {
+ $("#dialog-select-playlist").dialog("close");
+
+ var mediaFileIds = new Array();
+ for (var i = 0; i < ${fn:length(model.children)}; i++) {
+ var checkbox = $("#songIndex" + i);
+ if (checkbox && checkbox.is(":checked")) {
+ mediaFileIds.push($("#songId" + i).html());
+ }
+ }
+ playlistService.appendToPlaylist(playlistId, mediaFileIds, function (){top.left.updatePlaylists();});
+ }
+
+</script>
+
+<c:if test="${model.updateNowPlaying}">
+
+ <script type="text/javascript" language="javascript">
+ // Variable used by javascript in playlist.jsp
+ var updateNowPlaying = true;
+ </script>
+</c:if>
+
+<h1>
+ <a href="#" onclick="toggleStar(${model.dir.id}, '#starImage'); return false;">
+ <c:choose>
+ <c:when test="${not empty model.dir.starredDate}">
+ <img id="starImage" src="<spring:theme code="ratingOnImage"/>" alt="">
+ </c:when>
+ <c:otherwise>
+ <img id="starImage" src="<spring:theme code="ratingOffImage"/>" alt="">
+ </c:otherwise>
+ </c:choose>
+ </a>
+
+ <c:forEach items="${model.ancestors}" var="ancestor">
+ <sub:url value="main.view" var="ancestorUrl">
+ <sub:param name="id" value="${ancestor.id}"/>
+ </sub:url>
+ <a href="${ancestorUrl}">${ancestor.name}</a> &raquo;
+ </c:forEach>
+ ${model.dir.name}
+
+ <c:if test="${model.dir.album and model.averageRating gt 0}">
+ &nbsp;&nbsp;
+ <c:import url="rating.jsp">
+ <c:param name="path" value="${model.dir.path}"/>
+ <c:param name="readonly" value="true"/>
+ <c:param name="rating" value="${model.averageRating}"/>
+ </c:import>
+ </c:if>
+</h1>
+
+<c:if test="${not model.partyMode}">
+<h2>
+ <c:if test="${model.navigateUpAllowed}">
+ <sub:url value="main.view" var="upUrl">
+ <sub:param name="id" value="${model.parent.id}"/>
+ </sub:url>
+ <a href="${upUrl}"><fmt:message key="main.up"/></a>
+ <c:set var="needSep" value="true"/>
+ </c:if>
+
+ <c:if test="${model.user.streamRole}">
+ <c:if test="${needSep}">|</c:if>
+ <a href="#" onclick="top.playQueue.onPlay(${model.dir.id});"><fmt:message key="main.playall"/></a> |
+ <a href="#" onclick="top.playQueue.onPlayRandom(${model.dir.id}, 10);"><fmt:message key="main.playrandom"/></a> |
+ <a href="#" onclick="top.playQueue.onAdd(${model.dir.id});"><fmt:message key="main.addall"/></a>
+ <c:set var="needSep" value="true"/>
+ </c:if>
+
+ <c:if test="${model.dir.album}">
+
+ <c:if test="${model.user.downloadRole}">
+ <sub:url value="download.view" var="downloadUrl">
+ <sub:param name="id" value="${model.dir.id}"/>
+ </sub:url>
+ <c:if test="${needSep}">|</c:if>
+ <a href="${downloadUrl}"><fmt:message key="common.download"/></a>
+ <c:set var="needSep" value="true"/>
+ </c:if>
+
+ <c:if test="${model.user.coverArtRole}">
+ <sub:url value="editTags.view" var="editTagsUrl">
+ <sub:param name="id" value="${model.dir.id}"/>
+ </sub:url>
+ <c:if test="${needSep}">|</c:if>
+ <a href="${editTagsUrl}"><fmt:message key="main.tags"/></a>
+ <c:set var="needSep" value="true"/>
+ </c:if>
+
+ </c:if>
+
+ <c:if test="${model.user.commentRole}">
+ <c:if test="${needSep}">|</c:if>
+ <a href="javascript:toggleComment()"><fmt:message key="main.comment"/></a>
+ </c:if>
+</h2>
+</c:if>
+
+<c:if test="${model.dir.album}">
+
+ <div class="detail">
+ <c:if test="${model.user.commentRole}">
+ <c:import url="rating.jsp">
+ <c:param name="path" value="${model.dir.path}"/>
+ <c:param name="readonly" value="false"/>
+ <c:param name="rating" value="${model.userRating}"/>
+ </c:import>
+ </c:if>
+
+ <c:if test="${model.user.shareRole}">
+ <a href="${shareUrl}"><img src="<spring:theme code="shareFacebookImage"/>" alt=""></a>
+ <a href="${shareUrl}"><img src="<spring:theme code="shareTwitterImage"/>" alt=""></a>
+ <a href="${shareUrl}"><img src="<spring:theme code="shareGooglePlusImage"/>" alt=""></a>
+ <a href="${shareUrl}"><span class="detail"><fmt:message key="main.sharealbum"/></span></a> |
+ </c:if>
+
+ <c:if test="${not empty model.artist and not empty model.album}">
+ <sub:url value="http://www.google.com/search" var="googleUrl" encoding="UTF-8">
+ <sub:param name="q" value="\"${model.artist}\" \"${model.album}\""/>
+ </sub:url>
+ <sub:url value="http://en.wikipedia.org/wiki/Special:Search" var="wikipediaUrl" encoding="UTF-8">
+ <sub:param name="search" value="\"${model.album}\""/>
+ <sub:param name="go" value="Go"/>
+ </sub:url>
+ <sub:url value="allmusic.view" var="allmusicUrl">
+ <sub:param name="album" value="${model.album}"/>
+ </sub:url>
+ <sub:url value="http://www.last.fm/search" var="lastFmUrl" encoding="UTF-8">
+ <sub:param name="q" value="\"${model.artist}\" \"${model.album}\""/>
+ <sub:param name="type" value="album"/>
+ </sub:url>
+ <fmt:message key="top.search"/> <a target="_blank" href="${googleUrl}">Google</a> |
+ <a target="_blank" href="${wikipediaUrl}">Wikipedia</a> |
+ <a target="_blank" href="${allmusicUrl}">allmusic</a> |
+ <a target="_blank" href="${lastFmUrl}">Last.fm</a>
+ </c:if>
+ </div>
+ <div class="detail" style="padding-top:0.2em">
+ <fmt:message key="main.playcount"><fmt:param value="${model.dir.playCount}"/></fmt:message>
+ <c:if test="${not empty model.dir.lastPlayed}">
+ <fmt:message key="main.lastplayed">
+ <fmt:param><fmt:formatDate type="date" dateStyle="long" value="${model.dir.lastPlayed}"/></fmt:param>
+ </fmt:message>
+ </c:if>
+ </div>
+
+</c:if>
+
+<div id="comment" class="albumComment"><sub:wiki text="${model.dir.comment}"/></div>
+
+<div id="commentForm" style="display:none">
+ <form method="post" action="setMusicFileInfo.view">
+ <input type="hidden" name="action" value="comment">
+ <input type="hidden" name="path" value="${model.dir.path}">
+ <textarea name="comment" rows="6" cols="70">${model.dir.comment}</textarea>
+ <input type="submit" value="<fmt:message key="common.save"/>">
+ </form>
+ <fmt:message key="main.wiki"/>
+</div>
+
+<script type='text/javascript'>
+ function toggleComment() {
+ $("#commentForm").toggle();
+ $("#comment").toggle();
+ }
+</script>
+
+
+<table cellpadding="10" style="width:100%">
+<tr style="vertical-align:top;">
+ <td style="vertical-align:top;">
+ <table style="border-collapse:collapse;white-space:nowrap">
+ <c:set var="cutoff" value="${model.visibility.captionCutoff}"/>
+ <c:forEach items="${model.children}" var="child" varStatus="loopStatus">
+ <%--@elvariable id="child" type="net.sourceforge.subsonic.domain.MediaFile"--%>
+ <c:choose>
+ <c:when test="${loopStatus.count % 2 == 1}">
+ <c:set var="class" value="class='bgcolor2'"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="class" value=""/>
+ </c:otherwise>
+ </c:choose>
+
+ <tr style="margin:0;padding:0;border:0">
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${child.id}"/>
+ <c:param name="video" value="${child.video and model.player.web}"/>
+ <c:param name="playEnabled" value="${model.user.streamRole and not model.partyMode}"/>
+ <c:param name="addEnabled" value="${model.user.streamRole and (not model.partyMode or not child.directory)}"/>
+ <c:param name="downloadEnabled" value="${model.user.downloadRole and not model.partyMode}"/>
+ <c:param name="starEnabled" value="true"/>
+ <c:param name="starred" value="${not empty child.starredDate}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+
+ <c:choose>
+ <c:when test="${child.directory}">
+ <sub:url value="main.view" var="childUrl">
+ <sub:param name="id" value="${child.id}"/>
+ </sub:url>
+ <td style="padding-left:0.25em" colspan="3">
+ <a href="${childUrl}" title="${child.name}"><span style="white-space:nowrap;"><str:truncateNicely upper="${cutoff}">${child.name}</str:truncateNicely></span></a>
+ </td>
+ <td style="padding-left:1.25em"><c:if test="${model.showAlbumYear and not empty child.year}"><span class="detail">${child.year}</span></c:if></td>
+ </c:when>
+
+ <c:otherwise>
+ <td ${class} style="padding-left:0.25em"><input type="checkbox" class="checkbox" id="songIndex${loopStatus.count - 1}">
+ <span id="songId${loopStatus.count - 1}" style="display: none">${child.id}</span></td>
+
+ <c:if test="${model.visibility.trackNumberVisible}">
+ <td ${class} style="padding-right:0.5em;text-align:right">
+ <span class="detail">${child.trackNumber}</span>
+ </td>
+ </c:if>
+
+ <td ${class} style="padding-right:1.25em;white-space:nowrap">
+ <span title="${child.title}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(child.title)}</str:truncateNicely></span>
+ </td>
+
+ <c:if test="${model.visibility.albumVisible}">
+ <td ${class} style="padding-right:1.25em;white-space:nowrap">
+ <span class="detail" title="${child.albumName}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(child.albumName)}</str:truncateNicely></span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.artistVisible and model.multipleArtists}">
+ <td ${class} style="padding-right:1.25em;white-space:nowrap">
+ <span class="detail" title="${child.artist}"><str:truncateNicely upper="${cutoff}">${fn:escapeXml(child.artist)}</str:truncateNicely></span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.genreVisible}">
+ <td ${class} style="padding-right:1.25em;white-space:nowrap">
+ <span class="detail">${child.genre}</span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.yearVisible}">
+ <td ${class} style="padding-right:1.25em">
+ <span class="detail">${child.year}</span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.formatVisible}">
+ <td ${class} style="padding-right:1.25em">
+ <span class="detail">${fn:toLowerCase(child.format)}</span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.fileSizeVisible}">
+ <td ${class} style="padding-right:1.25em;text-align:right">
+ <span class="detail"><sub:formatBytes bytes="${child.fileSize}"/></span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.durationVisible}">
+ <td ${class} style="padding-right:1.25em;text-align:right">
+ <span class="detail">${child.durationString}</span>
+ </td>
+ </c:if>
+
+ <c:if test="${model.visibility.bitRateVisible}">
+ <td ${class} style="padding-right:0.25em">
+ <span class="detail">
+ <c:if test="${not empty child.bitRate}">
+ ${child.bitRate} Kbps ${child.variableBitRate ? "vbr" : ""}
+ </c:if>
+ <c:if test="${child.video and not empty child.width and not empty child.height}">
+ (${child.width}x${child.height})
+ </c:if>
+ </span>
+ </td>
+ </c:if>
+
+
+ </c:otherwise>
+ </c:choose>
+ </tr>
+ </c:forEach>
+ </table>
+ </td>
+
+ <td style="vertical-align:top;width:100%">
+ <c:forEach items="${model.coverArts}" var="coverArt" varStatus="loopStatus">
+ <div style="float:left; padding:5px">
+ <c:import url="coverArt.jsp">
+ <c:param name="albumId" value="${coverArt.id}"/>
+ <c:param name="albumName" value="${coverArt.name}"/>
+ <c:param name="coverArtSize" value="${model.coverArtSize}"/>
+ <c:param name="showLink" value="${coverArt ne model.dir}"/>
+ <c:param name="showZoom" value="${coverArt eq model.dir}"/>
+ <c:param name="showChange" value="${(coverArt eq model.dir) and model.user.coverArtRole}"/>
+ <c:param name="showCaption" value="true"/>
+ <c:param name="appearAfter" value="${loopStatus.count * 30}"/>
+ </c:import>
+ </div>
+ </c:forEach>
+
+ <c:if test="${model.showGenericCoverArt}">
+ <div style="float:left; padding:5px">
+ <c:import url="coverArt.jsp">
+ <c:param name="albumId" value="${model.dir.id}"/>
+ <c:param name="coverArtSize" value="${model.coverArtSize}"/>
+ <c:param name="showLink" value="false"/>
+ <c:param name="showZoom" value="false"/>
+ <c:param name="showChange" value="${model.user.coverArtRole}"/>
+ <c:param name="appearAfter" value="0"/>
+ </c:import>
+ </div>
+ </c:if>
+ </td>
+
+ <td style="vertical-align:top;">
+ <div style="padding:0 1em 0 1em;">
+ <c:if test="${not empty model.ad}">
+ <div class="detail" style="text-align:center">
+ ${model.ad}
+ <br/>
+ <br/>
+ <sub:url value="donate.view" var="donateUrl">
+ <sub:param name="path" value="${model.dir.path}"/>
+ </sub:url>
+ <fmt:message key="main.donate"><fmt:param value="${donateUrl}"/><fmt:param value="${model.brand}"/></fmt:message>
+ </div>
+ </c:if>
+ </div>
+ </td>
+</tr>
+</table>
+
+<select id="moreActions" onchange="actionSelected(this.options[selectedIndex].id);" style="margin-bottom:1.0em">
+ <option id="top" selected="selected"><fmt:message key="main.more"/></option>
+ <option style="color:blue;"><fmt:message key="main.more.selection"/></option>
+ <option id="selectAll">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.selectall"/></option>
+ <option id="selectNone">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.selectnone"/></option>
+ <c:if test="${model.user.shareRole}">
+ <option id="share">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="main.more.share"/></option>
+ </c:if>
+ <c:if test="${model.user.downloadRole}">
+ <option id="download">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="common.download"/></option>
+ </c:if>
+ <option id="appendPlaylist">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.append"/></option>
+</select>
+
+<div style="padding-bottom: 1em">
+ <c:if test="${not empty model.previousAlbum}">
+ <sub:url value="main.view" var="previousUrl">
+ <sub:param name="id" value="${model.previousAlbum.id}"/>
+ </sub:url>
+ <div class="back" style="float:left;padding-right:10pt"><a href="${previousUrl}" title="${model.previousAlbum.name}">
+ <str:truncateNicely upper="30">${fn:escapeXml(model.previousAlbum.name)}</str:truncateNicely>
+ </a></div>
+ </c:if>
+ <c:if test="${not empty model.nextAlbum}">
+ <sub:url value="main.view" var="nextUrl">
+ <sub:param name="id" value="${model.nextAlbum.id}"/>
+ </sub:url>
+ <div class="forward" style="float:left"><a href="${nextUrl}" title="${model.nextAlbum.name}">
+ <str:truncateNicely upper="30">${fn:escapeXml(model.nextAlbum.name)}</str:truncateNicely>
+ </a></div>
+ </c:if>
+</div>
+
+<div id="dialog-select-playlist" title="<fmt:message key="main.addtoplaylist.title"/>" style="display: none;">
+ <p><fmt:message key="main.addtoplaylist.text"/></p>
+ <div id="dialog-select-playlist-list"></div>
+</div>
+
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/more.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/more.jsp
new file mode 100644
index 00000000..18be91fe
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/more.jsp
@@ -0,0 +1,159 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <style type="text/css">
+ #progressBar {width: 350px; height: 10px; border: 1px solid black; display:none;}
+ #progressBarContent {width: 0; height: 10px; background: url("<c:url value="/icons/progress.png"/>") repeat;}
+ </style>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/transferService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+
+ <script type="text/javascript">
+ function refreshProgress() {
+ transferService.getUploadInfo(updateProgress);
+ }
+
+ function updateProgress(uploadInfo) {
+
+ var progressBar = document.getElementById("progressBar");
+ var progressBarContent = document.getElementById("progressBarContent");
+ var progressText = document.getElementById("progressText");
+
+
+ if (uploadInfo.bytesTotal > 0) {
+ var percent = Math.ceil((uploadInfo.bytesUploaded / uploadInfo.bytesTotal) * 100);
+ var width = parseInt(percent * 3.5) + 'px';
+ progressBarContent.style.width = width;
+ progressText.innerHTML = percent + "<fmt:message key="more.upload.progress"/>";
+ progressBar.style.display = "block";
+ progressText.style.display = "block";
+ window.setTimeout("refreshProgress()", 1000);
+ } else {
+ progressBar.style.display = "none";
+ progressText.style.display = "none";
+ window.setTimeout("refreshProgress()", 5000);
+ }
+ }
+ </script>
+
+</head>
+<body class="mainframe bgcolor1" onload="${model.user.uploadRole ? "refreshProgress()" : ""}">
+
+<h1>
+ <img src="<spring:theme code="moreImage"/>" alt=""/>
+ <fmt:message key="more.title"/>
+</h1>
+
+<c:if test="${model.user.streamRole}">
+ <h2><img src="<spring:theme code="randomImage"/>" alt=""/>&nbsp;<fmt:message key="more.random.title"/></h2>
+
+ <form method="post" action="randomPlayQueue.view?">
+ <table>
+ <tr>
+ <td><fmt:message key="more.random.text"/></td>
+ <td>
+ <select name="size">
+ <option value="5"><fmt:message key="more.random.songs"><fmt:param value="5"/></fmt:message></option>
+ <option value="10" selected="true"><fmt:message key="more.random.songs"><fmt:param value="10"/></fmt:message></option>
+ <option value="20"><fmt:message key="more.random.songs"><fmt:param value="20"/></fmt:message></option>
+ <option value="50"><fmt:message key="more.random.songs"><fmt:param value="50"/></fmt:message></option>
+ </select>
+ </td>
+ <td><fmt:message key="more.random.genre"/></td>
+ <td>
+ <select name="genre">
+ <option value="any"><fmt:message key="more.random.anygenre"/></option>
+ <c:forEach items="${model.genres}" var="genre">
+ <option value="${genre}"><str:truncateNicely upper="20">${genre}</str:truncateNicely></option>
+ </c:forEach>
+ </select>
+ </td>
+ <td><fmt:message key="more.random.year"/></td>
+ <td>
+ <select name="year">
+ <option value="any"><fmt:message key="more.random.anyyear"/></option>
+
+ <c:forEach begin="0" end="${model.currentYear - 2006}" var="yearOffset">
+ <c:set var="year" value="${model.currentYear - yearOffset}"/>
+ <option value="${year} ${year}">${year}</option>
+ </c:forEach>
+
+ <option value="2005 2010">2005 &ndash; 2010</option>
+ <option value="2000 2005">2000 &ndash; 2005</option>
+ <option value="1990 2000">1990 &ndash; 2000</option>
+ <option value="1980 1990">1980 &ndash; 1990</option>
+ <option value="1970 1980">1970 &ndash; 1980</option>
+ <option value="1960 1970">1960 &ndash; 1970</option>
+ <option value="1950 1960">1950 &ndash; 1960</option>
+ <option value="0 1949">&lt; 1950</option>
+ </select>
+ </td>
+ <td><fmt:message key="more.random.folder"/></td>
+ <td>
+ <select name="musicFolderId">
+ <option value="-1"><fmt:message key="more.random.anyfolder"/></option>
+ <c:forEach items="${model.musicFolders}" var="musicFolder">
+ <option value="${musicFolder.id}">${musicFolder.name}</option>
+ </c:forEach>
+ </select>
+ </td>
+ <td>
+ <input type="submit" value="<fmt:message key="more.random.ok"/>">
+ </td>
+ </tr>
+ <c:if test="${not model.clientSidePlaylist}">
+ <tr>
+ <td colspan="9">
+ <input type="checkbox" name="autoRandom" id="autoRandom" class="checkbox"/>
+ <label for="autoRandom"><fmt:message key="more.random.auto"/></label>
+ </td>
+ </tr>
+ </c:if>
+ </table>
+ </form>
+</c:if>
+<h2><img src="<spring:theme code="androidImage"/>" alt=""/>&nbsp;<fmt:message key="more.apps.title"/></h2>
+<fmt:message key="more.apps.text"/>
+
+<h2><img src="<spring:theme code="wapImage"/>" alt=""/>&nbsp;<fmt:message key="more.mobile.title"/></h2>
+<fmt:message key="more.mobile.text"><fmt:param value="${model.brand}"/></fmt:message>
+
+<h2><img src="<spring:theme code="podcastImage"/>" alt=""/>&nbsp;<fmt:message key="more.podcast.title"/></h2>
+<fmt:message key="more.podcast.text"/>
+
+<c:if test="${model.user.uploadRole}">
+
+ <h2><img src="<spring:theme code="uploadImage"/>" alt=""/>&nbsp;<fmt:message key="more.upload.title"/></h2>
+
+ <form method="post" enctype="multipart/form-data" action="upload.view">
+ <table>
+ <tr>
+ <td><fmt:message key="more.upload.source"/></td>
+ <td colspan="2"><input type="file" id="file" name="file" size="40"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="more.upload.target"/></td>
+ <td><input type="text" id="dir" name="dir" size="37" value="${model.uploadDirectory}"/></td>
+ <td><input type="submit" value="<fmt:message key="more.upload.ok"/>"/></td>
+ </tr>
+ <tr>
+ <td colspan="2">
+ <input type="checkbox" checked name="unzip" id="unzip" class="checkbox"/>
+ <label for="unzip"><fmt:message key="more.upload.unzip"/></label>
+ </td>
+ </tr>
+ </table>
+ </form>
+
+
+ <p class="detail" id="progressText"/>
+
+ <div id="progressBar">
+ <div id="progressBarContent"/>
+ </div>
+
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/musicFolderSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/musicFolderSettings.jsp
new file mode 100644
index 00000000..283e6878
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/musicFolderSettings.jsp
@@ -0,0 +1,114 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.MusicFolderSettingsCommand"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="musicFolder"/>
+</c:import>
+
+<form:form commandName="command" action="musicFolderSettings.view" method="post">
+
+<table class="indent">
+ <tr>
+ <th><fmt:message key="musicfoldersettings.name"/></th>
+ <th><fmt:message key="musicfoldersettings.path"/></th>
+ <th style="padding-left:1em"><fmt:message key="musicfoldersettings.enabled"/></th>
+ <th style="padding-left:1em"><fmt:message key="common.delete"/></th>
+ <th></th>
+ </tr>
+
+ <c:forEach items="${command.musicFolders}" var="folder" varStatus="loopStatus">
+ <tr>
+ <td><form:input path="musicFolders[${loopStatus.count-1}].name" size="20"/></td>
+ <td><form:input path="musicFolders[${loopStatus.count-1}].path" size="40"/></td>
+ <td align="center" style="padding-left:1em"><form:checkbox path="musicFolders[${loopStatus.count-1}].enabled" cssClass="checkbox"/></td>
+ <td align="center" style="padding-left:1em"><form:checkbox path="musicFolders[${loopStatus.count-1}].delete" cssClass="checkbox"/></td>
+ <td><c:if test="${not folder.existing}"><span class="warning"><fmt:message key="musicfoldersettings.notfound"/></span></c:if></td>
+ </tr>
+ </c:forEach>
+
+ <tr>
+ <th colspan="4" align="left" style="padding-top:1em"><fmt:message key="musicfoldersettings.add"/></th>
+ </tr>
+
+ <tr>
+ <td><form:input path="newMusicFolder.name" size="20"/></td>
+ <td><form:input path="newMusicFolder.path" size="40"/></td>
+ <td align="center" style="padding-left:1em"><form:checkbox path="newMusicFolder.enabled" cssClass="checkbox"/></td>
+ <td></td>
+ </tr>
+
+</table>
+
+ <div style="padding-top: 1.2em;padding-bottom: 0.3em">
+ <span style="white-space: nowrap">
+ <fmt:message key="musicfoldersettings.scan"/>
+ <form:select path="interval">
+ <fmt:message key="musicfoldersettings.interval.never" var="never"/>
+ <fmt:message key="musicfoldersettings.interval.one" var="one"/>
+ <form:option value="-1" label="${never}"/>
+ <form:option value="1" label="${one}"/>
+
+ <c:forTokens items="2 3 7 14 30 60" delims=" " var="interval">
+ <fmt:message key="musicfoldersettings.interval.many" var="many"><fmt:param value="${interval}"/></fmt:message>
+ <form:option value="${interval}" label="${many}"/>
+ </c:forTokens>
+ </form:select>
+ <form:select path="hour">
+ <c:forEach begin="0" end="23" var="hour">
+ <fmt:message key="musicfoldersettings.hour" var="hourLabel"><fmt:param value="${hour}"/></fmt:message>
+ <form:option value="${hour}" label="${hourLabel}"/>
+ </c:forEach>
+ </form:select>
+ </span>
+ </div>
+
+ <p class="forward"><a href="musicFolderSettings.view?scanNow"><fmt:message key="musicfoldersettings.scannow"/></a></p>
+
+ <c:if test="${command.scanning}">
+ <p style="width:60%"><b><fmt:message key="musicfoldersettings.nowscanning"/></b></p>
+ </c:if>
+
+ <div>
+ <form:checkbox path="fastCache" cssClass="checkbox" id="fastCache"/>
+ <form:label path="fastCache"><fmt:message key="musicfoldersettings.fastcache"/></form:label>
+ </div>
+
+ <p class="detail" style="width:60%;white-space:normal;">
+ <fmt:message key="musicfoldersettings.fastcache.description"/>
+ </p>
+
+ <p class="forward"><a href="musicFolderSettings.view?expunge"><fmt:message key="musicfoldersettings.expunge"/></a></p>
+ <p class="detail" style="width:60%;white-space:normal;margin-top:-10px;">
+ <fmt:message key="musicfoldersettings.expunge.description"/>
+ </p>
+
+ <%--<div>--%>
+ <%--<form:checkbox path="organizeByFolderStructure" cssClass="checkbox" id="organizeByFolderStructure"/>--%>
+ <%--<form:label path="organizeByFolderStructure"><fmt:message key="musicfoldersettings.organizebyfolderstructure"/></form:label>--%>
+ <%--</div>--%>
+
+ <%--<p class="detail" style="width:60%;white-space:normal;">--%>
+ <%--<fmt:message key="musicfoldersettings.organizebyfolderstructure.description"/>--%>
+ <%--</p>--%>
+
+ <p >
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </p>
+
+</form:form>
+
+<c:if test="${command.reload}">
+ <script type="text/javascript">
+ parent.frames.upper.location.href="top.view?";
+ parent.frames.left.location.href="left.view?";
+ parent.frames.right.location.href="right.view?";
+ </script>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/networkSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/networkSettings.jsp
new file mode 100644
index 00000000..c59c16c9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/networkSettings.jsp
@@ -0,0 +1,107 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.NetworkSettingsCommand"--%>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/multiService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+
+ function init() {
+ enableUrlRedirectionFields();
+ refreshStatus();
+ }
+
+ function refreshStatus() {
+ multiService.getNetworkStatus(updateStatus);
+ }
+
+ function updateStatus(networkStatus) {
+ dwr.util.setValue("portForwardingStatus", networkStatus.portForwardingStatusText);
+ dwr.util.setValue("urlRedirectionStatus", networkStatus.urlRedirectionStatusText);
+ window.setTimeout("refreshStatus()", 1000);
+ }
+
+ function enableUrlRedirectionFields() {
+ var checkbox = $("urlRedirectionEnabled");
+ var field = $("urlRedirectFrom");
+
+ if (checkbox && checkbox.checked) {
+ field.enable();
+ } else {
+ field.disable();
+ }
+ }
+
+ </script>
+</head>
+<body class="mainframe bgcolor1" onload="init()">
+<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
+<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="network"/>
+</c:import>
+
+<p style="padding-top:1em"><fmt:message key="networksettings.text"/></p>
+
+<form:form commandName="command" action="networkSettings.view" method="post">
+ <p style="padding-top:1em">
+ <form:checkbox id="portForwardingEnabled" path="portForwardingEnabled"/>
+ <label for="portForwardingEnabled"><fmt:message key="networksettings.portforwardingenabled"/></label>
+ </p>
+
+ <div style="padding-left:2em;max-width:60em">
+ <p>
+ <fmt:message key="networksettings.portforwardinghelp"><fmt:param>${command.port}</fmt:param></fmt:message>
+ </p>
+
+ <p class="detail">
+ <fmt:message key="networksettings.status"/>
+ <span id="portForwardingStatus" style="margin-left:0.25em"></span>
+ </p>
+ </div>
+
+ <p style="padding-top:1em"><form:checkbox id="urlRedirectionEnabled" path="urlRedirectionEnabled"
+ onclick="enableUrlRedirectionFields()"/>
+ <label for="urlRedirectionEnabled"><fmt:message key="networksettings.urlredirectionenabled"/></label>
+ </p>
+
+ <div style="padding-left:2em">
+
+ <p>http://<form:input id="urlRedirectFrom" path="urlRedirectFrom" size="16" cssStyle="margin-left:0.25em"/>.subsonic.org</p>
+
+ <p class="detail">
+ <fmt:message key="networksettings.status"/>
+ <span id="urlRedirectionStatus" style="margin-left:0.25em"></span>
+ <span id="urlRedirectionTestStatus" style="margin-left:0.25em"></span>
+ </p>
+ </div>
+
+ <c:if test="${command.trial}">
+ <fmt:formatDate value="${command.trialExpires}" dateStyle="long" var="expiryDate"/>
+
+ <p class="warning" style="padding-top:1em">
+ <c:choose>
+ <c:when test="${command.trialExpired}">
+ <fmt:message key="networksettings.trialexpired"><fmt:param>${expiryDate}</fmt:param></fmt:message>
+ </c:when>
+ <c:otherwise>
+ <fmt:message
+ key="networksettings.trialnotexpired"><fmt:param>${expiryDate}</fmt:param></fmt:message>
+ </c:otherwise>
+ </c:choose>
+ </p>
+ </c:if>
+
+ <p style="padding-top:1em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </p>
+
+</form:form>
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/notFound.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/notFound.jsp
new file mode 100644
index 00000000..84e666af
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/notFound.jsp
@@ -0,0 +1,21 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+</head>
+
+<body class="mainframe bgcolor1">
+
+<h1>
+ <img src="<spring:theme code="errorImage"/>" alt=""/>
+ <fmt:message key="notFound.title"/>
+</h1>
+
+<fmt:message key="notFound.text"/>
+
+<div class="forward" style="float:left;padding-right:10pt"><a href="javascript:top.location.reload(true)"><fmt:message key="notFound.reload"/></a></div>
+<div class="forward" style="float:left"><a href="musicFolderSettings.view"><fmt:message key="notFound.scan"/></a></div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/passwordSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/passwordSettings.jsp
new file mode 100644
index 00000000..75fd3e01
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/passwordSettings.jsp
@@ -0,0 +1,45 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="password"/>
+ <c:param name="restricted" value="true"/>
+</c:import>
+
+<c:choose>
+
+ <c:when test="${command.ldapAuthenticated}">
+ <p><fmt:message key="usersettings.passwordnotsupportedforldap"/></p>
+ </c:when>
+
+ <c:otherwise>
+ <h2><fmt:message key="passwordsettings.title"><fmt:param>${command.username}</fmt:param></fmt:message></h2>
+ <form:form method="post" action="passwordSettings.view" commandName="command">
+ <table class="indent">
+ <tr>
+ <td><fmt:message key="usersettings.newpassword"/></td>
+ <td><form:password path="password"/></td>
+ <td class="warning"><form:errors path="password"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="usersettings.confirmpassword"/></td>
+ <td><form:password path="confirmPassword"/></td>
+ <td/>
+ </tr>
+ <tr>
+ <td colspan="3" style="padding-top:1.5em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </td>
+ </tr>
+
+ </table>
+ </form:form>
+ </c:otherwise>
+</c:choose>
+
+</body></html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp
new file mode 100644
index 00000000..2942d195
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp
@@ -0,0 +1,228 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.PersonalSettingsCommand"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+
+ <script type="text/javascript" language="javascript">
+ function enableLastFmFields() {
+ var checkbox = $("lastFm");
+ var table = $("lastFmTable");
+
+ if (checkbox && checkbox.checked) {
+ table.show();
+ } else {
+ table.hide();
+ }
+ }
+ </script>
+</head>
+
+<body class="mainframe bgcolor1" onload="enableLastFmFields()">
+<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
+<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="personal"/>
+ <c:param name="restricted" value="${not command.user.adminRole}"/>
+</c:import>
+
+<h2><fmt:message key="personalsettings.title"><fmt:param>${command.user.username}</fmt:param></fmt:message></h2>
+
+<fmt:message key="common.default" var="default"/>
+<form:form method="post" action="personalSettings.view" commandName="command">
+
+ <table style="white-space:nowrap" class="indent">
+
+ <tr>
+ <td><fmt:message key="personalsettings.language"/></td>
+ <td>
+ <form:select path="localeIndex" cssStyle="width:15em">
+ <form:option value="-1" label="${default}"/>
+ <c:forEach items="${command.locales}" var="locale" varStatus="loopStatus">
+ <form:option value="${loopStatus.count - 1}" label="${locale}"/>
+ </c:forEach>
+ </form:select>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="language"/></c:import>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="personalsettings.theme"/></td>
+ <td>
+ <form:select path="themeIndex" cssStyle="width:15em">
+ <form:option value="-1" label="${default}"/>
+ <c:forEach items="${command.themes}" var="theme" varStatus="loopStatus">
+ <form:option value="${loopStatus.count - 1}" label="${theme.name}"/>
+ </c:forEach>
+ </form:select>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="theme"/></c:import>
+ </td>
+ </tr>
+ </table>
+
+ <table class="indent">
+ <tr>
+ <th style="padding:0 0.5em 0.5em 0;text-align:left;"><fmt:message key="personalsettings.display"/></th>
+ <th style="padding:0 0.5em 0.5em 0.5em;text-align:center;"><fmt:message key="personalsettings.browse"/></th>
+ <th style="padding:0 0 0.5em 0.5em;text-align:center;"><fmt:message key="personalsettings.playlist"/></th>
+ <th style="padding:0 0 0.5em 0.5em">
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="visibility"/></c:import>
+ </th>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.tracknumber"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.trackNumberVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.trackNumberVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.artist"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.artistVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.artistVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.album"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.albumVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.albumVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.genre"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.genreVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.genreVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.year"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.yearVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.yearVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.bitrate"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.bitRateVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.bitRateVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.duration"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.durationVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.durationVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.format"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.formatVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.formatVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.filesize"/></td>
+ <td style="text-align:center"><form:checkbox path="mainVisibility.fileSizeVisible" cssClass="checkbox"/></td>
+ <td style="text-align:center"><form:checkbox path="playlistVisibility.fileSizeVisible" cssClass="checkbox"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.captioncutoff"/></td>
+ <td style="text-align:center"><form:input path="mainVisibility.captionCutoff" size="3"/></td>
+ <td style="text-align:center"><form:input path="playlistVisibility.captionCutoff" size="3"/></td>
+ </tr>
+ </table>
+
+ <table class="indent">
+ <tr>
+ <td><form:checkbox path="showNowPlayingEnabled" id="nowPlaying" cssClass="checkbox"/></td>
+ <td><label for="nowPlaying"><fmt:message key="personalsettings.shownowplaying"/></label></td>
+ <td style="padding-left:2em"><form:checkbox path="showChatEnabled" id="chat" cssClass="checkbox"/></td>
+ <td><label for="chat"><fmt:message key="personalsettings.showchat"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="nowPlayingAllowed" id="nowPlayingAllowed" cssClass="checkbox"/></td>
+ <td><label for="nowPlayingAllowed"><fmt:message key="personalsettings.nowplayingallowed"/></label></td>
+ <td style="padding-left:2em"><form:checkbox path="partyModeEnabled" id="partyModeEnabled" cssClass="checkbox"/></td>
+ <td><label for="partyModeEnabled"><fmt:message key="personalsettings.partymode"/></label>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="partymode"/></c:import>
+ </td>
+ </tr>
+ </table>
+
+ <table class="indent">
+ <tr>
+ <td><form:checkbox path="finalVersionNotificationEnabled" id="final" cssClass="checkbox"/></td>
+ <td><label for="final"><fmt:message key="personalsettings.finalversionnotification"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="betaVersionNotificationEnabled" id="beta" cssClass="checkbox"/></td>
+ <td><label for="beta"><fmt:message key="personalsettings.betaversionnotification"/></label></td>
+ </tr>
+ </table>
+
+ <table class="indent">
+ <tr>
+ <td><form:checkbox path="lastFmEnabled" id="lastFm" cssClass="checkbox" onclick="javascript:enableLastFmFields()"/></td>
+ <td><label for="lastFm"><fmt:message key="personalsettings.lastfmenabled"/></label></td>
+ </tr>
+ </table>
+
+ <table id="lastFmTable" style="padding-left:2em">
+ <tr>
+ <td><fmt:message key="personalsettings.lastfmusername"/></td>
+ <td><form:input path="lastFmUsername" size="24"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="personalsettings.lastfmpassword"/></td>
+ <td><form:password path="lastFmPassword" size="24"/></td>
+ </tr>
+ </table>
+
+ <p style="padding-top:1em;padding-bottom:1em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em"/>
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </p>
+
+ <h2><fmt:message key="personalsettings.avatar.title"/></h2>
+
+ <p style="padding-top:1em">
+ <c:forEach items="${command.avatars}" var="avatar">
+ <c:url value="avatar.view" var="avatarUrl">
+ <c:param name="id" value="${avatar.id}"/>
+ </c:url>
+ <span style="white-space:nowrap;">
+ <form:radiobutton id="avatar-${avatar.id}" path="avatarId" value="${avatar.id}"/>
+ <label for="avatar-${avatar.id}"><img src="${avatarUrl}" alt="${avatar.name}" width="${avatar.width}" height="${avatar.height}" style="padding-right:2em;padding-bottom:1em"/></label>
+ </span>
+ </c:forEach>
+ </p>
+ <p>
+ <form:radiobutton id="noAvatar" path="avatarId" value="-1"/>
+ <label for="noAvatar"><fmt:message key="personalsettings.avatar.none"/></label>
+ </p>
+ <p>
+ <form:radiobutton id="customAvatar" path="avatarId" value="-2"/>
+ <label for="customAvatar"><fmt:message key="personalsettings.avatar.custom"/>
+ <c:if test="${not empty command.customAvatar}">
+ <sub:url value="avatar.view" var="avatarUrl">
+ <sub:param name="username" value="${command.user.username}"/>
+ </sub:url>
+ <img src="${avatarUrl}" alt="${command.customAvatar.name}" width="${command.customAvatar.width}" height="${command.customAvatar.height}" style="padding-right:2em"/>
+ </c:if>
+ </label>
+ </p>
+</form:form>
+
+<form method="post" enctype="multipart/form-data" action="avatarUpload.view">
+ <table>
+ <tr>
+ <td style="padding-right:1em"><fmt:message key="personalsettings.avatar.changecustom"/></td>
+ <td style="padding-right:1em"><input type="file" id="file" name="file" size="40"/></td>
+ <td style="padding-right:1em"><input type="submit" value="<fmt:message key="personalsettings.avatar.upload"/>"/></td>
+ </tr>
+ </table>
+</form>
+
+<p class="detail" style="text-align:right">
+ <fmt:message key="personalsettings.avatar.courtesy"/>
+</p>
+
+<c:if test="${command.reloadNeeded}">
+ <script language="javascript" type="text/javascript">
+ parent.location.href="index.view?";
+ </script>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/playAddDownload.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/playAddDownload.jsp
new file mode 100644
index 00000000..e0852182
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/playAddDownload.jsp
@@ -0,0 +1,64 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<%@ include file="include.jsp" %>
+
+<%--
+PARAMETERS
+ id: ID of file.
+ video: Whether the file is a video (default false).
+ playEnabled: Whether the current user is allowed to play songs (default true).
+ addEnabled: Whether the current user is allowed to add songs to the playlist (default true).
+ downloadEnabled: Whether the current user is allowed to download songs (default false).
+ starEnabled: Whether to show star/unstar controls (default false).
+ starred: Whether the file is currently starred.
+ asTable: Whether to put the images in td tags.
+--%>
+
+<sub:url value="/download.view" var="downloadUrl">
+ <sub:param name="id" value="${param.id}"/>
+</sub:url>
+<c:if test="${param.starEnabled}">
+ <c:if test="${param.asTable}"><td></c:if>
+ <a href="#" onclick="toggleStar(${param.id}, '#starImage${param.id}'); return false;">
+ <c:choose>
+ <c:when test="${param.starred}">
+ <img id="starImage${param.id}" src="<spring:theme code="ratingOnImage"/>" alt="">
+ </c:when>
+ <c:otherwise>
+ <img id="starImage${param.id}" src="<spring:theme code="ratingOffImage"/>" alt="">
+ </c:otherwise>
+ </c:choose>
+ </a>
+ <c:if test="${param.asTable}"></td></c:if>
+</c:if>
+
+<c:if test="${param.asTable}"><td></c:if>
+<c:if test="${empty param.playEnabled or param.playEnabled}">
+ <c:choose>
+ <c:when test="${param.video}">
+ <sub:url value="/videoPlayer.view" var="videoUrl">
+ <sub:param name="id" value="${param.id}"/>
+ </sub:url>
+ <a href="${videoUrl}" target="main">
+ <img src="<spring:theme code="playImage"/>" alt="<fmt:message key="common.play"/>" title="<fmt:message key="common.play"/>"></a>
+ </c:when>
+ <c:otherwise>
+ <a href="javascript:noop()" onclick="top.playQueue.onPlay(${param.id});">
+ <img src="<spring:theme code="playImage"/>" alt="<fmt:message key="common.play"/>" title="<fmt:message key="common.play"/>"></a>
+ </c:otherwise>
+ </c:choose>
+</c:if>
+<c:if test="${param.asTable}"></td></c:if>
+
+<c:if test="${param.asTable}"><td></c:if>
+<c:if test="${(empty param.addEnabled or param.addEnabled) and not param.video}">
+ <a href="javascript:noop()" onclick="top.playQueue.onAdd(${param.id});">
+ <img src="<spring:theme code="addImage"/>" alt="<fmt:message key="common.add"/>" title="<fmt:message key="common.add"/>"></a>
+</c:if>
+<c:if test="${param.asTable}"></td></c:if>
+
+<c:if test="${param.asTable}"><td></c:if>
+<c:if test="${param.downloadEnabled}">
+ <a href="${downloadUrl}">
+ <img src="<spring:theme code="downloadImage"/>" alt="<fmt:message key="common.download"/>" title="<fmt:message key="common.download"/>"></a>
+</c:if>
+<c:if test="${param.asTable}"></td></c:if>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp
new file mode 100644
index 00000000..5ac4ac46
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp
@@ -0,0 +1,617 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head>
+ <%@ include file="head.jsp" %>
+ <%@ include file="jquery.jsp" %>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/nowPlayingService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/playQueueService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/playlistService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/swfobject.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/webfx/range.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/webfx/timer.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/webfx/slider.js"/>"></script>
+ <link type="text/css" rel="stylesheet" href="<c:url value="/script/webfx/luna.css"/>">
+</head>
+
+<body class="bgcolor2 playlistframe" onload="init()">
+
+<script type="text/javascript" language="javascript">
+ var player = null;
+ var songs = null;
+ var currentAlbumUrl = null;
+ var currentStreamUrl = null;
+ var startPlayer = false;
+ var repeatEnabled = false;
+ var slider = null;
+
+ function init() {
+ dwr.engine.setErrorHandler(null);
+ startTimer();
+
+ $("#dialog-select-playlist").dialog({resizable: true, height: 220, position: 'top', modal: true, autoOpen: false,
+ buttons: {
+ "<fmt:message key="common.cancel"/>": function() {
+ $(this).dialog("close");
+ }
+ }});
+
+ <c:choose>
+ <c:when test="${model.player.web}">
+ createPlayer();
+ </c:when>
+ <c:otherwise>
+ getPlayQueue();
+ </c:otherwise>
+ </c:choose>
+ }
+
+ function startTimer() {
+ <!-- Periodically check if the current song has changed. -->
+ nowPlayingService.getNowPlayingForCurrentPlayer(nowPlayingCallback);
+ setTimeout("startTimer()", 10000);
+ }
+
+ function nowPlayingCallback(nowPlayingInfo) {
+ if (nowPlayingInfo != null && nowPlayingInfo.streamUrl != currentStreamUrl) {
+ getPlayQueue();
+ if (currentAlbumUrl != nowPlayingInfo.albumUrl && top.main.updateNowPlaying) {
+ top.main.location.replace("nowPlaying.view?");
+ currentAlbumUrl = nowPlayingInfo.albumUrl;
+ }
+ <c:if test="${not model.player.web}">
+ currentStreamUrl = nowPlayingInfo.streamUrl;
+ updateCurrentImage();
+ </c:if>
+ }
+ }
+
+ function createPlayer() {
+ var flashvars = {
+ backcolor:"<spring:theme code="backgroundColor"/>",
+ frontcolor:"<spring:theme code="textColor"/>",
+ id:"player1"
+ };
+ var params = {
+ allowfullscreen:"true",
+ allowscriptaccess:"always"
+ };
+ var attributes = {
+ id:"player1",
+ name:"player1"
+ };
+ swfobject.embedSWF("<c:url value="/flash/jw-player-5.6.swf"/>", "placeholder", "340", "24", "9.0.0", false, flashvars, params, attributes);
+ }
+
+ function playerReady(thePlayer) {
+ player = document.getElementById("player1");
+ player.addModelListener("STATE", "stateListener");
+ getPlayQueue();
+ }
+
+ function stateListener(obj) { // IDLE, BUFFERING, PLAYING, PAUSED, COMPLETED
+ if (obj.newstate == "COMPLETED") {
+ onNext(repeatEnabled);
+ }
+ }
+
+ function getPlayQueue() {
+ playQueueService.getPlayQueue(playQueueCallback);
+ }
+
+ function onClear() {
+ var ok = true;
+ <c:if test="${model.partyMode}">
+ ok = confirm("<fmt:message key="playlist.confirmclear"/>");
+ </c:if>
+ if (ok) {
+ playQueueService.clear(playQueueCallback);
+ }
+ }
+ function onStart() {
+ playQueueService.start(playQueueCallback);
+ }
+ function onStop() {
+ playQueueService.stop(playQueueCallback);
+ }
+ function onGain(gain) {
+ playQueueService.setGain(gain);
+ }
+ function onSkip(index) {
+ <c:choose>
+ <c:when test="${model.player.web}">
+ skip(index);
+ </c:when>
+ <c:otherwise>
+ currentStreamUrl = songs[index].streamUrl;
+ playQueueService.skip(index, playQueueCallback);
+ </c:otherwise>
+ </c:choose>
+ }
+ function onNext(wrap) {
+ var index = parseInt(getCurrentSongIndex()) + 1;
+ if (wrap) {
+ index = index % songs.length;
+ }
+ skip(index);
+ }
+ function onPrevious() {
+ skip(parseInt(getCurrentSongIndex()) - 1);
+ }
+ function onPlay(id) {
+ startPlayer = true;
+ playQueueService.play(id, playQueueCallback);
+ }
+ function onPlayPlaylist(id) {
+ startPlayer = true;
+ playQueueService.playPlaylist(id, playQueueCallback);
+ }
+ function onPlayRandom(id, count) {
+ startPlayer = true;
+ playQueueService.playRandom(id, count, playQueueCallback);
+ }
+ function onAdd(id) {
+ startPlayer = false;
+ playQueueService.add(id, playQueueCallback);
+ }
+ function onShuffle() {
+ playQueueService.shuffle(playQueueCallback);
+ }
+ function onStar(index) {
+ playQueueService.toggleStar(index, playQueueCallback);
+ }
+ function onRemove(index) {
+ playQueueService.remove(index, playQueueCallback);
+ }
+ function onRemoveSelected() {
+ var indexes = new Array();
+ var counter = 0;
+ for (var i = 0; i < songs.length; i++) {
+ var index = i + 1;
+ if ($("#songIndex" + index).is(":checked")) {
+ indexes[counter++] = i;
+ }
+ }
+ playQueueService.removeMany(indexes, playQueueCallback);
+ }
+
+ function onUp(index) {
+ playQueueService.up(index, playQueueCallback);
+ }
+ function onDown(index) {
+ playQueueService.down(index, playQueueCallback);
+ }
+ function onToggleRepeat() {
+ playQueueService.toggleRepeat(playQueueCallback);
+ }
+ function onUndo() {
+ playQueueService.undo(playQueueCallback);
+ }
+ function onSortByTrack() {
+ playQueueService.sortByTrack(playQueueCallback);
+ }
+ function onSortByArtist() {
+ playQueueService.sortByArtist(playQueueCallback);
+ }
+ function onSortByAlbum() {
+ playQueueService.sortByAlbum(playQueueCallback);
+ }
+ function onSavePlaylist() {
+ playQueueService.savePlaylist(function () {top.left.updatePlaylists();});
+ }
+ function onAppendPlaylist() {
+ playlistService.getWritablePlaylists(playlistCallback);
+ }
+ function playlistCallback(playlists) {
+ $("#dialog-select-playlist-list").empty();
+ for (var i = 0; i < playlists.length; i++) {
+ var playlist = playlists[i];
+ $("<p class='dense'><b><a href='#' onclick='appendPlaylist(" + playlist.id + ")'>" + playlist.name + "</a></b></p>").appendTo("#dialog-select-playlist-list");
+ }
+ $("#dialog-select-playlist").dialog("open");
+ }
+ function appendPlaylist(playlistId) {
+ $("#dialog-select-playlist").dialog("close");
+
+ var mediaFileIds = new Array();
+ for (var i = 0; i < songs.length; i++) {
+ if ($("#songIndex" + (i + 1)).is(":checked")) {
+ mediaFileIds.push(songs[i].id);
+ }
+ }
+ playlistService.appendToPlaylist(playlistId, mediaFileIds, function (){top.left.updatePlaylists();});
+ }
+
+ function playQueueCallback(playQueue) {
+ songs = playQueue.entries;
+ repeatEnabled = playQueue.repeatEnabled;
+ if ($("#start")) {
+ if (playQueue.stopEnabled) {
+ $("#start").hide();
+ $("#stop").show();
+ } else {
+ $("#start").show();
+ $("#stop").hide();
+ }
+ }
+
+ if ($("#toggleRepeat")) {
+ var text = repeatEnabled ? "<fmt:message key="playlist.repeat_on"/>" : "<fmt:message key="playlist.repeat_off"/>";
+ $("#toggleRepeat").html(text);
+ }
+
+ if (songs.length == 0) {
+ $("#empty").show();
+ } else {
+ $("#empty").hide();
+ }
+
+ // Delete all the rows except for the "pattern" row
+ dwr.util.removeAllRows("playlistBody", { filter:function(tr) {
+ return (tr.id != "pattern");
+ }});
+
+ // Create a new set cloned from the pattern row
+ for (var i = 0; i < songs.length; i++) {
+ var song = songs[i];
+ var id = i + 1;
+ dwr.util.cloneNode("pattern", { idSuffix:id });
+ if ($("#trackNumber" + id)) {
+ $("#trackNumber" + id).html(song.trackNumber);
+ }
+ if (song.starred) {
+ $("#starSong" + id).attr("src", "<spring:theme code='ratingOnImage'/>");
+ } else {
+ $("#starSong" + id).attr("src", "<spring:theme code='ratingOffImage'/>");
+ }
+ if ($("#currentImage" + id) && song.streamUrl == currentStreamUrl) {
+ $("#currentImage" + id).show();
+ }
+ if ($("#title" + id)) {
+ $("#title" + id).html(truncate(song.title));
+ $("#title" + id).attr("title", song.title);
+ }
+ if ($("#titleUrl" + id)) {
+ $("#titleUrl" + id).html(truncate(song.title));
+ $("#titleUrl" + id).attr("title", song.title);
+ $("#titleUrl" + id).click(function () {onSkip(this.id.substring(8) - 1)});
+ }
+ if ($("#album" + id)) {
+ $("#album" + id).html(truncate(song.album));
+ $("#album" + id).attr("title", song.album);
+ $("#albumUrl" + id).attr("href", song.albumUrl);
+ }
+ if ($("#artist" + id)) {
+ $("#artist" + id).html(truncate(song.artist));
+ $("#artist" + id).attr("title", song.artist);
+ }
+ if ($("#genre" + id)) {
+ $("#genre" + id).html(song.genre);
+ }
+ if ($("#year" + id)) {
+ $("#year" + id).html(song.year);
+ }
+ if ($("#bitRate" + id)) {
+ $("#bitRate" + id).html(song.bitRate);
+ }
+ if ($("#duration" + id)) {
+ $("#duration" + id).html(song.durationAsString);
+ }
+ if ($("#format" + id)) {
+ $("#format" + id).html(song.format);
+ }
+ if ($("#fileSize" + id)) {
+ $("#fileSize" + id).html(song.fileSize);
+ }
+
+ $("#pattern" + id).show();
+ $("#pattern" + id).addClass((i % 2 == 0) ? "bgcolor1" : "bgcolor2");
+ }
+
+ if (playQueue.sendM3U) {
+ parent.frames.main.location.href="play.m3u?";
+ }
+
+ if (slider) {
+ slider.setValue(playQueue.gain * 100);
+ }
+
+ <c:if test="${model.player.web}">
+ triggerPlayer();
+ </c:if>
+ }
+
+ function triggerPlayer() {
+ if (startPlayer) {
+ startPlayer = false;
+ if (songs.length > 0) {
+ skip(0);
+ }
+ }
+ updateCurrentImage();
+ if (songs.length == 0) {
+ player.sendEvent("LOAD", new Array());
+ player.sendEvent("STOP");
+ }
+ }
+
+ function skip(index) {
+ if (index < 0 || index >= songs.length) {
+ return;
+ }
+
+ var song = songs[index];
+ currentStreamUrl = song.streamUrl;
+ updateCurrentImage();
+ var list = new Array();
+ list[0] = {
+ file:song.streamUrl,
+ title:song.title,
+ provider:"sound"
+ };
+
+ if (song.duration != null) {
+ list[0].duration = song.duration;
+ }
+ if (song.format == "aac" || song.format == "m4a") {
+ list[0].provider = "video";
+ }
+
+ player.sendEvent("LOAD", list);
+ player.sendEvent("PLAY");
+ }
+
+ function updateCurrentImage() {
+ for (var i = 0; i < songs.length; i++) {
+ var song = songs[i];
+ var id = i + 1;
+ var image = $("#currentImage" + id);
+
+ if (image) {
+ if (song.streamUrl == currentStreamUrl) {
+ image.show();
+ } else {
+ image.hide();
+ }
+ }
+ }
+ }
+
+ function getCurrentSongIndex() {
+ for (var i = 0; i < songs.length; i++) {
+ if (songs[i].streamUrl == currentStreamUrl) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ function truncate(s) {
+ if (s == null) {
+ return s;
+ }
+ var cutoff = ${model.visibility.captionCutoff};
+
+ if (s.length > cutoff) {
+ return s.substring(0, cutoff) + "...";
+ }
+ return s;
+ }
+
+ <!-- actionSelected() is invoked when the users selects from the "More actions..." combo box. -->
+ function actionSelected(id) {
+ if (id == "top") {
+ return;
+ } else if (id == "savePlaylist") {
+ onSavePlaylist();
+ } else if (id == "downloadPlaylist") {
+ location.href = "download.view?player=${model.player.id}";
+ } else if (id == "sharePlaylist") {
+ parent.frames.main.location.href = "createShare.view?player=${model.player.id}&" + getSelectedIndexes();
+ } else if (id == "sortByTrack") {
+ onSortByTrack();
+ } else if (id == "sortByArtist") {
+ onSortByArtist();
+ } else if (id == "sortByAlbum") {
+ onSortByAlbum();
+ } else if (id == "selectAll") {
+ selectAll(true);
+ } else if (id == "selectNone") {
+ selectAll(false);
+ } else if (id == "removeSelected") {
+ onRemoveSelected();
+ } else if (id == "download") {
+ location.href = "download.view?player=${model.player.id}&" + getSelectedIndexes();
+ } else if (id == "appendPlaylist") {
+ onAppendPlaylist();
+ }
+ $("#moreActions").prop("selectedIndex", 0);
+ }
+
+ function getSelectedIndexes() {
+ var result = "";
+ for (var i = 0; i < songs.length; i++) {
+ if ($("#songIndex" + (i + 1)).is(":checked")) {
+ result += "i=" + i + "&";
+ }
+ }
+ return result;
+ }
+
+ function selectAll(b) {
+ for (var i = 0; i < songs.length; i++) {
+ if (b) {
+ $("#songIndex" + (i + 1)).attr("checked", "checked");
+ } else {
+ $("#songIndex" + (i + 1)).removeAttr("checked");
+ }
+ }
+ }
+
+</script>
+
+<div class="bgcolor2" style="position:fixed; top:0; width:100%;padding-top:0.5em">
+ <table style="white-space:nowrap;">
+ <tr style="white-space:nowrap;">
+ <c:if test="${model.user.settingsRole}">
+ <td><select name="player" onchange="location='playQueue.view?player=' + options[selectedIndex].value;">
+ <c:forEach items="${model.players}" var="player">
+ <option ${player.id eq model.player.id ? "selected" : ""} value="${player.id}">${player.shortDescription}</option>
+ </c:forEach>
+ </select></td>
+ </c:if>
+ <c:if test="${model.player.web}">
+ <td style="width:340px; height:24px;padding-left:10px;padding-right:10px"><div id="placeholder">
+ <a href="http://www.adobe.com/go/getflashplayer" target="_blank"><fmt:message key="playlist.getflash"/></a>
+ </div></td>
+ </c:if>
+
+ <c:if test="${model.user.streamRole and not model.player.web}">
+ <td style="white-space:nowrap;" id="stop"><b><a href="#" onclick="onStop()"><fmt:message key="playlist.stop"/></a></b> | </td>
+ <td style="white-space:nowrap;" id="start"><b><a href="#" onclick="onStart()"><fmt:message key="playlist.start"/></a></b> | </td>
+ </c:if>
+
+ <c:if test="${model.player.jukebox}">
+ <td style="white-space:nowrap;">
+ <img src="<spring:theme code="volumeImage"/>" alt="">
+ </td>
+ <td style="white-space:nowrap;">
+ <div class="slider bgcolor2" id="slider-1" style="width:90px">
+ <input class="slider-input" id="slider-input-1" name="slider-input-1">
+ </div>
+ <script type="text/javascript">
+
+ var updateGainTimeoutId = 0;
+ slider = new Slider(document.getElementById("slider-1"), document.getElementById("slider-input-1"));
+ slider.onchange = function () {
+ clearTimeout(updateGainTimeoutId);
+ updateGainTimeoutId = setTimeout("updateGain()", 250);
+ };
+
+ function updateGain() {
+ var gain = slider.getValue() / 100.0;
+ onGain(gain);
+ }
+ </script>
+ </td>
+ </c:if>
+
+ <c:if test="${model.player.web}">
+ <td style="white-space:nowrap;"><a href="#" onclick="onPrevious()"><b>&laquo;</b></a></td>
+ <td style="white-space:nowrap;"><a href="#" onclick="onNext(false)"><b>&raquo;</b></a> |</td>
+ </c:if>
+
+ <td style="white-space:nowrap;"><a href="#" onclick="onClear()"><fmt:message key="playlist.clear"/></a> |</td>
+ <td style="white-space:nowrap;"><a href="#" onclick="onShuffle()"><fmt:message key="playlist.shuffle"/></a> |</td>
+
+ <c:if test="${model.player.web or model.player.jukebox or model.player.external}">
+ <td style="white-space:nowrap;"><a href="#" onclick="onToggleRepeat()"><span id="toggleRepeat"><fmt:message key="playlist.repeat_on"/></span></a> |</td>
+ </c:if>
+
+ <td style="white-space:nowrap;"><a href="#" onclick="onUndo()"><fmt:message key="playlist.undo"/></a> |</td>
+
+ <c:if test="${model.user.settingsRole}">
+ <td style="white-space:nowrap;"><a href="playerSettings.view?id=${model.player.id}" target="main"><fmt:message key="playlist.settings"/></a> |</td>
+ </c:if>
+
+ <td style="white-space:nowrap;"><select id="moreActions" onchange="actionSelected(this.options[selectedIndex].id)">
+ <option id="top" selected="selected"><fmt:message key="playlist.more"/></option>
+ <option style="color:blue;"><fmt:message key="playlist.more.playlist"/></option>
+ <option id="savePlaylist">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.save"/></option>
+ <c:if test="${model.user.downloadRole}">
+ <option id="downloadPlaylist">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="common.download"/></option>
+ </c:if>
+ <c:if test="${model.user.shareRole}">
+ <option id="sharePlaylist">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="main.more.share"/></option>
+ </c:if>
+ <option id="sortByTrack">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.sortbytrack"/></option>
+ <option id="sortByAlbum">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.sortbyalbum"/></option>
+ <option id="sortByArtist">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.sortbyartist"/></option>
+ <option style="color:blue;"><fmt:message key="playlist.more.selection"/></option>
+ <option id="selectAll">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.selectall"/></option>
+ <option id="selectNone">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.more.selectnone"/></option>
+ <option id="removeSelected">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.remove"/></option>
+ <c:if test="${model.user.downloadRole}">
+ <option id="download">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="common.download"/></option>
+ </c:if>
+ <option id="appendPlaylist">&nbsp;&nbsp;&nbsp;&nbsp;<fmt:message key="playlist.append"/></option>
+ </select>
+ </td>
+
+ </tr></table>
+</div>
+
+<div style="height:3.2em"></div>
+
+<p id="empty"><em><fmt:message key="playlist.empty"/></em></p>
+
+<table style="border-collapse:collapse;white-space:nowrap;">
+ <tbody id="playlistBody">
+ <tr id="pattern" style="display:none;margin:0;padding:0;border:0">
+ <td class="bgcolor2"><a href="#">
+ <img id="starSong" onclick="onStar(this.id.substring(8) - 1)" src="<spring:theme code="ratingOffImage"/>"
+ alt="" title=""></a></td>
+ <td class="bgcolor2"><a href="#">
+ <img id="removeSong" onclick="onRemove(this.id.substring(10) - 1)" src="<spring:theme code="removeImage"/>"
+ alt="<fmt:message key="playlist.remove"/>" title="<fmt:message key="playlist.remove"/>"></a></td>
+ <td class="bgcolor2"><a href="#">
+ <img id="up" onclick="onUp(this.id.substring(2) - 1)" src="<spring:theme code="upImage"/>"
+ alt="<fmt:message key="playlist.up"/>" title="<fmt:message key="playlist.up"/>"></a></td>
+ <td class="bgcolor2"><a href="#">
+ <img id="down" onclick="onDown(this.id.substring(4) - 1)" src="<spring:theme code="downImage"/>"
+ alt="<fmt:message key="playlist.down"/>" title="<fmt:message key="playlist.down"/>"></a></td>
+
+ <td class="bgcolor2" style="padding-left: 0.1em"><input type="checkbox" class="checkbox" id="songIndex"></td>
+ <td style="padding-right:0.25em"></td>
+
+ <c:if test="${model.visibility.trackNumberVisible}">
+ <td style="padding-right:0.5em;text-align:right"><span class="detail" id="trackNumber">1</span></td>
+ </c:if>
+
+ <td style="padding-right:1.25em">
+ <img id="currentImage" src="<spring:theme code="currentImage"/>" alt="" style="display:none">
+ <c:choose>
+ <c:when test="${model.player.externalWithPlaylist}">
+ <span id="title">Title</span>
+ </c:when>
+ <c:otherwise>
+ <a id="titleUrl" href="#">Title</a>
+ </c:otherwise>
+ </c:choose>
+ </td>
+
+ <c:if test="${model.visibility.albumVisible}">
+ <td style="padding-right:1.25em"><a id="albumUrl" target="main"><span id="album" class="detail">Album</span></a></td>
+ </c:if>
+ <c:if test="${model.visibility.artistVisible}">
+ <td style="padding-right:1.25em"><span id="artist" class="detail">Artist</span></td>
+ </c:if>
+ <c:if test="${model.visibility.genreVisible}">
+ <td style="padding-right:1.25em"><span id="genre" class="detail">Genre</span></td>
+ </c:if>
+ <c:if test="${model.visibility.yearVisible}">
+ <td style="padding-right:1.25em"><span id="year" class="detail">Year</span></td>
+ </c:if>
+ <c:if test="${model.visibility.formatVisible}">
+ <td style="padding-right:1.25em"><span id="format" class="detail">Format</span></td>
+ </c:if>
+ <c:if test="${model.visibility.fileSizeVisible}">
+ <td style="padding-right:1.25em;text-align:right;"><span id="fileSize" class="detail">Format</span></td>
+ </c:if>
+ <c:if test="${model.visibility.durationVisible}">
+ <td style="padding-right:1.25em;text-align:right;"><span id="duration" class="detail">Duration</span></td>
+ </c:if>
+ <c:if test="${model.visibility.bitRateVisible}">
+ <td style="padding-right:0.25em"><span id="bitRate" class="detail">Bit Rate</span></td>
+ </c:if>
+ </tr>
+ </tbody>
+</table>
+
+<div id="dialog-select-playlist" title="<fmt:message key="main.addtoplaylist.title"/>" style="display: none;">
+ <p><fmt:message key="main.addtoplaylist.text"/></p>
+ <div id="dialog-select-playlist-list"></div>
+</div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/playerSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/playerSettings.jsp
new file mode 100644
index 00000000..3381c3a8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/playerSettings.jsp
@@ -0,0 +1,177 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.PlayerSettingsCommand"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+</head>
+<body class="mainframe bgcolor1">
+<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
+<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="player"/>
+ <c:param name="restricted" value="${not command.admin}"/>
+</c:import>
+
+<fmt:message key="common.unknown" var="unknown"/>
+
+<c:choose>
+<c:when test="${empty command.players}">
+ <p><fmt:message key="playersettings.noplayers"/></p>
+</c:when>
+<c:otherwise>
+
+<c:url value="playerSettings.view" var="deleteUrl">
+ <c:param name="delete" value="${command.playerId}"/>
+</c:url>
+<c:url value="playerSettings.view" var="cloneUrl">
+ <c:param name="clone" value="${command.playerId}"/>
+</c:url>
+
+<table class="indent">
+ <tr>
+ <td><b><fmt:message key="playersettings.title"/></b></td>
+ <td>
+ <select name="player" onchange="location='playerSettings.view?id=' + options[selectedIndex].value;">
+ <c:forEach items="${command.players}" var="player">
+ <option ${player.id eq command.playerId ? "selected" : ""}
+ value="${player.id}">${player.description}</option>
+ </c:forEach>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td style="padding-right:1em"><div class="forward"><a href="${deleteUrl}"><fmt:message key="playersettings.forget"/></a></div></td>
+ <td><div class="forward"><a href="${cloneUrl}"><fmt:message key="playersettings.clone"/></a></div></td>
+ </tr>
+</table>
+
+<form:form commandName="command" method="post" action="playerSettings.view">
+<form:hidden path="playerId"/>
+
+<table class="ruleTable indent">
+ <c:forEach items="${command.technologyHolders}" var="technologyHolder">
+ <c:set var="technologyName">
+ <fmt:message key="playersettings.technology.${fn:toLowerCase(technologyHolder.name)}.title"/>
+ </c:set>
+
+ <tr>
+ <td class="ruleTableHeader">
+ <form:radiobutton id="radio-${technologyName}" path="technologyName" value="${technologyHolder.name}"/>
+ <b><label for="radio-${technologyName}">${technologyName}</label></b>
+ </td>
+ <td class="ruleTableCell" style="width:40em">
+ <fmt:message key="playersettings.technology.${fn:toLowerCase(technologyHolder.name)}.text"/>
+ </td>
+ </tr>
+ </c:forEach>
+</table>
+
+
+<table class="indent" style="border-spacing:3pt">
+ <tr>
+ <td><fmt:message key="playersettings.type"/></td>
+ <td colspan="3">
+ <c:choose>
+ <c:when test="${empty command.type}">${unknown}</c:when>
+ <c:otherwise>${command.type}</c:otherwise>
+ </c:choose>
+ </td>
+ </tr>
+ <tr>
+ <td><fmt:message key="playersettings.lastseen"/></td>
+ <td colspan="3"><fmt:formatDate value="${command.lastSeen}" type="both" dateStyle="long" timeStyle="medium"/></td>
+ </tr>
+
+ <tr>
+ <td colspan="4">&nbsp;</td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="playersettings.name"/></td>
+ <td><form:input path="name" size="16"/></td>
+ <td colspan="2"><c:import url="helpToolTip.jsp"><c:param name="topic" value="playername"/></c:import></td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="playersettings.coverartsize"/></td>
+ <td>
+ <form:select path="coverArtSchemeName" cssStyle="width:8em">
+ <c:forEach items="${command.coverArtSchemeHolders}" var="coverArtSchemeHolder">
+ <c:set var="coverArtSchemeName">
+ <fmt:message key="playersettings.coverart.${fn:toLowerCase(coverArtSchemeHolder.name)}"/>
+ </c:set>
+ <form:option value="${coverArtSchemeHolder.name}" label="${coverArtSchemeName}"/>
+ </c:forEach>
+ </form:select>
+ </td>
+ <td colspan="2"><c:import url="helpToolTip.jsp"><c:param name="topic" value="cover"/></c:import></td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="playersettings.maxbitrate"/></td>
+ <td>
+ <form:select path="transcodeSchemeName" cssStyle="width:8em">
+ <c:forEach items="${command.transcodeSchemeHolders}" var="transcodeSchemeHolder">
+ <form:option value="${transcodeSchemeHolder.name}" label="${transcodeSchemeHolder.description}"/>
+ </c:forEach>
+ </form:select>
+ </td>
+ <td>
+ <c:import url="helpToolTip.jsp"><c:param name="topic" value="transcode"/></c:import>
+ </td>
+ <td class="warning">
+ <c:if test="${not command.transcodingSupported}">
+ <fmt:message key="playersettings.nolame"/>
+ </c:if>
+ </td>
+ </tr>
+
+</table>
+
+<table class="indent" style="border-spacing:3pt">
+
+ <tr>
+ <td>
+ <form:checkbox path="dynamicIp" id="dynamicIp" cssClass="checkbox"/>
+ <label for="dynamicIp"><fmt:message key="playersettings.dynamicip"/></label>
+ </td>
+ <td><c:import url="helpToolTip.jsp"><c:param name="topic" value="dynamicip"/></c:import></td>
+ </tr>
+
+ <tr>
+ <td>
+ <form:checkbox path="autoControlEnabled" id="autoControlEnabled" cssClass="checkbox"/>
+ <label for="autoControlEnabled"><fmt:message key="playersettings.autocontrol"/></label>
+ </td>
+ <td><c:import url="helpToolTip.jsp"><c:param name="topic" value="autocontrol"/></c:import></td>
+ </tr>
+</table>
+
+ <c:if test="${not empty command.allTranscodings}">
+ <table class="indent">
+ <tr><td><b><fmt:message key="playersettings.transcodings"/></b></td></tr>
+ <c:forEach items="${command.allTranscodings}" var="transcoding" varStatus="loopStatus">
+ <c:if test="${loopStatus.count % 3 == 1}"><tr></c:if>
+ <td style="padding-right:2em">
+ <form:checkbox path="activeTranscodingIds" id="transcoding${transcoding.id}" value="${transcoding.id}" cssClass="checkbox"/>
+ <label for="transcoding${transcoding.id}">${transcoding.name}</label>
+ </td>
+ <c:if test="${loopStatus.count % 3 == 0 or loopStatus.count eq fn:length(command.allTranscodings)}"></tr></c:if>
+ </c:forEach>
+ </table>
+ </c:if>
+
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-top:1em;margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" style="margin-top:1em" onclick="location.href='nowPlaying.view'">
+</form:form>
+
+</c:otherwise>
+</c:choose>
+
+<c:if test="${command.reloadNeeded}">
+ <script language="javascript" type="text/javascript">parent.frames.playQueue.location.href="playQueue.view?"</script>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/playlist.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/playlist.jsp
new file mode 100644
index 00000000..b0cb1f74
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/playlist.jsp
@@ -0,0 +1,235 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <%@ include file="jquery.jsp" %>
+ <script type="text/javascript" src="<c:url value='/dwr/util.js'/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/playlistService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/starService.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+
+ var playlist;
+ var songs;
+
+ function init() {
+ dwr.engine.setErrorHandler(null);
+ $("#dialog-edit").dialog({resizable: true, width:400, position: 'top', modal: true, autoOpen: false,
+ buttons: {
+ "<fmt:message key="common.save"/>": function() {
+ $(this).dialog("close");
+ var name = $("#newName").val();
+ var comment = $("#newComment").val();
+ var isPublic = $("#newPublic").is(":checked");
+ $("#name").html(name);
+ $("#comment").html(comment);
+ playlistService.updatePlaylist(playlist.id, name, comment, isPublic, function (playlistInfo){playlistCallback(playlistInfo); top.left.updatePlaylists()});
+ },
+ "<fmt:message key="common.cancel"/>": function() {
+ $(this).dialog("close");
+ }
+ }});
+
+ $("#dialog-delete").dialog({resizable: false, height: 170, position: 'top', modal: true, autoOpen: false,
+ buttons: {
+ "<fmt:message key="common.delete"/>": function() {
+ $(this).dialog("close");
+ playlistService.deletePlaylist(playlist.id, function (){top.left.updatePlaylists(); location = "home.view";});
+ },
+ "<fmt:message key="common.cancel"/>": function() {
+ $(this).dialog("close");
+ }
+ }});
+ getPlaylist();
+ }
+
+ function getPlaylist() {
+ playlistService.getPlaylist(${model.playlist.id}, playlistCallback);
+ }
+
+ function playlistCallback(playlistInfo) {
+ this.playlist = playlistInfo.playlist;
+ this.songs = playlistInfo.entries;
+
+ if (songs.length == 0) {
+ $("#empty").show();
+ } else {
+ $("#empty").hide();
+ }
+
+
+ $("#songCount").html(playlist.fileCount);
+ $("#duration").html(playlist.durationAsString);
+
+ if (playlist.public) {
+ $("#shared").html("<fmt:message key="playlist2.shared"/>");
+ } else {
+ $("#shared").html("<fmt:message key="playlist2.notshared"/>");
+ }
+
+ // Delete all the rows except for the "pattern" row
+ dwr.util.removeAllRows("playlistBody", { filter:function(tr) {
+ return (tr.id != "pattern");
+ }});
+
+ // Create a new set cloned from the pattern row
+ for (var i = 0; i < songs.length; i++) {
+ var song = songs[i];
+ var id = i + 1;
+ dwr.util.cloneNode("pattern", { idSuffix:id });
+ if (song.starred) {
+ $("#starSong" + id).attr("src", "<spring:theme code='ratingOnImage'/>");
+ } else {
+ $("#starSong" + id).attr("src", "<spring:theme code='ratingOffImage'/>");
+ }
+ if ($("#title" + id)) {
+ $("#title" + id).html(truncate(song.title));
+ $("#title" + id).attr("title", song.title);
+ }
+ if ($("#album" + id)) {
+ $("#album" + id).html(truncate(song.album));
+ $("#album" + id).attr("title", song.album);
+ $("#albumUrl" + id).attr("href", "main.view?id=" + song.id);
+ }
+ if ($("#artist" + id)) {
+ $("#artist" + id).html(truncate(song.artist));
+ $("#artist" + id).attr("title", song.artist);
+ }
+ if ($("#duration" + id)) {
+ $("#duration" + id).html(song.durationAsString);
+ }
+
+ $("#pattern" + id).addClass((i % 2 == 0) ? "bgcolor2" : "bgcolor1");
+ $("#pattern" + id).show();
+ }
+ }
+
+ function truncate(s) {
+ if (s == null) {
+ return s;
+ }
+ var cutoff = 30;
+
+ if (s.length > cutoff) {
+ return s.substring(0, cutoff) + "...";
+ }
+ return s;
+ }
+
+ function onPlay(index) {
+ top.playQueue.onPlay(songs[index].id);
+ }
+ function onPlayAll() {
+ top.playQueue.onPlayPlaylist(playlist.id);
+ }
+ function onAdd(index) {
+ top.playQueue.onAdd(songs[index].id);
+ }
+ function onStar(index) {
+ playlistService.toggleStar(playlist.id, index, playlistCallback);
+ }
+ function onRemove(index) {
+ playlistService.remove(playlist.id, index, function (playlistInfo){playlistCallback(playlistInfo); top.left.updatePlaylists()});
+ }
+ function onUp(index) {
+ playlistService.up(playlist.id, index, playlistCallback);
+ }
+ function onDown(index) {
+ playlistService.down(playlist.id, index, playlistCallback);
+ }
+ function onEditPlaylist() {
+ $("#dialog-edit").dialog("open");
+ }
+ function onDeletePlaylist() {
+ $("#dialog-delete").dialog("open");
+ }
+
+ </script>
+</head>
+<body class="mainframe bgcolor1" onload="init()">
+
+<h1 id="name">${model.playlist.name}</h1>
+<h2>
+ <a href="#" onclick="onPlayAll();"><fmt:message key="common.play"/></a>
+
+ <c:if test="${model.user.downloadRole}">
+ <c:url value="download.view" var="downloadUrl"><c:param name="playlist" value="${model.playlist.id}"/></c:url>
+ | <a href="${downloadUrl}"><fmt:message key="common.download"/></a>
+ </c:if>
+ <c:if test="${model.editAllowed}">
+ | <a href="#" onclick="onEditPlaylist();"><fmt:message key="common.edit"/></a>
+ | <a href="#" onclick="onDeletePlaylist();"><fmt:message key="common.delete"/></a>
+ </c:if>
+ <c:url value="exportPlaylist.view" var="exportUrl"><c:param name="id" value="${model.playlist.id}"/></c:url>
+ | <a href="${exportUrl}"><fmt:message key="playlist2.export"/></a>
+
+</h2>
+
+<div id="comment" class="detail" style="padding-top:0.2em">${model.playlist.comment}</div>
+
+<div class="detail" style="padding-top:0.2em">
+ <fmt:message key="playlist2.created">
+ <fmt:param>${model.playlist.username}</fmt:param>
+ <fmt:param><fmt:formatDate type="date" dateStyle="long" value="${model.playlist.created}"/></fmt:param>
+ </fmt:message>.
+ <span id="shared"></span>.
+ <span id="songCount"></span> <fmt:message key="playlist2.songs"/> (<span id="duration"></span>)
+</div>
+
+<div style="height:0.7em"></div>
+
+<p id="empty" style="display: none;"><em><fmt:message key="playlist2.empty"/></em></p>
+
+<table style="border-collapse:collapse;white-space:nowrap">
+ <tbody id="playlistBody">
+ <tr id="pattern" style="display:none;margin:0;padding:0;border:0">
+ <td class="bgcolor1"><a href="#">
+ <img id="starSong" onclick="onStar(this.id.substring(8) - 1)" src="<spring:theme code="ratingOffImage"/>" alt="" title=""></a></td>
+ <td class="bgcolor1"><a href="#">
+ <img id="play" src="<spring:theme code="playImage"/>" alt="<fmt:message key="common.play"/>" title="<fmt:message key="common.play"/>"
+ onclick="onPlay(this.id.substring(4) - 1)"></a></td>
+ <td class="bgcolor1"><a href="#">
+ <img id="add" src="<spring:theme code="addImage"/>" alt="<fmt:message key="common.add"/>" title="<fmt:message key="common.add"/>"
+ onclick="onAdd(this.id.substring(3) - 1)"></a></td>
+
+ <td style="padding-right:0.25em"></td>
+ <td style="padding-right:1.25em"><span id="title">Title</span></td>
+ <td style="padding-right:1.25em"><a id="albumUrl" target="main"><span id="album" class="detail">Album</span></a></td>
+ <td style="padding-right:1.25em"><span id="artist" class="detail">Artist</span></td>
+ <td style="padding-right:1.25em;text-align:right;"><span id="duration" class="detail">Duration</span></td>
+
+ <c:if test="${model.editAllowed}">
+ <td class="bgcolor1"><a href="#">
+ <img id="removeSong" onclick="onRemove(this.id.substring(10) - 1)" src="<spring:theme code="removeImage"/>"
+ alt="<fmt:message key="playlist.remove"/>" title="<fmt:message key="playlist.remove"/>"></a></td>
+ <td class="bgcolor1"><a href="#">
+ <img id="up" onclick="onUp(this.id.substring(2) - 1)" src="<spring:theme code="upImage"/>"
+ alt="<fmt:message key="playlist.up"/>" title="<fmt:message key="playlist.up"/>"></a></td>
+ <td class="bgcolor1"><a href="#">
+ <img id="down" onclick="onDown(this.id.substring(4) - 1)" src="<spring:theme code="downImage"/>"
+ alt="<fmt:message key="playlist.down"/>" title="<fmt:message key="playlist.down"/>"></a></td>
+ </c:if>
+
+ </tr>
+ </tbody>
+</table>
+
+<div id="dialog-delete" title="<fmt:message key="common.confirm"/>" style="display: none;">
+ <p><span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span>
+ <fmt:message key="playlist2.confirmdelete"/></p>
+</div>
+
+<div id="dialog-edit" title="<fmt:message key="common.edit"/>" style="display: none;">
+ <form>
+ <label for="newName" style="display:block;"><fmt:message key="playlist2.name"/></label>
+ <input type="text" name="newName" id="newName" value="${model.playlist.name}" class="ui-widget-content"
+ style="display:block;width:95%;"/>
+ <label for="newComment" style="display:block;margin-top:1em"><fmt:message key="playlist2.comment"/></label>
+ <input type="text" name="newComment" id="newComment" value="${model.playlist.comment}" class="ui-widget-content"
+ style="display:block;width:95%;"/>
+ <input type="checkbox" name="newPublic" id="newPublic" ${model.playlist.public ? "checked='checked'" : ""} style="margin-top:1.5em" class="ui-widget-content"/>
+ <label for="newPublic"><fmt:message key="playlist2.public"/></label>
+ </form>
+</div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/podcast.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/podcast.jsp
new file mode 100644
index 00000000..6f2b88d8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/podcast.jsp
@@ -0,0 +1,26 @@
+<%@ include file="include.jsp" %>
+<%@ page language="java" contentType="text/xml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<rss version="2.0">
+ <channel>
+ <title>Subsonic Podcast</title>
+ <link>${model.url}</link>
+ <description>Subsonic Podcast</description>
+ <language>en-us</language>
+ <image>
+ <url>http://subsonic.org/pages/inc/img/subsonic.png</url>
+ <title>Subsonic Podcast</title>
+ </image>
+
+ <c:forEach var="podcast" items="${model.podcasts}">
+ <item>
+ <title>${fn:escapeXml(podcast.name)}</title>
+ <link>${model.url}</link>
+ <description>Subsonic playlist "${fn:escapeXml(podcast.name)}"</description>
+ <pubDate>${podcast.publishDate}</pubDate>
+ <enclosure url="${podcast.enclosureUrl}" length="${podcast.length}" type="${podcast.type}"/>
+ </item>
+ </c:forEach>
+
+ </channel>
+</rss>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/podcastReceiver.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/podcastReceiver.jsp
new file mode 100644
index 00000000..35a0ffdb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/podcastReceiver.jsp
@@ -0,0 +1,269 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+</head>
+<body class="mainframe bgcolor1">
+
+<script type="text/javascript" language="javascript">
+ var channelCount = ${fn:length(model.channels)};
+
+ function downloadSelected() {
+ location.href = "podcastReceiverAdmin.view?downloadChannel=" + getSelectedChannels() +
+ "&downloadEpisode=" + getSelectedEpisodes() +
+ "&expandedChannels=" + getExpandedChannels();
+ }
+
+ function deleteSelected() {
+ if (confirm("<fmt:message key="podcastreceiver.confirmdelete"/>")) {
+ location.href = "podcastReceiverAdmin.view?deleteChannel=" + getSelectedChannels() +
+ "&deleteEpisode=" + getSelectedEpisodes() +
+ "&expandedChannels=" + getExpandedChannels();
+ }
+ }
+
+ function refreshChannels() {
+ location.href = "podcastReceiverAdmin.view?refresh=" +
+ "&expandedChannels=" + getExpandedChannels();
+ }
+
+ function refreshPage() {
+ location.href = "podcastReceiver.view?expandedChannels=" + getExpandedChannels();
+ }
+
+ function toggleEpisodes(channelIndex) {
+ for (var i = 0; i < episodeCount; i++) {
+ var row = $("episodeRow" + i);
+ if (row.title == "channel" + channelIndex) {
+ row.toggle();
+ $("channelExpanded" + channelIndex).checked = row.visible() ? "checked" : "";
+ }
+ }
+ }
+
+ function toggleAllEpisodes(visible) {
+ for (var i = 0; i < episodeCount; i++) {
+ var row = $("episodeRow" + i);
+ if (visible) {
+ row.show();
+ } else {
+ row.hide();
+ }
+ }
+ for (i = 0; i < channelCount; i++) {
+ $("channelExpanded" + i).checked = visible ? "checked" : "";
+ }
+ }
+
+ function getExpandedChannels() {
+ var result = "";
+ for (var i = 0; i < channelCount; i++) {
+ var checkbox = $("channelExpanded" + i);
+ if (checkbox.checked) {
+ result += (checkbox.value + " ");
+ }
+ }
+ return result;
+ }
+
+ function getSelectedChannels() {
+ var result = "";
+ for (var i = 0; i < channelCount; i++) {
+ var checkbox = $("channel" + i);
+ if (checkbox.checked) {
+ result += (checkbox.value + " ");
+ }
+ }
+ return result;
+ }
+
+ function getSelectedEpisodes() {
+ var result = "";
+ for (var i = 0; i < episodeCount; i++) {
+ var checkbox = $("episode" + i);
+ if (checkbox.checked) {
+ result += (checkbox.value + " ");
+ }
+ }
+ return result;
+ }
+</script>
+
+<h1>
+ <img src="<spring:theme code="podcastLargeImage"/>" alt=""/>
+ <fmt:message key="podcastreceiver.title"/>
+</h1>
+
+<table><tr>
+ <td style="padding-right:2em"><div class="forward"><a href="javascript:toggleAllEpisodes(true)"><fmt:message key="podcastreceiver.expandall"/></a></div></td>
+ <td style="padding-right:2em"><div class="forward"><a href="javascript:toggleAllEpisodes(false)"><fmt:message key="podcastreceiver.collapseall"/></a></div></td>
+</tr></table>
+
+<table style="border-collapse:collapse;white-space:nowrap">
+
+ <c:set var="episodeCount" value="0"/>
+
+ <c:forEach items="${model.channels}" var="channel" varStatus="i">
+
+ <c:set var="title" value="${channel.key.title}"/>
+ <c:if test="${empty title}">
+ <c:set var="title" value="${channel.key.url}"/>
+ </c:if>
+
+ <c:set var="channelExpanded" value="false"/>
+ <c:forEach items="${model.expandedChannels}" var="expandedChannelId">
+ <c:if test="${expandedChannelId eq channel.key.id}">
+ <c:set var="channelExpanded" value="true"/>
+ </c:if>
+ </c:forEach>
+
+ <tr style="margin:0;padding:0;border:0">
+ <td style="padding-top:1em">
+ <input type="checkbox" class="checkbox" id="channel${i.index}" value="${channel.key.id}"/>
+ <input type="checkbox" class="checkbox" id="channelExpanded${i.index}" value="${channel.key.id}" style="display:none"
+ <c:if test="${channelExpanded}">checked="checked"</c:if>/>
+ </td>
+ <td colspan="6" style="padding-left:0.25em;padding-top:1em">
+ <a href="javascript:toggleEpisodes(${i.index})">
+ <span title="${title}"><b><str:truncateNicely upper="40">${title}</str:truncateNicely></b></span>
+ (${fn:length(channel.value)})
+ </a>
+ </td>
+ <td style="padding-left:1.5em;padding-top:1em;text-align:center;">
+ <span class="detail"><fmt:message key="podcastreceiver.status.${fn:toLowerCase(channel.key.status)}"/></span>
+ </td>
+ <td style="padding-left:1.5em;padding-top:1em">
+ <c:choose>
+ <c:when test="${channel.key.status eq 'ERROR'}">
+ <span class="detail warning" title="${channel.key.errorMessage}"><str:truncateNicely upper="100">${channel.key.errorMessage}</str:truncateNicely></span>
+ </c:when>
+ <c:otherwise>
+ <span class="detail" title="${channel.key.description}"><str:truncateNicely upper="100">${channel.key.description}</str:truncateNicely></span>
+ </c:otherwise>
+ </c:choose>
+ </td>
+ </tr>
+
+ <c:set var="class" value=""/>
+
+ <c:forEach items="${channel.value}" var="episode" varStatus="j">
+
+ <c:choose>
+ <c:when test="${empty class}">
+ <c:set var="class" value="class='bgcolor2'"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="class" value=""/>
+ </c:otherwise>
+ </c:choose>
+ <tr title="channel${i.index}" id="episodeRow${episodeCount}" style="margin:0;padding:0;border:0;display:${channelExpanded ? "table-row" : "none"}">
+
+ <td><input type="checkbox" class="checkbox" id="episode${episodeCount}" value="${episode.id}"/></td>
+
+ <c:choose>
+ <c:when test="${empty episode.path}">
+ <td ${class} colspan="3"/>
+ </c:when>
+ <c:otherwise>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${episode.mediaFileId}"/>
+ <c:param name="playEnabled" value="${model.user.streamRole and not model.partyMode}"/>
+ <c:param name="addEnabled" value="${model.user.streamRole and not model.partyMode}"/>
+ <c:param name="downloadEnabled" value="false"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+ </c:otherwise>
+ </c:choose>
+
+ <c:set var="episodeCount" value="${episodeCount + 1}"/>
+
+
+ <sub:url value="main.view" var="mainUrl">
+ <sub:param name="path" value="${episode.path}"/>
+ </sub:url>
+
+
+ <td ${class} style="padding-left:0.6em">
+ <span title="${episode.title}">
+ <c:choose>
+ <c:when test="${empty episode.path}">
+ <str:truncateNicely upper="40">${episode.title}</str:truncateNicely>
+ </c:when>
+ <c:otherwise>
+ <a target="main" href="${mainUrl}"><str:truncateNicely upper="40">${episode.title}</str:truncateNicely></a>
+ </c:otherwise>
+ </c:choose>
+ </span>
+ </td>
+
+ <td ${class} style="padding-left:1.5em">
+ <span class="detail">${episode.duration}</span>
+ </td>
+
+ <td ${class} style="padding-left:1.5em">
+ <span class="detail"><fmt:formatDate value="${episode.publishDate}" dateStyle="medium"/></span>
+ </td>
+
+ <td ${class} style="padding-left:1.5em;text-align:center">
+ <span class="detail">
+ <c:choose>
+ <c:when test="${episode.status eq 'DOWNLOADING'}">
+ <fmt:formatNumber type="percent" value="${episode.completionRate}"/>
+ </c:when>
+ <c:otherwise>
+ <fmt:message key="podcastreceiver.status.${fn:toLowerCase(episode.status)}"/>
+ </c:otherwise>
+ </c:choose>
+ </span>
+ </td>
+
+ <td ${class} style="padding-left:1.5em">
+ <c:choose>
+ <c:when test="${episode.status eq 'ERROR'}">
+ <span class="detail warning" title="${episode.errorMessage}"><str:truncateNicely upper="100">${episode.errorMessage}</str:truncateNicely></span>
+ </c:when>
+ <c:otherwise>
+ <span class="detail" title="${episode.description}"><str:truncateNicely upper="100">${episode.description}</str:truncateNicely></span>
+ </c:otherwise>
+ </c:choose>
+ </td>
+
+ </tr>
+ </c:forEach>
+ </c:forEach>
+</table>
+
+<script type="text/javascript" language="javascript">
+ var episodeCount = ${episodeCount};
+</script>
+
+<table style="padding-top:1em"><tr>
+ <c:if test="${model.user.podcastRole}">
+ <td style="padding-right:2em"><div class="forward"><a href="javascript:downloadSelected()"><fmt:message key="podcastreceiver.downloadselected"/></a></div></td>
+ <td style="padding-right:2em"><div class="forward"><a href="javascript:deleteSelected()"><fmt:message key="podcastreceiver.deleteselected"/></a></div></td>
+ <td style="padding-right:2em"><div class="forward"><a href="javascript:refreshChannels()"><fmt:message key="podcastreceiver.check"/></a></div></td>
+ </c:if>
+ <td style="padding-right:2em"><div class="forward"><a href="javascript:refreshPage()"><fmt:message key="podcastreceiver.refresh"/></a></div></td>
+ <c:if test="${model.user.adminRole}">
+ <td style="padding-right:2em"><div class="forward"><a href="podcastSettings.view?"><fmt:message key="podcastreceiver.settings"/></a></div></td>
+ </c:if>
+</tr></table>
+
+<c:if test="${model.user.podcastRole}">
+ <form method="post" action="podcastReceiverAdmin.view?">
+ <input type="hidden" name="expandedChannels" value=""/>
+ <table>
+ <tr>
+ <td><fmt:message key="podcastreceiver.subscribe"/></td>
+ <td><input type="text" name="add" value="http://" style="width:30em" onclick="select()"/></td>
+ <td><input type="submit" value="<fmt:message key="common.ok"/>"/></td>
+ </tr>
+ </table>
+ </form>
+</c:if>
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/podcastSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/podcastSettings.jsp
new file mode 100644
index 00000000..07d99e28
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/podcastSettings.jsp
@@ -0,0 +1,88 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="podcast"/>
+</c:import>
+
+<form:form commandName="command" action="podcastSettings.view" method="post">
+
+<table class="indent">
+ <tr>
+ <td><fmt:message key="podcastsettings.update"/></td>
+ <td>
+ <form:select path="interval" cssStyle="width:20em">
+ <fmt:message key="podcastsettings.interval.manually" var="never"/>
+ <fmt:message key="podcastsettings.interval.hourly" var="hourly"/>
+ <fmt:message key="podcastsettings.interval.daily" var="daily"/>
+ <fmt:message key="podcastsettings.interval.weekly" var="weekly"/>
+
+ <form:option value="-1" label="${never}"/>
+ <form:option value="1" label="${hourly}"/>
+ <form:option value="24" label="${daily}"/>
+ <form:option value="168" label="${weekly}"/>
+ </form:select>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="podcastsettings.keep"/></td>
+ <td>
+ <form:select path="episodeRetentionCount" cssStyle="width:20em">
+ <fmt:message key="podcastsettings.keep.all" var="all"/>
+ <fmt:message key="podcastsettings.keep.one" var="one"/>
+
+ <form:option value="-1" label="${all}"/>
+ <form:option value="1" label="${one}"/>
+
+ <c:forTokens items="2 3 4 5 10 20 30 50" delims=" " var="count">
+ <fmt:message key="podcastsettings.keep.many" var="many"><fmt:param value="${count}"/></fmt:message>
+ <form:option value="${count}" label="${many}"/>
+ </c:forTokens>
+
+ </form:select>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="podcastsettings.download"/></td>
+ <td>
+ <form:select path="episodeDownloadCount" cssStyle="width:20em">
+ <fmt:message key="podcastsettings.download.all" var="all"/>
+ <fmt:message key="podcastsettings.download.one" var="one"/>
+ <fmt:message key="podcastsettings.download.none" var="none"/>
+
+ <form:option value="-1" label="${all}"/>
+ <form:option value="1" label="${one}"/>
+
+ <c:forTokens items="2 3 4 5 10" delims=" " var="count">
+ <fmt:message key="podcastsettings.download.many" var="many"><fmt:param value="${count}"/></fmt:message>
+ <form:option value="${count}" label="${many}"/>
+ </c:forTokens>
+ <form:option value="0" label="${none}"/>
+
+ </form:select>
+ </td>
+ </tr>
+
+ <tr>
+ <td><fmt:message key="podcastsettings.folder"/></td>
+ <td><form:input path="folder" cssStyle="width:20em"/></td>
+ </tr>
+
+ <tr>
+ <td style="padding-top:1.5em" colspan="2">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </td>
+ </tr>
+
+</table>
+
+</form:form>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/rating.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/rating.jsp
new file mode 100644
index 00000000..9e956de3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/rating.jsp
@@ -0,0 +1,51 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<%@ include file="include.jsp" %>
+
+<%--
+Creates HTML for displaying the rating stars.
+PARAMETERS
+ path: Album path. May be null if readonly.
+ readonly: Whether rating can be changed.
+ rating: The rating, an integer from 0 (no rating), through 10 (lowest rating), to 50 (highest rating).
+--%>
+
+<c:forEach var="i" begin="1" end="5">
+
+ <sub:url value="setRating.view" var="ratingUrl">
+ <sub:param name="path" value="${param.path}"/>
+ <sub:param name="action" value="rating"/>
+ <sub:param name="rating" value="${i}"/>
+ </sub:url>
+
+ <c:choose>
+ <c:when test="${param.rating ge i * 10}">
+ <spring:theme code="ratingOnImage" var="imageUrl"/>
+ </c:when>
+ <c:when test="${param.rating ge i*10 - 7 and param.rating le i*10 - 3}">
+ <spring:theme code="ratingHalfImage" var="imageUrl"/>
+ </c:when>
+ <c:otherwise>
+ <spring:theme code="ratingOffImage" var="imageUrl"/>
+ </c:otherwise>
+ </c:choose>
+
+ <c:choose>
+ <c:when test="${param.readonly}">
+ <img src="${imageUrl}" style="margin-right:-3px" alt="" title="<fmt:message key="rating.rating"/> ${param.rating/10}">
+ </c:when>
+ <c:otherwise>
+ <a href="${ratingUrl}"><img src="${imageUrl}" style="margin-right:-3px" alt="" title="<fmt:message key="rating.rating"/> ${i}"></a>
+ </c:otherwise>
+ </c:choose>
+
+</c:forEach>
+
+<sub:url value="setRating.view" var="clearRatingUrl">
+ <sub:param name="path" value="${param.path}"/>
+ <sub:param name="action" value="rating"/>
+ <sub:param name="rating" value="0"/>
+</sub:url>
+
+<c:if test="${not param.readonly}">
+ | <a href="${clearRatingUrl}"><img src="<spring:theme code="clearRatingImage"/>" alt="" title="<fmt:message key="rating.clearrating"/>" style="margin-left:-3px; margin-right:5px"></a>
+</c:if>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/recover.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/recover.jsp
new file mode 100644
index 00000000..ff206d14
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/recover.jsp
@@ -0,0 +1,34 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1" onload="document.getElementById('usernameOrEmail').focus()">
+
+<form action="recover.view" method="POST">
+ <div class="bgcolor2" style="border:1px solid black; padding:20px 50px 20px 50px; margin-top:100px">
+
+ <div style="margin-left: auto; margin-right: auto; width: 45em">
+
+ <h1><fmt:message key="recover.title"/></h1>
+ <p style="padding-top: 1em; padding-bottom: 0.5em"><fmt:message key="recover.text"/></p>
+ <input type="text" id="usernameOrEmail" name="usernameOrEmail" style="width:18em;margin-right: 1em">
+ <input name="submit" type="submit" value="<fmt:message key="recover.send"/>">
+
+ <c:if test="${not empty model.sentTo}">
+ <p style="padding-top: 1em"><fmt:message key="recover.success"><fmt:param value="${model.sentTo}"/></fmt:message></p>
+ </c:if>
+
+ <c:if test="${not empty model.error}">
+ <p style="padding-top: 1em" class="warning"><fmt:message key="${model.error}"/></p>
+ </c:if>
+
+ <div class="back" style="margin-top: 1.5em"><a href="login.view"><fmt:message key="common.back"/></a></div>
+
+ </div>
+ </div>
+</form>
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/reload.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/reload.jsp
new file mode 100644
index 00000000..1b8384a7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/reload.jsp
@@ -0,0 +1,11 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head><body>
+
+<c:forEach items="${model.reloadFrames}" var="reloadFrame">
+ <script language="javascript" type="text/javascript">parent.frames.${reloadFrame.frame}.location.href="${reloadFrame.view}"</script>
+</c:forEach>
+
+</body></html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/rest/videoPlayer.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/rest/videoPlayer.jsp
new file mode 100644
index 00000000..ccd18a93
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/rest/videoPlayer.jsp
@@ -0,0 +1,142 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="../include.jsp" %>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" href="../<spring:theme code="styleSheet"/>" type="text/css">
+ <link rel="shortcut icon" href="../<spring:theme code="faviconImage"/>">
+
+ <c:url value="/rest/stream.view" var="streamUrl">
+ <c:param name="c" value="${model.c}"/>
+ <c:param name="v" value="${model.v}"/>
+ <c:param name="id" value="${model.id}"/>
+ </c:url>
+
+ <script type="text/javascript" src="<c:url value="/script/swfobject.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+
+ var player;
+ var position;
+ var maxBitRate = ${model.maxBitRate};
+ var timeOffset = ${model.timeOffset};
+
+ function init() {
+ var flashvars = {
+ id:"player1",
+ skin:"<c:url value="/flash/whotube.zip"/>",
+ screencolor:"000000",
+ autostart:false,
+ bufferlength:4,
+ backcolor:"<spring:theme code="backgroundColor"/>",
+ frontcolor:"<spring:theme code="textColor"/>",
+ provider:"video"
+ };
+ var params = {
+ allowfullscreen:"true",
+ allowscriptaccess:"always"
+ };
+ var attributes = {
+ id:"player1",
+ name:"player1"
+ };
+
+ swfobject.embedSWF("<c:url value="/flash/jw-player-5.6.swf"/>", "placeholder1", "360", "240", "9.0.0", false, flashvars, params, attributes);
+ }
+
+ function playerReady(thePlayer) {
+ player = $("player1");
+ player.addModelListener("TIME", "timeListener");
+
+ <c:if test="${model.autoplay}">
+ play();
+ </c:if>
+ }
+
+ function play() {
+ var list = new Array();
+ list[0] = {
+ file:"${streamUrl}&maxBitRate=" + maxBitRate + "&timeOffset=" + timeOffset + "&p=${model.p}" + "&u=${model.u}",
+ duration:${model.duration} - timeOffset,
+ provider:"video"
+ };
+ player.sendEvent("LOAD", list);
+ player.sendEvent("PLAY");
+ }
+
+ function timeListener(obj) {
+ var newPosition = Math.round(obj.position);
+ if (newPosition != position) {
+ position = newPosition;
+ updatePosition();
+ }
+ }
+
+ function updatePosition() {
+ var pos = parseInt(timeOffset) + parseInt(position);
+
+ var minutes = Math.round(pos / 60);
+ var seconds = pos % 60;
+
+ var result = minutes + ":";
+ if (seconds < 10) {
+ result += "0";
+ }
+ result += seconds;
+ $("position").innerHTML = result;
+ }
+
+ function changeTimeOffset() {
+ timeOffset = $("timeOffset").getValue();
+ play();
+ }
+
+ function changeBitRate() {
+ maxBitRate = $("maxBitRate").getValue();
+ timeOffset = parseInt(timeOffset) + parseInt(position);
+ play();
+ }
+
+ </script>
+</head>
+
+<body class="mainframe bgcolor1" onload="init();">
+<h1>${model.video.title}</h1>
+
+<div id="wrapper" style="padding-top:1em">
+ <div id="placeholder1"><span class="warning"><a href="http://www.adobe.com/go/getflashplayer"><fmt:message key="playlist.getflash"/></a></span></div>
+</div>
+
+<div style="padding-top:1.3em;padding-bottom:0.7em;font-size:16px">
+
+ <span id="position" style="padding-right:0.5em">0:00</span>
+ <select id="timeOffset" onchange="changeTimeOffset();" style="padding-left:0.25em;padding-right:0.25em;margin-right:0.5em;font-size:16px">
+ <c:forEach items="${model.skipOffsets}" var="skipOffset">
+ <c:choose>
+ <c:when test="${skipOffset.value eq model.timeOffset}">
+ <option selected="selected" value="${skipOffset.value}">${skipOffset.key}</option>
+ </c:when>
+ <c:otherwise>
+ <option value="${skipOffset.value}">${skipOffset.key}</option>
+ </c:otherwise>
+ </c:choose>
+ </c:forEach>
+ </select>
+
+ <select id="maxBitRate" onchange="changeBitRate();" style="padding-left:0.25em;padding-right:0.25em;margin-right:0.5em;font-size:16px">
+ <c:forEach items="${model.bitRates}" var="bitRate">
+ <c:choose>
+ <c:when test="${bitRate eq model.maxBitRate}">
+ <option selected="selected" value="${bitRate}">${bitRate} Kbps</option>
+ </c:when>
+ <c:otherwise>
+ <option value="${bitRate}">${bitRate} Kbps</option>
+ </c:otherwise>
+ </c:choose>
+ </c:forEach>
+ </select>
+</div>
+
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/right.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/right.jsp
new file mode 100644
index 00000000..ada7385f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/right.jsp
@@ -0,0 +1,191 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/util.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/chatService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/nowPlayingService.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/fancyzoom/FancyZoom.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/fancyzoom/FancyZoomHTML.js"/>"></script>
+</head>
+<body class="bgcolor1 rightframe" style="padding-top:2em" onload="init()">
+
+<script type="text/javascript">
+ function init() {
+ setupZoom('<c:url value="/"/>');
+ dwr.engine.setErrorHandler(null);
+ <c:if test="${model.showChat}">
+ chatService.addMessage(null);
+ </c:if>
+ }
+</script>
+
+<div id="scanningStatus" style="display: none;" class="warning">
+ <img src="<spring:theme code="scanningImage"/>" title="" alt=""> <fmt:message key="main.scanning"/> <span id="scanCount"></span>
+</div>
+
+<c:if test="${model.showNowPlaying}">
+
+ <!-- This script uses AJAX to periodically retrieve what all users are playing. -->
+ <script type="text/javascript" language="javascript">
+
+ startGetNowPlayingTimer();
+
+ function startGetNowPlayingTimer() {
+ nowPlayingService.getNowPlaying(getNowPlayingCallback);
+ setTimeout("startGetNowPlayingTimer()", 10000);
+ }
+
+ function getNowPlayingCallback(nowPlaying) {
+ var html = nowPlaying.length == 0 ? "" : "<h2><fmt:message key="main.nowplaying"/></h2><table>";
+ for (var i = 0; i < nowPlaying.length; i++) {
+ html += "<tr><td colspan='2' class='detail' style='padding-top:1em;white-space:nowrap'>";
+
+ if (nowPlaying[i].avatarUrl != null) {
+ html += "<img src='" + nowPlaying[i].avatarUrl + "' style='padding-right:5pt'>";
+ }
+ html += "<b>" + nowPlaying[i].username + "</b></td></tr>"
+
+ html += "<tr><td class='detail' style='padding-right:1em'>" +
+ "<a title='" + nowPlaying[i].tooltip + "' target='main' href='" + nowPlaying[i].albumUrl + "'>";
+
+ if (nowPlaying[i].artist != null) {
+ html += "<em>" + nowPlaying[i].artist + "</em><br/>";
+ }
+
+ html += nowPlaying[i].title + "</a><br/>" +
+ "<span class='forward'><a href='" + nowPlaying[i].lyricsUrl + "' onclick=\"return popupSize(this, 'lyrics', 430, 550)\">" +
+ "<fmt:message key="main.lyrics"/>" + "</a></span></td><td style='padding-top:1em'>";
+
+ if (nowPlaying[i].coverArtUrl != null) {
+ html += "<a title='" + nowPlaying[i].tooltip + "' rel='zoom' href='" + nowPlaying[i].coverArtZoomUrl + "'>" +
+ "<img src='" + nowPlaying[i].coverArtUrl + "' width='48' height='48'></a>";
+ }
+ html += "</td></tr>";
+
+ var minutesAgo = nowPlaying[i].minutesAgo;
+ if (minutesAgo > 4) {
+ html += "<tr><td class='detail' colspan='2'>" + minutesAgo + " <fmt:message key="main.minutesago"/></td></tr>";
+ }
+ }
+ html += "</table>";
+ $('nowPlaying').innerHTML = html;
+ prepZooms();
+ }
+ </script>
+
+ <div id="nowPlaying">
+ </div>
+
+</c:if>
+
+<c:if test="${model.showChat}">
+ <script type="text/javascript">
+
+ var revision = 0;
+ startGetMessagesTimer();
+
+ function startGetMessagesTimer() {
+ chatService.getMessages(revision, getMessagesCallback);
+ setTimeout("startGetMessagesTimer()", 10000);
+ }
+
+ function addMessage() {
+ chatService.addMessage($("message").value);
+ dwr.util.setValue("message", null);
+ setTimeout("startGetMessagesTimer()", 500);
+ }
+ function clearMessages() {
+ chatService.clearMessages();
+ setTimeout("startGetMessagesTimer()", 500);
+ }
+ function getMessagesCallback(messages) {
+
+ if (messages == null) {
+ return;
+ }
+ revision = messages.revision;
+
+ // Delete all the rows except for the "pattern" row
+ dwr.util.removeAllRows("chatlog", { filter:function(div) {
+ return (div.id != "pattern");
+ }});
+
+ // Create a new set cloned from the pattern row
+ for (var i = 0; i < messages.messages.length; i++) {
+ var message = messages.messages[i];
+ var id = i + 1;
+ dwr.util.cloneNode("pattern", { idSuffix:id });
+ dwr.util.setValue("user" + id, message.username);
+ dwr.util.setValue("date" + id, " [" + formatDate(message.date) + "]");
+ dwr.util.setValue("content" + id, message.content);
+ $("pattern" + id).show();
+ }
+
+ var clearDiv = $("clearDiv");
+ if (clearDiv) {
+ if (messages.messages.length == 0) {
+ clearDiv.hide();
+ } else {
+ clearDiv.show();
+ }
+ }
+ }
+ function formatDate(date) {
+ var hours = date.getHours();
+ var minutes = date.getMinutes();
+ var result = hours < 10 ? "0" : "";
+ result += hours;
+ result += ":";
+ if (minutes < 10) {
+ result += "0";
+ }
+ result += minutes;
+ return result;
+ }
+ </script>
+
+ <script type="text/javascript">
+
+ startGetScanningStatusTimer();
+
+ function startGetScanningStatusTimer() {
+ nowPlayingService.getScanningStatus(getScanningStatusCallback);
+ }
+
+ function getScanningStatusCallback(scanInfo) {
+ dwr.util.setValue("scanCount", scanInfo.count);
+ if (scanInfo.scanning) {
+ $("scanningStatus").show();
+ setTimeout("startGetScanningStatusTimer()", 1000);
+ } else {
+ $("scanningStatus").hide();
+ setTimeout("startGetScanningStatusTimer()", 15000);
+ }
+ }
+ </script>
+
+ <h2><fmt:message key="main.chat"/></h2>
+ <div style="padding-top:0.3em;padding-bottom:0.3em">
+ <input id="message" value=" <fmt:message key="main.message"/>" style="width:90%" onclick="dwr.util.setValue('message', null);" onkeypress="dwr.util.onReturn(event, addMessage)"/>
+ </div>
+
+ <table>
+ <tbody id="chatlog">
+ <tr id="pattern" style="display:none;margin:0;padding:0 0 0.15em 0;border:0"><td>
+ <span id="user" class="detail" style="font-weight:bold"></span>&nbsp;<span id="date" class="detail"></span> <span id="content"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <c:if test="${model.user.adminRole}">
+ <div id="clearDiv" style="display:none;" class="forward"><a href="#" onclick="clearMessages(); return false;"> <fmt:message key="main.clearchat"/></a></div>
+ </c:if>
+</c:if>
+
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/search.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/search.jsp
new file mode 100644
index 00000000..a01f7afe
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/search.jsp
@@ -0,0 +1,150 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<%--@elvariable id="command" type="net.sourceforge.subsonic.command.SearchCommand"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value='/script/scripts.js'/>"></script>
+ <script type="text/javascript" src="<c:url value='/script/prototype.js'/>"></script>
+ <script type="text/javascript" src="<c:url value='/dwr/util.js'/>"></script>
+
+ <script type="text/javascript">
+ function more(rowSelector, moreId) {
+ var rows = $$(rowSelector);
+ for (var i = 0; i < rows.length; i++) {
+ rows[i].show();
+ }
+ $(moreId).hide();
+ }
+ </script>
+
+</head>
+<body class="mainframe bgcolor1">
+
+<h1>
+ <img src="<spring:theme code="searchImage"/>" alt=""/>
+ <fmt:message key="search.title"/>
+</h1>
+
+<form:form commandName="command" method="post" action="search.view" name="searchForm">
+ <table>
+ <tr>
+ <td><fmt:message key="search.query"/></td>
+ <td style="padding-left:0.25em"><form:input path="query" size="35"/></td>
+ <td style="padding-left:0.25em"><input type="submit" onclick="search(0)" value="<fmt:message key="search.search"/>"/></td>
+ </tr>
+ </table>
+
+</form:form>
+
+<c:if test="${command.indexBeingCreated}">
+ <p class="warning"><fmt:message key="search.index"/></p>
+</c:if>
+
+<c:if test="${not command.indexBeingCreated and empty command.artists and empty command.albums and empty command.songs}">
+ <p class="warning"><fmt:message key="search.hits.none"/></p>
+</c:if>
+
+<c:if test="${not empty command.artists}">
+ <h2><fmt:message key="search.hits.artists"/></h2>
+ <table style="border-collapse:collapse">
+ <c:forEach items="${command.artists}" var="match" varStatus="loopStatus">
+
+ <sub:url value="/main.view" var="mainUrl">
+ <sub:param name="path" value="${match.path}"/>
+ </sub:url>
+
+ <tr class="artistRow" ${loopStatus.count > 5 ? "style='display:none'" : ""}>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${match.id}"/>
+ <c:param name="playEnabled" value="${command.user.streamRole and not command.partyModeEnabled}"/>
+ <c:param name="addEnabled" value="${command.user.streamRole and (not command.partyModeEnabled or not match.directory)}"/>
+ <c:param name="downloadEnabled" value="${command.user.downloadRole and not command.partyModeEnabled}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-left:0.25em;padding-right:1.25em">
+ <a href="${mainUrl}">${match.name}</a>
+ </td>
+ </tr>
+
+ </c:forEach>
+ </table>
+ <c:if test="${fn:length(command.artists) gt 5}">
+ <div id="moreArtists" class="forward"><a href="javascript:noop()" onclick="more('tr.artistRow', 'moreArtists')"><fmt:message key="search.hits.more"/></a></div>
+ </c:if>
+</c:if>
+
+<c:if test="${not empty command.albums}">
+ <h2><fmt:message key="search.hits.albums"/></h2>
+ <table style="border-collapse:collapse">
+ <c:forEach items="${command.albums}" var="match" varStatus="loopStatus">
+
+ <sub:url value="/main.view" var="mainUrl">
+ <sub:param name="path" value="${match.path}"/>
+ </sub:url>
+
+ <tr class="albumRow" ${loopStatus.count > 5 ? "style='display:none'" : ""}>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${match.id}"/>
+ <c:param name="playEnabled" value="${command.user.streamRole and not command.partyModeEnabled}"/>
+ <c:param name="addEnabled" value="${command.user.streamRole and (not command.partyModeEnabled or not match.directory)}"/>
+ <c:param name="downloadEnabled" value="${command.user.downloadRole and not command.partyModeEnabled}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-left:0.25em;padding-right:1.25em">
+ <a href="${mainUrl}">${match.albumName}</a>
+ </td>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-right:0.25em">
+ <span class="detail">${match.artist}</span>
+ </td>
+ </tr>
+
+ </c:forEach>
+ </table>
+ <c:if test="${fn:length(command.albums) gt 5}">
+ <div id="moreAlbums" class="forward"><a href="javascript:noop()" onclick="more('tr.albumRow', 'moreAlbums')"><fmt:message key="search.hits.more"/></a></div>
+ </c:if>
+</c:if>
+
+
+<c:if test="${not empty command.songs}">
+ <h2><fmt:message key="search.hits.songs"/></h2>
+ <table style="border-collapse:collapse">
+ <c:forEach items="${command.songs}" var="match" varStatus="loopStatus">
+
+ <sub:url value="/main.view" var="mainUrl">
+ <sub:param name="path" value="${match.parentPath}"/>
+ </sub:url>
+
+ <tr class="songRow" ${loopStatus.count > 15 ? "style='display:none'" : ""}>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${match.id}"/>
+ <c:param name="playEnabled" value="${command.user.streamRole and not command.partyModeEnabled}"/>
+ <c:param name="addEnabled" value="${command.user.streamRole and (not command.partyModeEnabled or not match.directory)}"/>
+ <c:param name="downloadEnabled" value="${command.user.downloadRole and not command.partyModeEnabled}"/>
+ <c:param name="video" value="${match.video and command.player.web}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-left:0.25em;padding-right:1.25em">
+ ${match.title}
+ </td>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-right:1.25em">
+ <a href="${mainUrl}"><span class="detail">${match.albumName}</span></a>
+ </td>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-right:0.25em">
+ <span class="detail">${match.artist}</span>
+ </td>
+ </tr>
+
+ </c:forEach>
+ </table>
+<c:if test="${fn:length(command.songs) gt 15}">
+ <div id="moreSongs" class="forward"><a href="javascript:noop()" onclick="more('tr.songRow', 'moreSongs')"><fmt:message key="search.hits.more"/></a></div>
+</c:if>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/settingsHeader.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/settingsHeader.jsp
new file mode 100644
index 00000000..6f7f240c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/settingsHeader.jsp
@@ -0,0 +1,32 @@
+
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%@ include file="include.jsp" %>
+
+<c:set var="categories" value="${param.restricted ? 'personal password player share' : 'musicFolder general advanced personal user player share network transcoding internetRadio podcast'}"/>
+<h1>
+ <img src="<spring:theme code="settingsImage"/>" alt=""/>
+ <fmt:message key="settingsheader.title"/>
+</h1>
+
+<h2>
+<c:forTokens items="${categories}" delims=" " var="cat" varStatus="loopStatus">
+ <c:choose>
+ <c:when test="${loopStatus.count > 1 and (loopStatus.count - 1) % 6 != 0}">&nbsp;|&nbsp;</c:when>
+ <c:otherwise></h2><h2></c:otherwise>
+ </c:choose>
+
+ <c:url var="url" value="${cat}Settings.view?"/>
+
+ <c:choose>
+ <c:when test="${param.cat eq cat}">
+ <span class="headerSelected"><fmt:message key="settingsheader.${cat}"/></span>
+ </c:when>
+ <c:otherwise>
+ <a href="${url}"><fmt:message key="settingsheader.${cat}"/></a>
+ </c:otherwise>
+ </c:choose>
+
+</c:forTokens>
+</h2>
+
+<p></p>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/shareSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/shareSettings.jsp
new file mode 100644
index 00000000..448f4741
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/shareSettings.jsp
@@ -0,0 +1,72 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+<%--@elvariable id="model" type="Map"--%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="share"/>
+ <c:param name="restricted" value="${not model.user.adminRole}"/>
+</c:import>
+
+<form method="post" action="shareSettings.view">
+
+ <table class="indent" style="border-collapse:collapse;white-space:nowrap">
+ <tr>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.name"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.owner"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.description"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.expires"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.lastvisited"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.visits"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.files"/></th>
+ <th style="padding-left:1em"><fmt:message key="sharesettings.expirein"/></th>
+ <th style="padding-left:1em"><fmt:message key="common.delete"/></th>
+ </tr>
+
+ <c:forEach items="${model.shareInfos}" var="shareInfo" varStatus="loopStatus">
+ <c:set var="share" value="${shareInfo.share}"/>
+ <c:choose>
+ <c:when test="${loopStatus.count % 2 == 1}">
+ <c:set var="class" value="class='bgcolor2'"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="class" value=""/>
+ </c:otherwise>
+ </c:choose>
+
+ <sub:url value="main.view" var="albumUrl">
+ <sub:param name="path" value="${shareInfo.dir.path}"/>
+ </sub:url>
+
+ <tr>
+ <td ${class} style="padding-left:1em"><a href="${model.shareBaseUrl}${share.name}" target="_blank">${share.name}</a></td>
+ <td ${class} style="padding-left:1em">${share.username}</td>
+ <td ${class} style="padding-left:1em"><input type="text" name="description[${share.id}]" size="40" value="${share.description}"/></td>
+ <td ${class} style="padding-left:1em"><fmt:formatDate value="${share.expires}" type="date" dateStyle="medium"/></td>
+ <td ${class} style="padding-left:1em"><fmt:formatDate value="${share.lastVisited}" type="date" dateStyle="medium"/></td>
+ <td ${class} style="padding-left:1em; text-align:right">${share.visitCount}</td>
+ <td ${class} style="padding-left:1em"><a href="${albumUrl}" title="${shareInfo.dir.name}"><str:truncateNicely upper="30">${fn:escapeXml(shareInfo.dir.name)}</str:truncateNicely></a></td>
+ <td ${class} style="padding-left:1em">
+ <label><input type="radio" name="expireIn[${share.id}]" value="7"><fmt:message key="sharesettings.expirein.week"/></label>
+ <label><input type="radio" name="expireIn[${share.id}]" value="30"><fmt:message key="sharesettings.expirein.month"/></label>
+ <label><input type="radio" name="expireIn[${share.id}]" value="365"><fmt:message key="sharesettings.expirein.year"/></label>
+ <label><input type="radio" name="expireIn[${share.id}]" value="0"><fmt:message key="sharesettings.expirein.never"/></label>
+ </td>
+ <td ${class} style="padding-left:1em" align="center" style="padding-left:1em"><input type="checkbox" name="delete[${share.id}]" class="checkbox"/></td>
+ </tr>
+ </c:forEach>
+
+ <tr>
+ <td colspan="4" style="padding-top:1.5em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'">
+ </td>
+ </tr>
+
+ </table>
+</form>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/starred.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/starred.jsp
new file mode 100644
index 00000000..967fc7b7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/starred.jsp
@@ -0,0 +1,131 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <%@ include file="jquery.jsp" %>
+ <script type="text/javascript" src="<c:url value='/dwr/util.js'/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/engine.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/dwr/interface/starService.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+
+ function toggleStar(mediaFileId, imageId) {
+ if ($(imageId).attr("src").indexOf("<spring:theme code="ratingOnImage"/>") != -1) {
+ $(imageId).attr("src", "<spring:theme code="ratingOffImage"/>");
+ starService.unstar(mediaFileId);
+ }
+ else if ($(imageId).attr("src").indexOf("<spring:theme code="ratingOffImage"/>") != -1) {
+ $(imageId).attr("src", "<spring:theme code="ratingOnImage"/>");
+ starService.star(mediaFileId);
+ }
+ }
+ </script>
+</head>
+<body class="mainframe bgcolor1">
+
+<h1>
+ <fmt:message key="starred.title"/>
+</h1>
+
+<c:if test="${empty model.artists and empty model.albums and empty model.songs}">
+ <p style="padding-top: 1em"><em><fmt:message key="starred.empty"/></em></p>
+</c:if>
+
+<c:if test="${not empty model.artists}">
+ <h2><fmt:message key="search.hits.artists"/></h2>
+ <table style="border-collapse:collapse">
+ <c:forEach items="${model.artists}" var="artist" varStatus="loopStatus">
+
+ <sub:url value="/main.view" var="mainUrl">
+ <sub:param name="path" value="${artist.path}"/>
+ </sub:url>
+
+ <tr>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${artist.id}"/>
+ <c:param name="playEnabled" value="${model.user.streamRole and not model.partyModeEnabled}"/>
+ <c:param name="addEnabled" value="${model.user.streamRole and (not model.partyModeEnabled or not artist.directory)}"/>
+ <c:param name="downloadEnabled" value="${model.user.downloadRole and not model.partyModeEnabled}"/>
+ <c:param name="starEnabled" value="true"/>
+ <c:param name="starred" value="${not empty artist.starredDate}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-left:0.25em;padding-right:1.25em">
+ <a href="${mainUrl}">${artist.name}</a>
+ </td>
+ </tr>
+ </c:forEach>
+ </table>
+</c:if>
+
+<c:if test="${not empty model.albums}">
+ <h2><fmt:message key="search.hits.albums"/></h2>
+ <table style="border-collapse:collapse">
+ <c:forEach items="${model.albums}" var="album" varStatus="loopStatus">
+
+ <sub:url value="/main.view" var="mainUrl">
+ <sub:param name="path" value="${album.path}"/>
+ </sub:url>
+
+ <tr>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${album.id}"/>
+ <c:param name="playEnabled" value="${model.user.streamRole and not model.partyModeEnabled}"/>
+ <c:param name="addEnabled" value="${model.user.streamRole and (not model.partyModeEnabled or not album.directory)}"/>
+ <c:param name="downloadEnabled" value="${model.user.downloadRole and not model.partyModeEnabled}"/>
+ <c:param name="starEnabled" value="true"/>
+ <c:param name="starred" value="${not empty album.starredDate}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-left:0.25em;padding-right:1.25em">
+ <a href="${mainUrl}">${album.albumName}</a>
+ </td>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-right:0.25em">
+ <span class="detail">${album.artist}</span>
+ </td>
+ </tr>
+
+ </c:forEach>
+ </table>
+</c:if>
+
+<c:if test="${not empty model.songs}">
+ <h2><fmt:message key="search.hits.songs"/></h2>
+ <table style="border-collapse:collapse">
+ <c:forEach items="${model.songs}" var="song" varStatus="loopStatus">
+
+ <sub:url value="/main.view" var="mainUrl">
+ <sub:param name="path" value="${song.parentPath}"/>
+ </sub:url>
+
+ <tr>
+ <c:import url="playAddDownload.jsp">
+ <c:param name="id" value="${song.id}"/>
+ <c:param name="playEnabled" value="${model.user.streamRole and not model.partyModeEnabled}"/>
+ <c:param name="addEnabled" value="${model.user.streamRole and (not model.partyModeEnabled or not song.directory)}"/>
+ <c:param name="downloadEnabled" value="${model.user.downloadRole and not model.partyModeEnabled}"/>
+ <c:param name="starEnabled" value="true"/>
+ <c:param name="starred" value="${not empty song.starredDate}"/>
+ <c:param name="video" value="${song.video and model.player.web}"/>
+ <c:param name="asTable" value="true"/>
+ </c:import>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-left:0.25em;padding-right:1.25em">
+ ${song.title}
+ </td>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-right:1.25em">
+ <a href="${mainUrl}"><span class="detail">${song.albumName}</span></a>
+ </td>
+
+ <td ${loopStatus.count % 2 == 1 ? "class='bgcolor2'" : ""} style="padding-right:0.25em">
+ <span class="detail">${song.artist}</span>
+ </td>
+ </tr>
+
+ </c:forEach>
+ </table>
+</c:if>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/status.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/status.jsp
new file mode 100644
index 00000000..2861022d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/status.jsp
@@ -0,0 +1,93 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <meta http-equiv="CACHE-CONTROL" content="NO-CACHE">
+ <meta http-equiv="REFRESH" content="20;URL=status.view">
+</head>
+<body class="mainframe bgcolor1">
+
+<h1>
+ <img src="<spring:theme code="statusImage"/>" alt="">
+ <fmt:message key="status.title"/>
+</h1>
+
+<table width="100%" class="ruleTable indent">
+ <tr>
+ <th class="ruleTableHeader"><fmt:message key="status.type"/></th>
+ <th class="ruleTableHeader"><fmt:message key="status.player"/></th>
+ <th class="ruleTableHeader"><fmt:message key="status.user"/></th>
+ <th class="ruleTableHeader"><fmt:message key="status.current"/></th>
+ <th class="ruleTableHeader"><fmt:message key="status.transmitted"/></th>
+ <th class="ruleTableHeader"><fmt:message key="status.bitrate"/></th>
+ </tr>
+
+ <c:forEach items="${model.transferStatuses}" var="status">
+
+ <c:choose>
+ <c:when test="${empty status.playerType}">
+ <fmt:message key="common.unknown" var="type"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="type" value="(${status.playerType})"/>
+ </c:otherwise>
+ </c:choose>
+
+ <c:choose>
+ <c:when test="${status.stream}">
+ <fmt:message key="status.stream" var="transferType"/>
+ </c:when>
+ <c:when test="${status.download}">
+ <fmt:message key="status.download" var="transferType"/>
+ </c:when>
+ <c:when test="${status.upload}">
+ <fmt:message key="status.upload" var="transferType"/>
+ </c:when>
+ </c:choose>
+
+ <c:choose>
+ <c:when test="${empty status.username}">
+ <fmt:message key="common.unknown" var="user"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="user" value="${status.username}"/>
+ </c:otherwise>
+ </c:choose>
+
+ <c:choose>
+ <c:when test="${empty status.path}">
+ <fmt:message key="common.unknown" var="current"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="current" value="${status.path}"/>
+ </c:otherwise>
+ </c:choose>
+
+ <sub:url value="/statusChart.view" var="chartUrl">
+ <c:if test="${status.stream}">
+ <sub:param name="type" value="stream"/>
+ </c:if>
+ <c:if test="${status.download}">
+ <sub:param name="type" value="download"/>
+ </c:if>
+ <c:if test="${status.upload}">
+ <sub:param name="type" value="upload"/>
+ </c:if>
+ <sub:param name="index" value="${status.index}"/>
+ </sub:url>
+
+ <tr>
+ <td class="ruleTableCell">${transferType}</td>
+ <td class="ruleTableCell">${status.player}<br>${type}</td>
+ <td class="ruleTableCell">${user}</td>
+ <td class="ruleTableCell">${current}</td>
+ <td class="ruleTableCell">${status.bytes}</td>
+ <td class="ruleTableCell" width="${model.chartWidth}"><img width="${model.chartWidth}" height="${model.chartHeight}" src="${chartUrl}" alt=""></td>
+ </tr>
+ </c:forEach>
+</table>
+
+<div class="forward"><a href="status.view?"><fmt:message key="common.refresh"/></a></div>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/test.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/test.jsp
new file mode 100644
index 00000000..64f7c792
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/test.jsp
@@ -0,0 +1,20 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+</head>
+<body>
+
+<div id="tFfBc" style="opacity:0;">
+ <img src="/coverArt.view?size=200" alt="">
+</div>
+
+<ul>
+ <li><a href="#" onclick="new Effect.Opacity('tFfBc', { from: 1, to: 0 }); return false;">Hide this box</a></li>
+ <li><a href="#" onclick="new Effect.Opacity('tFfBc', { from: 0, to: 1 }); return false;">Show this box</a></li>
+</ul>
+
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/top.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/top.jsp
new file mode 100644
index 00000000..18acb053
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/top.jsp
@@ -0,0 +1,98 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+
+<body class="bgcolor2 topframe" style="margin:0.4em 1em 0.4em 1em">
+
+<fmt:message key="top.home" var="home"/>
+<fmt:message key="top.now_playing" var="nowPlaying"/>
+<fmt:message key="top.starred" var="starred"/>
+<fmt:message key="top.settings" var="settings"/>
+<fmt:message key="top.status" var="status"/>
+<fmt:message key="top.podcast" var="podcast"/>
+<fmt:message key="top.more" var="more"/>
+<fmt:message key="top.help" var="help"/>
+<fmt:message key="top.search" var="search"/>
+
+<table style="margin:0"><tr valign="middle">
+ <td class="logo" style="padding-right:2em"><a href="help.view?" target="main"><img src="<spring:theme code="logoImage"/>" title="${help}" alt=""></a></td>
+
+ <c:if test="${not model.musicFoldersExist}">
+ <td style="padding-right:2em">
+ <p class="warning"><fmt:message key="top.missing"/></p>
+ </td>
+ </c:if>
+
+ <td>
+ <table><tr align="center">
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="home.view?" target="main"><img src="<spring:theme code="homeImage"/>" title="${home}" alt="${home}"></a><br>
+ <a href="home.view?" target="main">${home}</a>
+ </td>
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="nowPlaying.view?" target="main"><img src="<spring:theme code="nowPlayingImage"/>" title="${nowPlaying}" alt="${nowPlaying}"></a><br>
+ <a href="nowPlaying.view?" target="main">${nowPlaying}</a>
+ </td>
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="starred.view?" target="main"><img src="<spring:theme code="starredImage"/>" title="${starred}" alt="${starred}"></a><br>
+ <a href="starred.view?" target="main">${starred}</a>
+ </td>
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="podcastReceiver.view?" target="main"><img src="<spring:theme code="podcastLargeImage"/>" title="${podcast}" alt="${podcast}"></a><br>
+ <a href="podcastReceiver.view?" target="main">${podcast}</a>
+ </td>
+ <c:if test="${model.user.settingsRole}">
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="settings.view?" target="main"><img src="<spring:theme code="settingsImage"/>" title="${settings}" alt="${settings}"></a><br>
+ <a href="settings.view?" target="main">${settings}</a>
+ </td>
+ </c:if>
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="status.view?" target="main"><img src="<spring:theme code="statusImage"/>" title="${status}" alt="${status}"></a><br>
+ <a href="status.view?" target="main">${status}</a>
+ </td>
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="more.view?" target="main"><img src="<spring:theme code="moreImage"/>" title="${more}" alt="${more}"></a><br>
+ <a href="more.view?" target="main">${more}</a>
+ </td>
+ <td style="min-width:4em;padding-right:1.5em">
+ <a href="help.view?" target="main"><img src="<spring:theme code="helpImage"/>" title="${help}" alt="${help}"></a><br>
+ <a href="help.view?" target="main">${help}</a>
+ </td>
+
+ <td style="padding-left:1em">
+ <form method="post" action="search.view" target="main" name="searchForm">
+ <table><tr>
+ <td><input type="text" name="query" id="query" size="28" value="${search}" onclick="select();"></td>
+ <td><a href="javascript:document.searchForm.submit()"><img src="<spring:theme code="searchImage"/>" alt="${search}" title="${search}"></a></td>
+ </tr></table>
+ </form>
+ </td>
+
+ <td style="padding-left:15pt;text-align:center;">
+ <p class="detail" style="line-height:1.5">
+ <a href="j_acegi_logout" target="_top"><fmt:message key="top.logout"><fmt:param value="${model.user.username}"/></fmt:message></a>
+ <c:if test="${not model.licensed}">
+ <br>
+ <a href="donate.view" target="main"><img src="<spring:theme code="donateSmallImage"/>" alt=""></a>
+ <a href="donate.view" target="main"><fmt:message key="donate.title"/></a>
+ </c:if>
+ </p>
+ </td>
+
+ <c:if test="${model.newVersionAvailable}">
+ <td style="padding-left:15pt">
+ <p class="warning">
+ <fmt:message key="top.upgrade"><fmt:param value="${model.brand}"/><fmt:param value="${model.latestVersion}"/></fmt:message>
+ </p>
+ </td>
+ </c:if>
+ </tr></table>
+ </td>
+
+</tr></table>
+
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/transcodingSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/transcodingSettings.jsp
new file mode 100644
index 00000000..a641cb53
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/transcodingSettings.jsp
@@ -0,0 +1,70 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head>
+<body class="mainframe bgcolor1">
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="transcoding"/>
+</c:import>
+
+<form method="post" action="transcodingSettings.view">
+<table class="indent">
+ <tr>
+ <th><fmt:message key="transcodingsettings.name"/></th>
+ <th><fmt:message key="transcodingsettings.sourceformat"/></th>
+ <th><fmt:message key="transcodingsettings.targetformat"/></th>
+ <th><fmt:message key="transcodingsettings.step1"/></th>
+ <th><fmt:message key="transcodingsettings.step2"/></th>
+ <th style="padding-left:1em"><fmt:message key="common.delete"/></th>
+ </tr>
+
+ <c:forEach items="${model.transcodings}" var="transcoding">
+ <tr>
+ <td><input style="font-family:monospace" type="text" name="name[${transcoding.id}]" size="10" value="${transcoding.name}"/></td>
+ <td><input style="font-family:monospace" type="text" name="sourceFormats[${transcoding.id}]" size="36" value="${transcoding.sourceFormats}"/></td>
+ <td><input style="font-family:monospace" type="text" name="targetFormat[${transcoding.id}]" size="10" value="${transcoding.targetFormat}"/></td>
+ <td><input style="font-family:monospace" type="text" name="step1[${transcoding.id}]" size="60" value="${transcoding.step1}"/></td>
+ <td><input style="font-family:monospace" type="text" name="step2[${transcoding.id}]" size="22" value="${transcoding.step2}"/></td>
+ <td align="center" style="padding-left:1em"><input type="checkbox" name="delete[${transcoding.id}]" class="checkbox"/></td>
+ </tr>
+ </c:forEach>
+
+ <tr>
+ <th colspan="6" align="left" style="padding-top:1em"><fmt:message key="transcodingsettings.add"/></th>
+ </tr>
+
+ <tr>
+ <td><input style="font-family:monospace" type="text" name="name" size="10" value="${model.newTranscoding.name}"/></td>
+ <td><input style="font-family:monospace" type="text" name="sourceFormats" size="36" value="${model.newTranscoding.sourceFormats}"/></td>
+ <td><input style="font-family:monospace" type="text" name="targetFormat" size="10" value="${model.newTranscoding.targetFormat}"/></td>
+ <td><input style="font-family:monospace" type="text" name="step1" size="60" value="${model.newTranscoding.step1}"/></td>
+ <td><input style="font-family:monospace" type="text" name="step2" size="22" value="${model.newTranscoding.step2}"/></td>
+ <td/>
+ </tr>
+
+ <tr>
+ <td colspan="6" style="padding-top:0.1em">
+ <input type="checkbox" id="defaultActive" name="defaultActive" class="checkbox" checked/>
+ <label for="defaultActive"><fmt:message key="transcodingsettings.defaultactive"/></label>
+ </td>
+ </tr>
+</table>
+
+ <p style="padding-top:0.75em">
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'" style="margin-right:1.3em">
+ <a href="http://www.subsonic.org/pages/transcoding.jsp" target="_blank"><fmt:message key="transcodingsettings.recommended"/></a>
+ </p>
+
+</form>
+
+<c:if test="${not empty model.error}">
+ <p class="warning"><fmt:message key="${model.error}"/></p>
+</c:if>
+
+<div style="width:60%">
+ <fmt:message key="transcodingsettings.info"><fmt:param value="${model.transcodeDirectory}"/><fmt:param value="${model.brand}"/></fmt:message>
+</div>
+</body></html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/upload.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/upload.jsp
new file mode 100644
index 00000000..eb79d17c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/upload.jsp
@@ -0,0 +1,29 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+</head><body>
+
+<h1><fmt:message key="upload.title"/></h1>
+
+<c:forEach items="${model.uploadedFiles}" var="file">
+ <p><fmt:message key="upload.success"><fmt:param value="${file.path}"/></fmt:message></p>
+</c:forEach>
+
+<c:forEach items="${model.unzippedFiles}" var="file">
+ <fmt:message key="upload.unzipped"><fmt:param value="${file.path}"/></fmt:message><br/>
+</c:forEach>
+
+<c:choose>
+ <c:when test="${not empty model.exception}">
+ <p><fmt:message key="upload.failed"><fmt:param value="${model.exception.message}"/></fmt:message></p>
+ </c:when>
+ <c:when test="${empty model.uploadedFiles}">
+ <p><fmt:message key="upload.empty"/></p>
+ </c:when>
+</c:choose>
+
+<div class="back"><a href="more.view?"><fmt:message key="common.back"/></a></div>
+</body></html>
+
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/userSettings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/userSettings.jsp
new file mode 100644
index 00000000..a26c9113
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/userSettings.jsp
@@ -0,0 +1,201 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<html><head>
+ <%@ include file="head.jsp" %>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+</head>
+
+<body class="mainframe bgcolor1" onload="enablePasswordChangeFields();">
+<script type="text/javascript" src="<c:url value="/script/wz_tooltip.js"/>"></script>
+<script type="text/javascript" src="<c:url value="/script/tip_balloon.js"/>"></script>
+
+<c:import url="settingsHeader.jsp">
+ <c:param name="cat" value="user"/>
+</c:import>
+
+<script type="text/javascript" language="javascript">
+ function enablePasswordChangeFields() {
+ var changePasswordCheckbox = $("passwordChange");
+ var ldapCheckbox = $("ldapAuthenticated");
+ var passwordChangeTable = $("passwordChangeTable");
+ var passwordChangeCheckboxTable = $("passwordChangeCheckboxTable");
+
+ if (changePasswordCheckbox && changePasswordCheckbox.checked && (ldapCheckbox == null || !ldapCheckbox.checked)) {
+ passwordChangeTable.show();
+ } else {
+ passwordChangeTable.hide();
+ }
+
+ if (changePasswordCheckbox) {
+ if (ldapCheckbox && ldapCheckbox.checked) {
+ passwordChangeCheckboxTable.hide();
+ } else {
+ passwordChangeCheckboxTable.show();
+ }
+ }
+ }
+</script>
+
+<table class="indent">
+ <tr>
+ <td><b><fmt:message key="usersettings.title"/></b></td>
+ <td>
+ <select name="username" onchange="location='userSettings.view?userIndex=' + (selectedIndex - 1);">
+ <option value="">-- <fmt:message key="usersettings.newuser"/> --</option>
+ <c:forEach items="${command.users}" var="user">
+ <option ${user.username eq command.username ? "selected" : ""}
+ value="${user.username}">${user.username}</option>
+ </c:forEach>
+ </select>
+ </td>
+ </tr>
+</table>
+
+<p/>
+
+<form:form method="post" action="userSettings.view" commandName="command">
+ <c:if test="${not command.admin}">
+ <table>
+ <tr>
+ <td><form:checkbox path="adminRole" id="admin" cssClass="checkbox"/></td>
+ <td><label for="admin"><fmt:message key="usersettings.admin"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="settingsRole" id="settings" cssClass="checkbox"/></td>
+ <td><label for="settings"><fmt:message key="usersettings.settings"/></label></td>
+ </tr>
+ <tr>
+ <td style="padding-top:1em"><form:checkbox path="streamRole" id="stream" cssClass="checkbox"/></td>
+ <td style="padding-top:1em"><label for="stream"><fmt:message key="usersettings.stream"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="jukeboxRole" id="jukebox" cssClass="checkbox"/></td>
+ <td><label for="jukebox"><fmt:message key="usersettings.jukebox"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="downloadRole" id="download" cssClass="checkbox"/></td>
+ <td><label for="download"><fmt:message key="usersettings.download"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="uploadRole" id="upload" cssClass="checkbox"/></td>
+ <td><label for="upload"><fmt:message key="usersettings.upload"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="shareRole" id="share" cssClass="checkbox"/></td>
+ <td><label for="share"><fmt:message key="usersettings.share"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="coverArtRole" id="coverArt" cssClass="checkbox"/></td>
+ <td><label for="coverArt"><fmt:message key="usersettings.coverart"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="commentRole" id="comment" cssClass="checkbox"/></td>
+ <td><label for="comment"><fmt:message key="usersettings.comment"/></label></td>
+ </tr>
+ <tr>
+ <td><form:checkbox path="podcastRole" id="podcast" cssClass="checkbox"/></td>
+ <td><label for="podcast"><fmt:message key="usersettings.podcast"/></label></td>
+ </tr>
+ </table>
+ </c:if>
+
+ <table class="indent">
+ <tr>
+ <td><fmt:message key="playersettings.maxbitrate"/></td>
+ <td>
+ <form:select path="transcodeSchemeName" cssStyle="width:8em">
+ <c:forEach items="${command.transcodeSchemeHolders}" var="transcodeSchemeHolder">
+ <form:option value="${transcodeSchemeHolder.name}" label="${transcodeSchemeHolder.description}"/>
+ </c:forEach>
+ </form:select>
+ </td>
+ <td><c:import url="helpToolTip.jsp"><c:param name="topic" value="transcode"/></c:import></td>
+ <c:if test="${not command.transcodingSupported}">
+ <td class="warning"><fmt:message key="playersettings.nolame"/></td>
+ </c:if>
+ </tr>
+ </table>
+
+ <c:if test="${not command.new and not command.admin}">
+ <table class="indent">
+ <tr>
+ <td><form:checkbox path="delete" id="delete" cssClass="checkbox"/></td>
+ <td><label for="delete"><fmt:message key="usersettings.delete"/></label></td>
+ </tr>
+ </table>
+ </c:if>
+
+ <c:if test="${command.ldapEnabled and not command.admin}">
+ <table>
+ <tr>
+ <td><form:checkbox path="ldapAuthenticated" id="ldapAuthenticated" cssClass="checkbox" onclick="javascript:enablePasswordChangeFields()"/></td>
+ <td><label for="ldapAuthenticated"><fmt:message key="usersettings.ldap"/></label></td>
+ <td><c:import url="helpToolTip.jsp"><c:param name="topic" value="ldap"/></c:import></td>
+ </tr>
+ </table>
+ </c:if>
+
+ <c:choose>
+ <c:when test="${command.new}">
+
+ <table class="indent">
+ <tr>
+ <td><fmt:message key="usersettings.username"/></td>
+ <td><form:input path="username"/></td>
+ <td class="warning"><form:errors path="username"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="usersettings.email"/></td>
+ <td><form:input path="email"/></td>
+ <td class="warning"><form:errors path="email"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="usersettings.password"/></td>
+ <td><form:password path="password"/></td>
+ <td class="warning"><form:errors path="password"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="usersettings.confirmpassword"/></td>
+ <td><form:password path="confirmPassword"/></td>
+ <td/>
+ </tr>
+ </table>
+ </c:when>
+
+ <c:otherwise>
+ <table id="passwordChangeCheckboxTable">
+ <tr>
+ <td><form:checkbox path="passwordChange" id="passwordChange" onclick="enablePasswordChangeFields();" cssClass="checkbox"/></td>
+ <td><label for="passwordChange"><fmt:message key="usersettings.changepassword"/></label></td>
+ </tr>
+ </table>
+
+ <table id="passwordChangeTable" style="display:none">
+ <tr>
+ <td><fmt:message key="usersettings.newpassword"/></td>
+ <td><form:password path="password" id="path"/></td>
+ <td class="warning"><form:errors path="password"/></td>
+ </tr>
+ <tr>
+ <td><fmt:message key="usersettings.confirmpassword"/></td>
+ <td><form:password path="confirmPassword" id="confirmPassword"/></td>
+ <td/>
+ </tr>
+ </table>
+
+ <table>
+ <tr>
+ <td><fmt:message key="usersettings.email"/></td>
+ <td><form:input path="email"/></td>
+ <td class="warning"><form:errors path="email"/></td>
+ </tr>
+ </table>
+ </c:otherwise>
+ </c:choose>
+
+ <input type="submit" value="<fmt:message key="common.save"/>" style="margin-top:1.5em;margin-right:0.3em">
+ <input type="button" value="<fmt:message key="common.cancel"/>" onclick="location.href='nowPlaying.view'" style="margin-top:1.5em">
+</form:form>
+
+</body></html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/videoPlayer.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/videoPlayer.jsp
new file mode 100644
index 00000000..681a41a8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/videoPlayer.jsp
@@ -0,0 +1,190 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<html>
+<head>
+ <%@ include file="head.jsp" %>
+
+ <sub:url value="videoPlayer.view" var="baseUrl"><sub:param name="id" value="${model.video.id}"/></sub:url>
+ <sub:url value="main.view" var="backUrl"><sub:param name="id" value="${model.video.id}"/></sub:url>
+
+ <sub:url value="/stream" var="streamUrl">
+ <sub:param name="id" value="${model.video.id}"/>
+ </sub:url>
+
+ <script type="text/javascript" src="<c:url value="/script/swfobject.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/prototype.js"/>"></script>
+ <script type="text/javascript" src="<c:url value="/script/scripts.js"/>"></script>
+ <script type="text/javascript" language="javascript">
+
+ var player;
+ var position;
+ var maxBitRate = ${model.maxBitRate};
+ var timeOffset = ${model.timeOffset};
+
+ function init() {
+
+ var flashvars = {
+ id:"player1",
+ skin:"<c:url value="/flash/whotube.zip"/>",
+// plugins:"metaviewer-1",
+ screencolor:"000000",
+ controlbar:"over",
+ autostart:"false",
+ bufferlength:3,
+ backcolor:"<spring:theme code="backgroundColor"/>",
+ frontcolor:"<spring:theme code="textColor"/>",
+ provider:"video"
+ };
+ var params = {
+ allowfullscreen:"true",
+ allowscriptaccess:"always"
+ };
+ var attributes = {
+ id:"player1",
+ name:"player1"
+ };
+
+ var width = "${model.popout ? '100%' : '600'}";
+ var height = "${model.popout ? '85%' : '360'}";
+ swfobject.embedSWF("<c:url value="/flash/jw-player-5.6.swf"/>", "placeholder1", width, height, "9.0.0", false, flashvars, params, attributes);
+ }
+
+ function playerReady(thePlayer) {
+ player = $("player1");
+ player.addModelListener("TIME", "timeListener");
+
+ <c:if test="${not (model.trial and model.trialExpired)}">
+ play();
+ </c:if>
+ }
+
+ function play() {
+ var list = new Array();
+ list[0] = {
+ file:"${streamUrl}&maxBitRate=" + maxBitRate + "&timeOffset=" + timeOffset + "&player=${model.player}",
+ duration:${model.duration} - timeOffset,
+ provider:"video"
+ };
+ player.sendEvent("LOAD", list);
+ player.sendEvent("PLAY");
+ }
+
+ function timeListener(obj) {
+ var newPosition = Math.round(obj.position);
+ if (newPosition != position) {
+ position = newPosition;
+ updatePosition();
+ }
+ }
+
+ function updatePosition() {
+ var pos = getPosition();
+
+ var minutes = Math.round(pos / 60);
+ var seconds = pos % 60;
+
+ var result = minutes + ":";
+ if (seconds < 10) {
+ result += "0";
+ }
+ result += seconds;
+ $("position").innerHTML = result;
+ }
+
+ function changeTimeOffset() {
+ timeOffset = $("timeOffset").getValue();
+ play();
+ }
+
+ function changeBitRate() {
+ maxBitRate = $("maxBitRate").getValue();
+ timeOffset = getPosition();
+ play();
+ }
+
+ function popout() {
+ var url = "${baseUrl}&maxBitRate=" + maxBitRate + "&timeOffset=" + getPosition() + "&popout=true";
+ popupSize(url, "video", 600, 400);
+ window.location.href = "${backUrl}";
+ }
+
+ function popin() {
+ window.opener.location.href = "${baseUrl}&maxBitRate=" + maxBitRate + "&timeOffset=" + getPosition();
+ window.close();
+ }
+
+ function getPosition() {
+ return parseInt(timeOffset) + parseInt(position);
+ }
+
+ </script>
+</head>
+
+<body class="mainframe bgcolor1" style="padding-bottom:0.5em" onload="init();">
+<c:if test="${not model.popout}">
+ <h1>${model.video.title}</h1>
+</c:if>
+
+<c:if test="${model.trial}">
+ <fmt:formatDate value="${model.trialExpires}" dateStyle="long" var="expiryDate"/>
+
+ <p class="warning" style="padding-top:1em">
+ <c:choose>
+ <c:when test="${model.trialExpired}">
+ <fmt:message key="networksettings.trialexpired"><fmt:param>${expiryDate}</fmt:param></fmt:message>
+ </c:when>
+ <c:otherwise>
+ <fmt:message
+ key="networksettings.trialnotexpired"><fmt:param>${expiryDate}</fmt:param></fmt:message>
+ </c:otherwise>
+ </c:choose>
+ </p>
+</c:if>
+
+
+<div id="wrapper" style="padding-top:1em">
+ <div id="placeholder1"><a href="http://www.adobe.com/go/getflashplayer" target="_blank"><fmt:message key="playlist.getflash"/></a></div>
+</div>
+
+<div style="padding-top:0.7em;padding-bottom:0.7em">
+
+ <span id="position" style="padding-right:0.5em">0:00</span>
+ <select id="timeOffset" onchange="changeTimeOffset();" style="padding-left:0.25em;padding-right:0.25em;margin-right:0.5em">
+ <c:forEach items="${model.skipOffsets}" var="skipOffset">
+ <c:choose>
+ <c:when test="${skipOffset.value - skipOffset.value mod 60 eq model.timeOffset - model.timeOffset mod 60}">
+ <option selected="selected" value="${skipOffset.value}">${skipOffset.key}</option>
+ </c:when>
+ <c:otherwise>
+ <option value="${skipOffset.value}">${skipOffset.key}</option>
+ </c:otherwise>
+ </c:choose>
+ </c:forEach>
+ </select>
+
+ <select id="maxBitRate" onchange="changeBitRate();" style="padding-left:0.25em;padding-right:0.25em;margin-right:0.5em">
+ <c:forEach items="${model.bitRates}" var="bitRate">
+ <c:choose>
+ <c:when test="${bitRate eq model.maxBitRate}">
+ <option selected="selected" value="${bitRate}">${bitRate} Kbps</option>
+ </c:when>
+ <c:otherwise>
+ <option value="${bitRate}">${bitRate} Kbps</option>
+ </c:otherwise>
+ </c:choose>
+ </c:forEach>
+ </select>
+</div>
+
+<c:choose>
+ <c:when test="${model.popout}">
+ <div class="back"><a href="javascript:popin();"><fmt:message key="common.back"/></a></div>
+ </c:when>
+ <c:otherwise>
+ <div class="back" style="float:left;padding-right:2em"><a href="${backUrl}"><fmt:message key="common.back"/></a></div>
+ <div class="forward" style="float:left;"><a href="javascript:popout();"><fmt:message key="videoPlayer.popout"/></a></div>
+ </c:otherwise>
+</c:choose>
+
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/browse.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/browse.jsp
new file mode 100644
index 00000000..ac1ce096
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/browse.jsp
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<wml>
+
+ <%@ include file="head.jsp" %>
+
+ <card id="main" title="Subsonic" newcontext="false">
+
+ <p><small><b>
+
+ <sub:url value="/wap/playlist.view" var="playUrl">
+ <sub:param name="play" value="${model.parent.path}"/>
+ </sub:url>
+ <sub:url value="/wap/playlist.view" var="addUrl">
+ <sub:param name="add" value="${model.parent.path}"/>
+ </sub:url>
+ <sub:url value="/wap/download.view" var="downloadUrl">
+ <sub:param name="path" value="${model.parent.path}"/>
+ </sub:url>
+
+ <c:choose>
+ <c:when test="${fn:length(model.children) eq 1 and model.children[0].file}">
+ <a href="${playUrl}">[<fmt:message key="wap.browse.playone"/>]</a><br/>
+ <a href="${addUrl}">[<fmt:message key="wap.browse.addone"/>]</a><br/>
+ <c:if test="${model.user.downloadRole}">
+ <a href="${downloadUrl}">[<fmt:message key="wap.browse.downloadone"/>]</a><br/>
+ </c:if>
+ </c:when>
+ <c:otherwise>
+ <a href="${playUrl}">[<fmt:message key="wap.browse.playall"/>]</a><br/>
+ <a href="${addUrl}">[<fmt:message key="wap.browse.addall"/>]</a><br/>
+ <c:if test="${model.user.downloadRole}">
+ <a href="${downloadUrl}">[<fmt:message key="wap.browse.downloadall"/>]</a><br/>
+ </c:if>
+ </c:otherwise>
+ </c:choose>
+
+ <a href="<c:url value="/wap/index.view"/>">[<fmt:message key="common.home"/>]</a><br/>
+ </b></small></p>
+
+ <p><small>
+
+ <c:forEach items="${model.children}" var="child">
+ <sub:url value="/wap/browse.view" var="browseUrl">
+ <sub:param name="path" value="${child.path}"/>
+ </sub:url>
+ <a href="${browseUrl}">${fn:escapeXml(child.title)}</a><br/>
+ </c:forEach>
+
+ </small></p>
+ </card>
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/head.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/head.jsp
new file mode 100644
index 00000000..d902de1d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/head.jsp
@@ -0,0 +1,10 @@
+<%@ include file="../include.jsp" %>
+
+<head>
+ <meta http-equiv="Cache-Control" content="max-age=0" forua="true"/>
+ <meta http-equiv="Cache-Control" content="must-revalidate" forua="true"/>
+</head>
+
+<template>
+ <do type="prev" name="back" label="<fmt:message key="common.back"/>"><prev/></do>
+</template>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/index.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/index.jsp
new file mode 100644
index 00000000..2773a55e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/index.jsp
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<wml>
+
+ <%@ include file="head.jsp" %>
+
+ <card id="main" title="Subsonic" newcontext="false">
+ <p>
+ <small>
+
+ <c:choose>
+ <c:when test="${empty model.artists}">
+
+ <b>
+ <a href="<c:url value="/wap/playlist.view"/>">[<fmt:message key="wap.index.playlist"/>]</a>
+ </b>
+ <br/>
+ <b>
+ <a href="<c:url value="/wap/search.view"/>">[<fmt:message key="wap.index.search"/>]</a>
+ </b>
+ <br/>
+ <b>
+ <a href="<c:url value="/wap/settings.view"/>">[<fmt:message key="wap.index.settings"/>]</a>
+ </b>
+ <br/>
+ </small>
+ </p>
+ <p>
+ <small>
+ <c:forEach items="${model.indexes}" var="index">
+ <sub:url var="url" value="/wap/index.view">
+ <sub:param name="index" value="${index.index}"/>
+ </sub:url>
+ <a href="${url}">${index.index}</a>
+ </c:forEach>
+ </c:when>
+
+ <c:otherwise>
+ <c:forEach items="${model.artists}" var="artist">
+ <c:forEach items="${artist.musicFiles}" var="mediaFile">
+ <sub:url var="url" value="/wap/browse.view">
+ <sub:param name="path" value="${mediaFile.path}"/>
+ </sub:url>
+ <a href="${url}">${fn:escapeXml(mediaFile.title)}</a>
+ <br/>
+ </c:forEach>
+ </c:forEach>
+ </c:otherwise>
+ </c:choose>
+
+ <c:if test="${model.noMusic}">
+ <fmt:message key="wap.index.missing"/>
+ </c:if>
+
+ </small>
+ </p>
+ </card>
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/loadPlaylist.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/loadPlaylist.jsp
new file mode 100644
index 00000000..2640cce0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/loadPlaylist.jsp
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<wml>
+
+ <%@ include file="head.jsp" %>
+
+ <card id="main" title="Subsonic" newcontext="false">
+ <p><small>
+
+ <c:forEach items="${model.playlists}" var="playlist">
+ <sub:url var="url" value="/wap/playlist.view">
+ <sub:param name="load" value="${playlist.id}"/>
+ </sub:url>
+ <b><a href="${url}">${fn:escapeXml(playlist.name)}</a></b><br/>
+ </c:forEach>
+ </small></p>
+
+ </card>
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/playlist.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/playlist.jsp
new file mode 100644
index 00000000..481e00d3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/playlist.jsp
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<wml>
+
+ <%@ include file="head.jsp" %>
+
+ <c:if test="${fn:length(model.players) eq 1}">
+ <c:choose>
+ <c:when test="${empty model.players[0].name}">
+ <c:set var="playerName" value=" - Player ${model.players[0].id}"/>
+ </c:when>
+ <c:otherwise>
+ <c:set var="playerName" value=" - ${model.players[0].name}"/>
+ </c:otherwise>
+ </c:choose>
+ </c:if>
+
+
+ <card id="main" title="Subsonic" newcontext="false">
+ <p><small><b><fmt:message key="wap.playlist.title"/>${playerName}</b></small></p>
+ <p><small>
+
+ <c:choose>
+ <c:when test="${empty model.players}">
+ <fmt:message key="wap.playlist.noplayer"/>
+ </c:when>
+ <c:otherwise>
+ <b><a href="<c:url value="/wap/index.view"/>">[<fmt:message key="common.home"/>]</a></b><br/>
+ <b><a href="<c:url value="/wap/loadPlaylist.view"/>">[<fmt:message key="wap.playlist.load"/>]</a></b><br/>
+ <b><a href="<c:url value="/wap/playlist.view?random"/>">[<fmt:message key="wap.playlist.random"/>]</a></b><br/>
+
+ <c:set var="playlist" value="${model.players[0].playlist}"/>
+
+ <c:if test="${not empty playlist.files}">
+ <b><a href="<c:url value="/play.m3u"/>">[<fmt:message key="wap.playlist.play"/>]</a></b><br/>
+ <b><a href="<c:url value="/wap/playlist.view?clear"/>">[<fmt:message key="wap.playlist.clear"/>]</a></b><br/>
+ </small></p>
+ <p><small>
+
+ <c:forEach items="${playlist.files}" var="file" varStatus="loopStatus">
+ <c:set var="isCurrent" value="${(file eq playlist.currentFile) and (loopStatus.count - 1 eq playlist.index)}"/>
+ ${isCurrent ? "<b>" : ""}
+ <a href="<c:url value="/wap/playlist.view?skip=${loopStatus.count - 1}"/>">${fn:escapeXml(file.title)}</a>
+ ${isCurrent ? "</b>" : ""}
+ <br/>
+ </c:forEach>
+ </c:if>
+ </c:otherwise>
+ </c:choose>
+ </small></p>
+ </card>
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/search.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/search.jsp
new file mode 100644
index 00000000..b35b04a3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/search.jsp
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<wml>
+ <%@ include file="head.jsp" %>
+ <card id="main" title="Subsonic" newcontext="false">
+ <p>
+ <input name="query" value="" size="10"/>
+ <anchor><fmt:message key="wap.search.title"/>
+ <go href="<c:url value="/wap/searchResult.view"/>" method="get">
+ <postfield name="query" value="$query"/>
+ </go>
+ </anchor>
+ </p>
+ </card>
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/searchResult.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/searchResult.jsp
new file mode 100644
index 00000000..2267c069
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/searchResult.jsp
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<wml>
+
+ <%@ include file="head.jsp" %>
+ <card id="main" title="Subsonic" newcontext="false">
+ <p><small>
+
+ <c:choose>
+ <c:when test="${model.creatingIndex}">
+ <fmt:message key="wap.searchresult.index"/>
+ </c:when>
+
+ <c:otherwise>
+ <c:forEach items="${model.hits}" var="hit">
+ <sub:url var="url" value="/wap/browse.view">
+ <sub:param name="path" value="${hit.path}"/>
+ </sub:url>
+ <a href="${url}">${fn:escapeXml(hit.title)}</a><br/>
+ </c:forEach>
+ </c:otherwise>
+ </c:choose>
+ </small></p>
+ </card>
+
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/settings.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/settings.jsp
new file mode 100644
index 00000000..5c44e87d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/wap/settings.jsp
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml">
+
+<%@ page language="java" contentType="text/vnd.wap.wml; charset=utf-8" pageEncoding="iso-8859-1"%>
+
+<wml>
+
+ <%@ include file="head.jsp" %>
+ <card id="main" title="Subsonic" newcontext="false">
+ <p><small>
+ <b><a href="<c:url value="/wap/index.view"/>">[<fmt:message key="common.home"/>]</a><br/></b>
+ <b><a href="#player">[<fmt:message key="wap.settings.selectplayer"/>]</a></b>
+ </small></p>
+ </card>
+
+ <card id="player" title="Subsonic" newcontext="false">
+ <p><small>
+
+ <b><a href="<c:url value="/wap/index.view"/>">[<fmt:message key="common.home"/>]</a><br/></b>
+ </small></p><p><small>
+
+ <c:choose>
+ <c:when test="${empty model.playerId}">
+ <fmt:message key="wap.settings.allplayers"/>
+ </c:when>
+ <c:otherwise>
+ <a href="<c:url value="/wap/selectPlayer.view"/>"><fmt:message key="wap.settings.allplayers"/></a>
+ </c:otherwise>
+ </c:choose>
+ <br/>
+
+ <c:forEach items="${model.players}" var="player">
+ <c:choose>
+ <c:when test="${player.id eq model.playerId}">
+ ${player}
+ </c:when>
+ <c:otherwise>
+ <a href="<c:url value="/wap/selectPlayer.view?playerId=${player.id}"/>">${player}</a>
+ </c:otherwise>
+ </c:choose>
+ <br/>
+ </c:forEach>
+ </small></p>
+ </card>
+
+</wml>
+
diff --git a/subsonic-main/src/main/webapp/WEB-INF/jsp/xspfPlaylist.jsp b/subsonic-main/src/main/webapp/WEB-INF/jsp/xspfPlaylist.jsp
new file mode 100644
index 00000000..4ee8fb46
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/jsp/xspfPlaylist.jsp
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<%@ include file="include.jsp" %>
+<%@ page language="java" contentType="text/xml; charset=utf-8" pageEncoding="iso-8859-1" %>
+
+<playlist version="0" xmlns="http://xspf.org/ns/0/">
+ <trackList>
+
+<c:forEach var="song" items="${model.songs}">
+
+ <sub:url value="/stream" var="streamUrl">
+ <sub:param name="path" value="${song.musicFile.path}"/>
+ </sub:url>
+
+ <sub:url value="coverArt.view" var="coverArtUrl">
+ <sub:param name="size" value="200"/>
+ <c:if test="${not empty song.coverArtFile}">
+ <sub:param name="path" value="${song.coverArtFile.path}"/>
+ </c:if>
+ </sub:url>
+
+ <track>
+ <location>${streamUrl}</location>
+ <image>${coverArtUrl}</image>
+ <annotation>${song.musicFile.metaData.artist} - ${song.musicFile.title}</annotation>
+ </track>
+
+</c:forEach>
+
+ </trackList>
+</playlist> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/sub.tld b/subsonic-main/src/main/webapp/WEB-INF/sub.tld
new file mode 100644
index 00000000..ef712968
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/sub.tld
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+
+<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
+ version="2.0">
+
+ <description>Subsonic tag library</description>
+ <display-name>Subsonic tag library</display-name>
+ <tlib-version>1.1</tlib-version>
+ <short-name>sub</short-name>
+ <uri>http://subsonic.org/taglib/sub</uri>
+
+ <tag>
+ <description>
+ 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:
+ a) Parameter values are encoded as the hexadecimal representation of the UTF-8 bytes of the original string.
+ b) Parameter names are prepended with the suffix "Utf8Hex"
+ </description>
+ <name>url</name>
+ <tag-class>net.sourceforge.subsonic.taglib.UrlTag</tag-class>
+ <body-content>JSP</body-content>
+ <attribute>
+ <description>
+ Name of the exported scoped variable for the
+ processed url. The type of the scoped variable is
+ String.
+ </description>
+ <name>var</name>
+ <required>false</required>
+ <rtexprvalue>false</rtexprvalue>
+ </attribute>
+ <attribute>
+ <description>URL to be processed.</description>
+ <name>value</name>
+ <required>false</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ <attribute>
+ <description>The encoding to use. Default is ISO-8859-1.</description>
+ <name>encoding</name>
+ <required>false</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ </tag>
+
+ <tag>
+ <description>Adds a parameter to a containing 'url' tag.</description>
+ <name>param</name>
+ <tag-class>net.sourceforge.subsonic.taglib.ParamTag</tag-class>
+ <body-content>empty</body-content>
+ <attribute>
+ <description>Name of the query string parameter.</description>
+ <name>name</name>
+ <required>true</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ <attribute>
+ <description>Value of the parameter.</description>
+ <name>value</name>
+ <required>false</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ </tag>
+
+ <tag>
+ <description>
+ Converts a byte-count to a formatted string suitable for display to the user, with respect
+ to the current locale.
+ </description>
+ <name>formatBytes</name>
+ <tag-class>net.sourceforge.subsonic.taglib.FormatBytesTag</tag-class>
+ <body-content>JSP</body-content>
+ <attribute>
+ <description>The byte count.</description>
+ <name>bytes</name>
+ <required>true</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ </tag>
+
+ <tag>
+ <description>
+ Renders a Wiki text with markup to HTML, using the Radeox render engine.
+ </description>
+ <name>wiki</name>
+ <tag-class>net.sourceforge.subsonic.taglib.WikiTag</tag-class>
+ <body-content>JSP</body-content>
+ <attribute>
+ <description>The Wiki markup text.</description>
+ <name>text</name>
+ <required>true</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ </tag>
+
+ <tag>
+ <description>
+ Escapes the characters in a string using JavaScript rules.
+ </description>
+ <name>escapeJavaScript</name>
+ <tag-class>net.sourceforge.subsonic.taglib.EscapeJavaScriptTag</tag-class>
+ <body-content>JSP</body-content>
+ <attribute>
+ <description>The string to escape.</description>
+ <name>string</name>
+ <required>true</required>
+ <rtexprvalue>true</rtexprvalue>
+ </attribute>
+ </tag>
+
+</taglib> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/WEB-INF/subsonic-servlet.xml b/subsonic-main/src/main/webapp/WEB-INF/subsonic-servlet.xml
new file mode 100644
index 00000000..b36bb0c8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/subsonic-servlet.xml
@@ -0,0 +1,479 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="leftController" class="net.sourceforge.subsonic.controller.LeftController">
+ <property name="viewName" value="left"/>
+ <property name="mediaScannerService" ref="mediaScannerService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="musicIndexService" ref="musicIndexService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="playlistService" ref="playlistService"/>
+ </bean>
+ <bean id="rightController" class="net.sourceforge.subsonic.controller.RightController">
+ <property name="viewName" value="right"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="statusController" class="net.sourceforge.subsonic.controller.StatusController">
+ <property name="viewName" value="status"/>
+ <property name="statusService" ref="statusService"/>
+ </bean>
+ <bean id="mainController" class="net.sourceforge.subsonic.controller.MainController">
+ <property name="viewName" value="main"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="ratingService" ref="musicInfoService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="adService" ref="adService"/>
+ </bean>
+ <bean id="playlistController" class="net.sourceforge.subsonic.controller.PlaylistController">
+ <property name="viewName" value="playlist"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="importPlaylistController" class="net.sourceforge.subsonic.controller.ImportPlaylistController">
+ <property name="viewName" value="importPlaylist"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="playlistService" ref="playlistService"/>
+ </bean>
+ <bean id="topController" class="net.sourceforge.subsonic.controller.TopController">
+ <property name="viewName" value="top"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="versionService" ref="versionService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="helpController" class="net.sourceforge.subsonic.controller.HelpController">
+ <property name="viewName" value="help"/>
+ <property name="versionService" ref="versionService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="moreController" class="net.sourceforge.subsonic.controller.MoreController">
+ <property name="viewName" value="more"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="playerService" ref="playerService"/>
+ </bean>
+ <bean id="uploadController" class="net.sourceforge.subsonic.controller.UploadController">
+ <property name="viewName" value="upload"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="lyricsController" class="net.sourceforge.subsonic.controller.LyricsController">
+ <property name="viewName" value="lyrics"/>
+ </bean>
+ <bean id="allmusicController" class="net.sourceforge.subsonic.controller.AllmusicController">
+ <property name="viewName" value="allmusic"/>
+ </bean>
+ <bean id="podcastController" class="net.sourceforge.subsonic.controller.PodcastController">
+ <property name="viewName" value="podcast"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="podcastReceiverController" class="net.sourceforge.subsonic.controller.PodcastReceiverController">
+ <property name="viewName" value="podcastReceiver"/>
+ <property name="podcastService" ref="podcastService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="podcastReceiverAdminController"
+ class="net.sourceforge.subsonic.controller.PodcastReceiverAdminController">
+ <property name="podcastService" ref="podcastService"/>
+ </bean>
+ <bean id="setMusicFileInfoController" class="net.sourceforge.subsonic.controller.SetMusicFileInfoController">
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+ <bean id="shareManagementController" class="net.sourceforge.subsonic.controller.ShareManagementController">
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="shareService" ref="shareService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="setRatingController" class="net.sourceforge.subsonic.controller.SetRatingController">
+ <property name="ratingService" ref="musicInfoService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="randomPlayQueueController" class="net.sourceforge.subsonic.controller.RandomPlayQueueController">
+ <property name="viewName" value="reload"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="searchService" ref="searchService"/>
+ <property name="reloadFrames">
+ <list>
+ <bean class="net.sourceforge.subsonic.controller.ReloadFrame">
+ <property name="frame" value="playQueue"/>
+ <property name="view" value="playQueue.view?"/>
+ </bean>
+ <bean class="net.sourceforge.subsonic.controller.ReloadFrame">
+ <property name="frame" value="main"/>
+ <property name="view" value="more.view"/>
+ </bean>
+ </list>
+ </property>
+ </bean>
+ <bean id="changeCoverArtController" class="net.sourceforge.subsonic.controller.ChangeCoverArtController">
+ <property name="viewName" value="changeCoverArt"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+ <bean id="nowPlayingController" class="net.sourceforge.subsonic.controller.NowPlayingController">
+ <property name="playerService" ref="playerService"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+ <bean id="starredController" class="net.sourceforge.subsonic.controller.StarredController">
+ <property name="viewName" value="starred"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+ <bean id="searchController" class="net.sourceforge.subsonic.controller.SearchController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.SearchCommand"/>
+ <property name="successView" value="search"/>
+ <property name="formView" value="search"/>
+ <property name="searchService" ref="searchService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="playerService" ref="playerService"/>
+ </bean>
+ <bean id="settingsController" class="net.sourceforge.subsonic.controller.SettingsController">
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="playerSettingsController" class="net.sourceforge.subsonic.controller.PlayerSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.PlayerSettingsCommand"/>
+ <property name="successView" value="playerSettings"/>
+ <property name="formView" value="playerSettings"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ </bean>
+ <bean id="shareSettingsController" class="net.sourceforge.subsonic.controller.ShareSettingsController">
+ <property name="viewName" value="shareSettings"/>
+ <property name="shareService" ref="shareService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+ <bean id="musicFolderSettingsController" class="net.sourceforge.subsonic.controller.MusicFolderSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.MusicFolderSettingsCommand"/>
+ <property name="successView" value="musicFolderSettings"/>
+ <property name="formView" value="musicFolderSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaScannerService" ref="mediaScannerService"/>
+ <property name="artistDao" ref="artistDao"/>
+ <property name="albumDao" ref="albumDao"/>
+ <property name="mediaFolderDao" ref="mediaFileDao"/>
+ </bean>
+ <bean id="networkSettingsController" class="net.sourceforge.subsonic.controller.NetworkSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.NetworkSettingsCommand"/>
+ <property name="successView" value="networkSettings"/>
+ <property name="formView" value="networkSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="networkService" ref="networkService"/>
+ </bean>
+ <bean id="transcodingSettingsController" class="net.sourceforge.subsonic.controller.TranscodingSettingsController">
+ <property name="viewName" value="transcodingSettings"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="internetRadioSettingsController"
+ class="net.sourceforge.subsonic.controller.InternetRadioSettingsController">
+ <property name="viewName" value="internetRadioSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="podcastSettingsController" class="net.sourceforge.subsonic.controller.PodcastSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.PodcastSettingsCommand"/>
+ <property name="successView" value="podcastSettings"/>
+ <property name="formView" value="podcastSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="podcastService" ref="podcastService"/>
+ </bean>
+ <bean id="generalSettingsController" class="net.sourceforge.subsonic.controller.GeneralSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.GeneralSettingsCommand"/>
+ <property name="successView" value="generalSettings"/>
+ <property name="formView" value="generalSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="advancedSettingsController" class="net.sourceforge.subsonic.controller.AdvancedSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.AdvancedSettingsCommand"/>
+ <property name="successView" value="advancedSettings"/>
+ <property name="formView" value="advancedSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="personalSettingsController" class="net.sourceforge.subsonic.controller.PersonalSettingsController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.PersonalSettingsCommand"/>
+ <property name="successView" value="personalSettings"/>
+ <property name="formView" value="personalSettings"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="avatarUploadController" class="net.sourceforge.subsonic.controller.AvatarUploadController">
+ <property name="viewName" value="avatarUploadResult"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="userSettingsController" class="net.sourceforge.subsonic.controller.UserSettingsController">
+ <property name="sessionForm" value="true"/>
+ <property name="commandClass" value="net.sourceforge.subsonic.command.UserSettingsCommand"/>
+ <property name="validator" ref="userSettingsValidator"/>
+ <property name="successView" value="userSettings"/>
+ <property name="formView" value="userSettings"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ </bean>
+ <bean id="passwordSettingsController" class="net.sourceforge.subsonic.controller.PasswordSettingsController">
+ <property name="sessionForm" value="true"/>
+ <property name="commandClass" value="net.sourceforge.subsonic.command.PasswordSettingsCommand"/>
+ <property name="validator" ref="passwordSettingsValidator"/>
+ <property name="successView" value="passwordSettings"/>
+ <property name="formView" value="passwordSettings"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="homeController" class="net.sourceforge.subsonic.controller.HomeController">
+ <property name="viewName" value="home"/>
+ <property name="ratingService" ref="musicInfoService"/>
+ <property name="mediaScannerService" ref="mediaScannerService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="searchService" ref="searchService"/>
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="editTagsController" class="net.sourceforge.subsonic.controller.EditTagsController">
+ <property name="viewName" value="editTags"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="metaDataParserFactory" ref="metaDataParserFactory"/>
+ </bean>
+ <bean id="playQueueController" class="net.sourceforge.subsonic.controller.PlayQueueController">
+ <property name="viewName" value="playQueue"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="coverArtController" class="net.sourceforge.subsonic.controller.CoverArtController">
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="artistDao" ref="artistDao"/>
+ <property name="albumDao" ref="albumDao"/>
+ </bean>
+ <bean id="avatarController" class="net.sourceforge.subsonic.controller.AvatarController">
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="proxyController" class="net.sourceforge.subsonic.controller.ProxyController"/>
+ <bean id="statusChartController" class="net.sourceforge.subsonic.controller.StatusChartController">
+ <property name="statusService" ref="statusService"/>
+ </bean>
+ <bean id="userChartController" class="net.sourceforge.subsonic.controller.UserChartController">
+ <property name="securityService" ref="securityService"/>
+ </bean>
+ <bean id="m3uController" class="net.sourceforge.subsonic.controller.M3UController">
+ <property name="playerService" ref="playerService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ </bean>
+ <bean id="streamController" class="net.sourceforge.subsonic.controller.StreamController">
+ <property name="playerService" ref="playerService"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="searchService" ref="searchService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ <property name="audioScrobblerService" ref="audioScrobblerService"/>
+ </bean>
+ <bean id="videoPlayerController" class="net.sourceforge.subsonic.controller.VideoPlayerController">
+ <property name="viewName" value="videoPlayer"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="playerService" ref="playerService"/>
+ </bean>
+ <bean id="externalPlayerController" class="net.sourceforge.subsonic.controller.ExternalPlayerController">
+ <property name="viewName" value="externalPlayer"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="shareDao" ref="shareDao"/>
+ </bean>
+ <bean id="downloadController" class="net.sourceforge.subsonic.controller.DownloadController">
+ <property name="playerService" ref="playerService"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ </bean>
+ <bean id="donateController" class="net.sourceforge.subsonic.controller.DonateController">
+ <property name="commandClass" value="net.sourceforge.subsonic.command.DonateCommand"/>
+ <property name="successView" value="donate"/>
+ <property name="formView" value="donate"/>
+ <property name="validator" ref="donateValidator"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="multiController" class="net.sourceforge.subsonic.controller.MultiController">
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ <property name="playlistService" ref="playlistService"/>
+ </bean>
+ <bean id="wapController" class="net.sourceforge.subsonic.controller.WapController">
+ <property name="settingsService" ref="settingsService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="searchService" ref="searchService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="musicIndexService" ref="musicIndexService"/>
+ </bean>
+ <bean id="restController" class="net.sourceforge.subsonic.controller.RESTController">
+ <property name="settingsService" ref="settingsService"/>
+ <property name="securityService" ref="securityService"/>
+ <property name="playerService" ref="playerService"/>
+ <property name="mediaFileService" ref="mediaFileService"/>
+ <property name="transcodingService" ref="transcodingService"/>
+ <property name="statusService" ref="statusService"/>
+ <property name="searchService" ref="searchService"/>
+ <property name="jukeboxService" ref="jukeboxService"/>
+ <property name="audioScrobblerService" ref="audioScrobblerService"/>
+ <property name="playlistService" ref="playlistService"/>
+ <property name="playQueueService" ref="ajaxPlayQueueService"/>
+ <property name="ratingService" ref="musicInfoService"/>
+ <property name="chatService" ref="ajaxChatService"/>
+ <property name="lyricsService" ref="ajaxLyricsService"/>
+ <property name="podcastService" ref="podcastService"/>
+ <property name="shareService" ref="shareService"/>
+ <property name="mediaFileDao" ref="mediaFileDao"/>
+ <property name="artistDao" ref="artistDao"/>
+ <property name="albumDao" ref="albumDao"/>
+ <property name="downloadController" ref="downloadController"/>
+ <property name="streamController" ref="streamController"/>
+ <property name="coverArtController" ref="coverArtController"/>
+ <property name="avatarController" ref="avatarController"/>
+ <property name="userSettingsController" ref="userSettingsController"/>
+ <property name="leftController" ref="leftController"/>
+ <property name="homeController" ref="homeController"/>
+ </bean>
+ <bean id="dbController" class="net.sourceforge.subsonic.controller.DBController">
+ <property name="viewName" value="db"/>
+ <property name="daoHelper" ref="daoHelper"/>
+ </bean>
+ <bean id="donateValidator" class="net.sourceforge.subsonic.validator.DonateValidator">
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="userSettingsValidator" class="net.sourceforge.subsonic.validator.UserSettingsValidator">
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+ <bean id="passwordSettingsValidator" class="net.sourceforge.subsonic.validator.PasswordSettingsValidator"/>
+
+ <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
+ <property name="alwaysUseFullPath" value="true"/>
+ <property name="mappings">
+ <props>
+ <prop key="/main.view">mainController</prop>
+ <prop key="/playlist.view">playlistController</prop>
+ <prop key="/help.view">helpController</prop>
+ <prop key="/lyrics.view">lyricsController</prop>
+ <prop key="/left.view">leftController</prop>
+ <prop key="/right.view">rightController</prop>
+ <prop key="/status.view">statusController</prop>
+ <prop key="/more.view">moreController</prop>
+ <prop key="/upload.view">uploadController</prop>
+ <prop key="/importPlaylist.view">importPlaylistController</prop>
+ <prop key="/exportPlaylist.view">multiController</prop>
+ <prop key="/setMusicFileInfo.view">setMusicFileInfoController</prop>
+ <prop key="/createShare.view">shareManagementController</prop>
+ <prop key="/setRating.view">setRatingController</prop>
+ <prop key="/top.view">topController</prop>
+ <prop key="/randomPlayQueue.view">randomPlayQueueController</prop>
+ <prop key="/changeCoverArt.view">changeCoverArtController</prop>
+ <prop key="/login.view">multiController</prop>
+ <prop key="/recover.view">multiController</prop>
+ <prop key="/accessDenied.view">multiController</prop>
+ <prop key="/notFound.view">multiController</prop>
+ <prop key="/gettingStarted.view">multiController</prop>
+ <prop key="/index.view">multiController</prop>
+ <prop key="/videoPlayer.view">videoPlayerController</prop>
+ <prop key="/nowPlaying.view">nowPlayingController</prop>
+ <prop key="/starred.view">starredController</prop>
+ <prop key="/search.view">searchController</prop>
+ <prop key="/settings.view">settingsController</prop>
+ <prop key="/playerSettings.view">playerSettingsController</prop>
+ <prop key="/shareSettings.view">shareSettingsController</prop>
+ <prop key="/musicFolderSettings.view">musicFolderSettingsController</prop>
+ <prop key="/networkSettings.view">networkSettingsController</prop>
+ <prop key="/transcodingSettings.view">transcodingSettingsController</prop>
+ <prop key="/internetRadioSettings.view">internetRadioSettingsController</prop>
+ <prop key="/podcastSettings.view">podcastSettingsController</prop>
+ <prop key="/generalSettings.view">generalSettingsController</prop>
+ <prop key="/advancedSettings.view">advancedSettingsController</prop>
+ <prop key="/personalSettings.view">personalSettingsController</prop>
+ <prop key="/avatarUpload.view">avatarUploadController</prop>
+ <prop key="/userSettings.view">userSettingsController</prop>
+ <prop key="/passwordSettings.view">passwordSettingsController</prop>
+ <prop key="/allmusic.view">allmusicController</prop>
+ <prop key="/home.view">homeController</prop>
+ <prop key="/editTags.view">editTagsController</prop>
+ <prop key="/playQueue.view">playQueueController</prop>
+ <prop key="/coverArt.view">coverArtController</prop>
+ <prop key="/avatar.view">avatarController</prop>
+ <prop key="/proxy.view">proxyController</prop>
+ <prop key="/statusChart.view">statusChartController</prop>
+ <prop key="/userChart.view">userChartController</prop>
+ <prop key="/download.view">downloadController</prop>
+ <prop key="/donate.view">donateController</prop>
+ <prop key="/db.view">dbController</prop>
+ <prop key="/test.view">multiController</prop>
+ <prop key="/podcastReceiver.view">podcastReceiverController</prop>
+ <prop key="/podcastReceiverAdmin.view">podcastReceiverAdminController</prop>
+ <prop key="/podcast.view">podcastController</prop>
+ <prop key="/podcast/**">podcastController</prop>
+ <prop key="/wap/download.view">downloadController</prop>
+ <prop key="/wap/**">wapController</prop>
+ <prop key="/rest/**">restController</prop>
+ <prop key="/play.m3u">m3uController</prop>
+ <prop key="/stream/**">streamController</prop>
+ <prop key="/share/**">externalPlayerController</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
+ <property name="basename" value="net.sourceforge.subsonic.i18n.ResourceBundle"/>
+ </bean>
+
+ <bean id="themeSource" class="net.sourceforge.subsonic.theme.SubsonicThemeSource">
+ <property name="basenamePrefix" value="net.sourceforge.subsonic.theme."/>
+ <property name="defaultResourceBundle" value="net.sourceforge.subsonic.theme.default"/>
+ </bean>
+
+ <bean id="localeResolver"
+ class="net.sourceforge.subsonic.i18n.SubsonicLocaleResolver">
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+
+ <bean id="themeResolver"
+ class="net.sourceforge.subsonic.theme.SubsonicThemeResolver">
+ <property name="securityService" ref="securityService"/>
+ <property name="settingsService" ref="settingsService"/>
+ </bean>
+
+ <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
+ <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
+ <property name="prefix" value="/WEB-INF/jsp/"/>
+ <property name="suffix" value=".jsp"/>
+ </bean>
+
+</beans>
diff --git a/subsonic-main/src/main/webapp/WEB-INF/web.xml b/subsonic-main/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000..bf484e28
--- /dev/null
+++ b/subsonic-main/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app id="subsonic" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
+
+ <display-name>Subsonic Music Streamer</display-name>
+
+ <!-- Location of application context. Used by ContextLoaderListener. -->
+ <context-param>
+ <param-name>contextConfigLocation</param-name>
+ <param-value>
+ /WEB-INF/applicationContext-service.xml
+ /WEB-INF/applicationContext-security.xml
+ /WEB-INF/applicationContext-cache.xml
+ </param-value>
+ </context-param>
+
+ <listener>
+ <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
+ </listener>
+ <listener>
+ <listener-class>net.sf.ehcache.constructs.web.ShutdownListener</listener-class>
+ </listener>
+
+ <servlet>
+ <servlet-name>subsonic</servlet-name>
+ <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+ <load-on-startup>1</load-on-startup>
+ </servlet>
+
+ <servlet>
+ <display-name>DWR Servlet</display-name>
+ <servlet-name>dwr-invoker</servlet-name>
+ <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>*.view</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>/podcast</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>/wap</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>/play.m3u</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>/stream/*</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>/rest/*</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>subsonic</servlet-name>
+ <url-pattern>/share/*</url-pattern>
+ </servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>dwr-invoker</servlet-name>
+ <url-pattern>/dwr/*</url-pattern>
+ </servlet-mapping>
+
+ <welcome-file-list>
+ <welcome-file>index.html</welcome-file>
+ <welcome-file>index.jsp</welcome-file>
+ </welcome-file-list>
+
+ <error-page>
+ <exception-type>java.lang.Throwable</exception-type>
+ <location>/error.jsp</location>
+ </error-page>
+
+ <filter>
+ <filter-name>BootstrapVerificationFilter</filter-name>
+ <filter-class>net.sourceforge.subsonic.filter.BootstrapVerificationFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>BootstrapVerificationFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+ <filter>
+ <filter-name>ParameterDecodingFilter</filter-name>
+ <filter-class>net.sourceforge.subsonic.filter.ParameterDecodingFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>ParameterDecodingFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+ <filter>
+ <filter-name>RequestEncodingFilter</filter-name>
+ <filter-class>net.sourceforge.subsonic.filter.RequestEncodingFilter</filter-class>
+ <init-param>
+ <param-name>encoding</param-name>
+ <param-value>UTF-8</param-value>
+ </init-param>
+ </filter>
+ <filter-mapping>
+ <filter-name>RequestEncodingFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+ <filter>
+ <description>Sets HTTP headers to enable browser caching.</description>
+ <filter-name>CacheFilter</filter-name>
+ <filter-class>net.sourceforge.subsonic.filter.ResponseHeaderFilter</filter-class>
+ <init-param>
+ <param-name>Cache-Control</param-name>
+ <param-value>max-age=36000</param-value>
+ </init-param>
+ </filter>
+
+ <filter>
+ <description>Sets HTTP headers to disable browser caching.</description>
+ <filter-name>NoCacheFilter</filter-name>
+ <filter-class>net.sourceforge.subsonic.filter.ResponseHeaderFilter</filter-class>
+ <init-param>
+ <param-name>Cache-Control</param-name>
+ <param-value>no-cache, post-check=0, pre-check=0</param-value>
+ </init-param>
+ <init-param>
+ <param-name>Pragma</param-name>
+ <param-value>no-cache</param-value>
+ </init-param>
+ <init-param>
+ <param-name>Expires</param-name>
+ <param-value>Thu, 01 Dec 1994 16:00:00 GMT</param-value>
+ </init-param>
+ </filter>
+
+ <filter>
+ <description>The "Expires" HTTP header is set to avoid overly eager browser caching of
+ pages that implements LastModified.</description>
+ <filter-name>ExpiresFilter</filter-name>
+ <filter-class>net.sourceforge.subsonic.filter.ResponseHeaderFilter</filter-class>
+ <init-param>
+ <param-name>Expires</param-name>
+ <param-value>Thu, 01 Dec 1994 16:00:00 GMT</param-value>
+ </init-param>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>CacheFilter</filter-name>
+ <url-pattern>/icons/*</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>CacheFilter</filter-name>
+ <url-pattern>/style/*</url-pattern>
+ </filter-mapping>
+
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/statusChart.view</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/userChart.view</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/playQueue.view</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/podcastReceiver.view</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/help.view</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/top.view</url-pattern>
+ </filter-mapping>
+ <filter-mapping>
+ <filter-name>NoCacheFilter</filter-name>
+ <url-pattern>/home.view</url-pattern>
+ </filter-mapping>
+
+ <filter-mapping>
+ <filter-name>ExpiresFilter</filter-name>
+ <url-pattern>/left.view</url-pattern>
+ </filter-mapping>
+
+ <filter>
+ <filter-name>AcegiFilter</filter-name>
+ <filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
+ <init-param>
+ <param-name>targetClass</param-name>
+ <param-value>org.acegisecurity.util.FilterChainProxy</param-value>
+ </init-param>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>AcegiFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+</web-app> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/ad/omakasa.html b/subsonic-main/src/main/webapp/ad/omakasa.html
new file mode 100644
index 00000000..12145df8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/ad/omakasa.html
@@ -0,0 +1,5 @@
+<html>
+<body topmargin=0 leftmargin=0 marginheight=0 marginwidth=0>
+<script type='text/javascript'>amazon_ad_tag = 'subsonic-20'; amazon_ad_width = '120'; amazon_ad_height = '240'; amazon_ad_link_target = 'new';</script><script type='text/javascript' src='http://www.assoc-amazon.com/s/ads.js'></script>
+</body>
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/crossdomain.xml b/subsonic-main/src/main/webapp/crossdomain.xml
new file mode 100644
index 00000000..7f6057fa
--- /dev/null
+++ b/subsonic-main/src/main/webapp/crossdomain.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
+
+<cross-domain-policy>
+ <allow-access-from domain="*" />
+</cross-domain-policy> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/error.jsp b/subsonic-main/src/main/webapp/error.jsp
new file mode 100644
index 00000000..39d38a53
--- /dev/null
+++ b/subsonic-main/src/main/webapp/error.jsp
@@ -0,0 +1,48 @@
+<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="iso-8859-1" isErrorPage="true" %>
+<%@ page import="java.io.*"%>
+
+<html><head>
+ <!--[if lt IE 7.]>
+ <script defer type="text/javascript" src="script/pngfix.js"></script>
+ <![endif]-->
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" href="style/default.css" type="text/css"/>
+</head>
+
+<body>
+<h1><img src="icons/error.png" alt=""/> Error</h1>
+
+<p>
+ Subsonic encountered an internal error. You can report this error in the
+ <a href="http://forum.subsonic.org" target="_blank">Subsonic Forum</a>.
+ Please include the information below.
+</p>
+
+<%
+ StringWriter sw = new StringWriter();
+ exception.printStackTrace(new PrintWriter(sw));
+
+ long totalMemory = Runtime.getRuntime().totalMemory();
+ long freeMemory = Runtime.getRuntime().freeMemory();
+ long usedMemory = totalMemory - freeMemory;
+%>
+
+<table class="ruleTable indent">
+ <tr><td class="ruleTableHeader">Exception</td>
+ <td class="ruleTableCell"><%=exception.getClass().getName()%></td></tr>
+ <tr><td class="ruleTableHeader">Message</td>
+ <td class="ruleTableCell"><%=exception.getMessage()%></td></tr>
+ <tr><td class="ruleTableHeader">Java version</td>
+ <td class="ruleTableCell"><%=System.getProperty("java.vendor") + ' ' + System.getProperty("java.version")%></td></tr>
+ <tr><td class="ruleTableHeader">Operating system</td>
+ <td class="ruleTableCell"><%=System.getProperty("os.name") + ' ' + System.getProperty("os.version")%></td></tr>
+ <tr><td class="ruleTableHeader">Server</td>
+ <td class="ruleTableCell"><%=application.getServerInfo()%></td></tr>
+ <tr><td class="ruleTableHeader">Memory</td>
+ <td class="ruleTableCell">Used <%=usedMemory/1024L/1024L%> of <%=totalMemory/1024L/1024L%> MB</td></tr>
+ <tr><td class="ruleTableHeader" style="vertical-align:top;">Stack trace</td>
+ <td class="ruleTableCell" style="white-space:pre"><%=sw.getBuffer()%></td></tr>
+</table>
+
+</body>
+</html>
diff --git a/subsonic-main/src/main/webapp/flash/jw-player-5.6.swf b/subsonic-main/src/main/webapp/flash/jw-player-5.6.swf
new file mode 100644
index 00000000..b98411bb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/flash/jw-player-5.6.swf
Binary files differ
diff --git a/subsonic-main/src/main/webapp/flash/whotube.zip b/subsonic-main/src/main/webapp/flash/whotube.zip
new file mode 100644
index 00000000..f78a1acc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/flash/whotube.zip
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/add.gif b/subsonic-main/src/main/webapp/icons/add.gif
new file mode 100644
index 00000000..d1ff937f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/add.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/android.png b/subsonic-main/src/main/webapp/icons/android.png
new file mode 100644
index 00000000..747ee31c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/android.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/back.gif b/subsonic-main/src/main/webapp/icons/back.gif
new file mode 100644
index 00000000..81721dc1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/back.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/add.png b/subsonic-main/src/main/webapp/icons/buuftheme/add.png
new file mode 100644
index 00000000..9b4e0c1f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/android.png b/subsonic-main/src/main/webapp/icons/buuftheme/android.png
new file mode 100644
index 00000000..279b31b0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/android.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/back.png b/subsonic-main/src/main/webapp/icons/buuftheme/back.png
new file mode 100644
index 00000000..9f955e98
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/background_main.png b/subsonic-main/src/main/webapp/icons/buuftheme/background_main.png
new file mode 100644
index 00000000..357ab473
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/background_main.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/clear_rating.png b/subsonic-main/src/main/webapp/icons/buuftheme/clear_rating.png
new file mode 100644
index 00000000..4a83edbd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/donate.png b/subsonic-main/src/main/webapp/icons/buuftheme/donate.png
new file mode 100644
index 00000000..3b693839
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/donate_small.png b/subsonic-main/src/main/webapp/icons/buuftheme/donate_small.png
new file mode 100644
index 00000000..f01e447a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/donate_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/down.png b/subsonic-main/src/main/webapp/icons/buuftheme/down.png
new file mode 100644
index 00000000..9b2aae76
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/download.png b/subsonic-main/src/main/webapp/icons/buuftheme/download.png
new file mode 100644
index 00000000..0ca87330
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/error.png b/subsonic-main/src/main/webapp/icons/buuftheme/error.png
new file mode 100644
index 00000000..bec7cc39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/favicon.ico b/subsonic-main/src/main/webapp/icons/buuftheme/favicon.ico
new file mode 100644
index 00000000..5fe21f0c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/forward.png b/subsonic-main/src/main/webapp/icons/buuftheme/forward.png
new file mode 100644
index 00000000..7bc6d2bc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/gpl.png b/subsonic-main/src/main/webapp/icons/buuftheme/gpl.png
new file mode 100644
index 00000000..cd3deb71
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/gpl.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/help.png b/subsonic-main/src/main/webapp/icons/buuftheme/help.png
new file mode 100644
index 00000000..8dbbb633
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/help_small.png b/subsonic-main/src/main/webapp/icons/buuftheme/help_small.png
new file mode 100644
index 00000000..4d2f25a1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/home.png b/subsonic-main/src/main/webapp/icons/buuftheme/home.png
new file mode 100644
index 00000000..36a27453
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/home_hover.png b/subsonic-main/src/main/webapp/icons/buuftheme/home_hover.png
new file mode 100644
index 00000000..cff7ebc4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/home_hover.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/list_heading.png b/subsonic-main/src/main/webapp/icons/buuftheme/list_heading.png
new file mode 100644
index 00000000..437c55ab
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/list_heading.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/log.png b/subsonic-main/src/main/webapp/icons/buuftheme/log.png
new file mode 100644
index 00000000..d7d13bae
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/logo.png b/subsonic-main/src/main/webapp/icons/buuftheme/logo.png
new file mode 100644
index 00000000..0ac76190
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/more.png b/subsonic-main/src/main/webapp/icons/buuftheme/more.png
new file mode 100644
index 00000000..a679d8b3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/now_playing.png b/subsonic-main/src/main/webapp/icons/buuftheme/now_playing.png
new file mode 100644
index 00000000..472e6cca
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/paypal.gif b/subsonic-main/src/main/webapp/icons/buuftheme/paypal.gif
new file mode 100644
index 00000000..6d8c7f99
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/phone.png b/subsonic-main/src/main/webapp/icons/buuftheme/phone.png
new file mode 100644
index 00000000..8727f23a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/play.png b/subsonic-main/src/main/webapp/icons/buuftheme/play.png
new file mode 100644
index 00000000..29baf96f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/playing.png b/subsonic-main/src/main/webapp/icons/buuftheme/playing.png
new file mode 100644
index 00000000..a870cc1d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/podcast.png b/subsonic-main/src/main/webapp/icons/buuftheme/podcast.png
new file mode 100644
index 00000000..927d9ae3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/podcast_small.png b/subsonic-main/src/main/webapp/icons/buuftheme/podcast_small.png
new file mode 100644
index 00000000..61741833
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/progress.png b/subsonic-main/src/main/webapp/icons/buuftheme/progress.png
new file mode 100644
index 00000000..ddd5a913
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/random.png b/subsonic-main/src/main/webapp/icons/buuftheme/random.png
new file mode 100644
index 00000000..d892b383
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/rating_half.png b/subsonic-main/src/main/webapp/icons/buuftheme/rating_half.png
new file mode 100644
index 00000000..4024923c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/rating_off.png b/subsonic-main/src/main/webapp/icons/buuftheme/rating_off.png
new file mode 100644
index 00000000..97640ef2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/rating_on.png b/subsonic-main/src/main/webapp/icons/buuftheme/rating_on.png
new file mode 100644
index 00000000..cfdc2628
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/remove.png b/subsonic-main/src/main/webapp/icons/buuftheme/remove.png
new file mode 100644
index 00000000..391d89f7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/search.png b/subsonic-main/src/main/webapp/icons/buuftheme/search.png
new file mode 100644
index 00000000..1e81fd02
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/settings.png b/subsonic-main/src/main/webapp/icons/buuftheme/settings.png
new file mode 100644
index 00000000..6d520fa3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/status.png b/subsonic-main/src/main/webapp/icons/buuftheme/status.png
new file mode 100644
index 00000000..1ace9646
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/subair.png b/subsonic-main/src/main/webapp/icons/buuftheme/subair.png
new file mode 100644
index 00000000..a8a36dbe
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/subair.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/up.png b/subsonic-main/src/main/webapp/icons/buuftheme/up.png
new file mode 100644
index 00000000..e2f0193c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/upload.png b/subsonic-main/src/main/webapp/icons/buuftheme/upload.png
new file mode 100644
index 00000000..25af770e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/buuftheme/wap.png b/subsonic-main/src/main/webapp/icons/buuftheme/wap.png
new file mode 100644
index 00000000..7a3b688c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/buuftheme/wap.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/clearRating.png b/subsonic-main/src/main/webapp/icons/clearRating.png
new file mode 100644
index 00000000..a41afa13
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/clearRating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/add.png b/subsonic-main/src/main/webapp/icons/coolandclean/add.png
new file mode 100644
index 00000000..1b2b3e57
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/back.png b/subsonic-main/src/main/webapp/icons/coolandclean/back.png
new file mode 100644
index 00000000..187916f2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/background.png b/subsonic-main/src/main/webapp/icons/coolandclean/background.png
new file mode 100644
index 00000000..2de28330
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/background.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/background_main.png b/subsonic-main/src/main/webapp/icons/coolandclean/background_main.png
new file mode 100644
index 00000000..78d50118
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/background_main.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/clear_rating.png b/subsonic-main/src/main/webapp/icons/coolandclean/clear_rating.png
new file mode 100644
index 00000000..711fa862
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/donate.png b/subsonic-main/src/main/webapp/icons/coolandclean/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/down.png b/subsonic-main/src/main/webapp/icons/coolandclean/down.png
new file mode 100644
index 00000000..eeb09a15
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/download.png b/subsonic-main/src/main/webapp/icons/coolandclean/download.png
new file mode 100644
index 00000000..a0551ff7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/error.png b/subsonic-main/src/main/webapp/icons/coolandclean/error.png
new file mode 100644
index 00000000..3416f146
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/favicon.ico b/subsonic-main/src/main/webapp/icons/coolandclean/favicon.ico
new file mode 100644
index 00000000..d568f347
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/forward.png b/subsonic-main/src/main/webapp/icons/coolandclean/forward.png
new file mode 100644
index 00000000..d4541d69
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/help.png b/subsonic-main/src/main/webapp/icons/coolandclean/help.png
new file mode 100644
index 00000000..3e1efec9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/help_small.png b/subsonic-main/src/main/webapp/icons/coolandclean/help_small.png
new file mode 100644
index 00000000..c1967b47
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/home.png b/subsonic-main/src/main/webapp/icons/coolandclean/home.png
new file mode 100644
index 00000000..0b0a9dff
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/home_hover.png b/subsonic-main/src/main/webapp/icons/coolandclean/home_hover.png
new file mode 100644
index 00000000..91b069c9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/home_hover.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/list_heading.png b/subsonic-main/src/main/webapp/icons/coolandclean/list_heading.png
new file mode 100644
index 00000000..3b1d2623
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/list_heading.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/log.png b/subsonic-main/src/main/webapp/icons/coolandclean/log.png
new file mode 100644
index 00000000..8cd79b89
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/logo.png b/subsonic-main/src/main/webapp/icons/coolandclean/logo.png
new file mode 100644
index 00000000..b393527e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/more.png b/subsonic-main/src/main/webapp/icons/coolandclean/more.png
new file mode 100644
index 00000000..1825f36e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/now_playing.png b/subsonic-main/src/main/webapp/icons/coolandclean/now_playing.png
new file mode 100644
index 00000000..30997a6c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/paypal.gif b/subsonic-main/src/main/webapp/icons/coolandclean/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/phone.png b/subsonic-main/src/main/webapp/icons/coolandclean/phone.png
new file mode 100644
index 00000000..d8d85646
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/play.png b/subsonic-main/src/main/webapp/icons/coolandclean/play.png
new file mode 100644
index 00000000..bb81db5e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/playing.png b/subsonic-main/src/main/webapp/icons/coolandclean/playing.png
new file mode 100644
index 00000000..a12e02aa
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/podcast.png b/subsonic-main/src/main/webapp/icons/coolandclean/podcast.png
new file mode 100644
index 00000000..cb0c6967
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/podcast_small.png b/subsonic-main/src/main/webapp/icons/coolandclean/podcast_small.png
new file mode 100644
index 00000000..81a851b7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/progress.png b/subsonic-main/src/main/webapp/icons/coolandclean/progress.png
new file mode 100644
index 00000000..ddd5a913
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/random.png b/subsonic-main/src/main/webapp/icons/coolandclean/random.png
new file mode 100644
index 00000000..4316a393
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/rating_half.png b/subsonic-main/src/main/webapp/icons/coolandclean/rating_half.png
new file mode 100644
index 00000000..d8550838
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/rating_off.png b/subsonic-main/src/main/webapp/icons/coolandclean/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/rating_on.png b/subsonic-main/src/main/webapp/icons/coolandclean/rating_on.png
new file mode 100644
index 00000000..b3e99d1d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/remove.png b/subsonic-main/src/main/webapp/icons/coolandclean/remove.png
new file mode 100644
index 00000000..f2280eae
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/search.png b/subsonic-main/src/main/webapp/icons/coolandclean/search.png
new file mode 100644
index 00000000..0f7b5360
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/settings.png b/subsonic-main/src/main/webapp/icons/coolandclean/settings.png
new file mode 100644
index 00000000..a38f5915
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/status.png b/subsonic-main/src/main/webapp/icons/coolandclean/status.png
new file mode 100644
index 00000000..d0e5b4d4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/up.png b/subsonic-main/src/main/webapp/icons/coolandclean/up.png
new file mode 100644
index 00000000..4dc14491
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/coolandclean/upload.png b/subsonic-main/src/main/webapp/icons/coolandclean/upload.png
new file mode 100644
index 00000000..81920b18
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/coolandclean/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/current.gif b/subsonic-main/src/main/webapp/icons/current.gif
new file mode 100644
index 00000000..4176913d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/current.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/add.png b/subsonic-main/src/main/webapp/icons/denim/add.png
new file mode 100644
index 00000000..26e12f87
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/back.png b/subsonic-main/src/main/webapp/icons/denim/back.png
new file mode 100644
index 00000000..295ec91b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/clear_rating.png b/subsonic-main/src/main/webapp/icons/denim/clear_rating.png
new file mode 100644
index 00000000..64d78601
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/donate.png b/subsonic-main/src/main/webapp/icons/denim/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/down.png b/subsonic-main/src/main/webapp/icons/denim/down.png
new file mode 100644
index 00000000..5635af51
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/download.png b/subsonic-main/src/main/webapp/icons/denim/download.png
new file mode 100644
index 00000000..1b24ae83
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/error.png b/subsonic-main/src/main/webapp/icons/denim/error.png
new file mode 100644
index 00000000..c7d60958
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/favicon.ico b/subsonic-main/src/main/webapp/icons/denim/favicon.ico
new file mode 100644
index 00000000..ab7802d8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/forward.png b/subsonic-main/src/main/webapp/icons/denim/forward.png
new file mode 100644
index 00000000..b88d5106
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/help.png b/subsonic-main/src/main/webapp/icons/denim/help.png
new file mode 100644
index 00000000..de1a0f6d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/help_small.png b/subsonic-main/src/main/webapp/icons/denim/help_small.png
new file mode 100644
index 00000000..824519e9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/home.png b/subsonic-main/src/main/webapp/icons/denim/home.png
new file mode 100644
index 00000000..7f4a4d58
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/log.png b/subsonic-main/src/main/webapp/icons/denim/log.png
new file mode 100644
index 00000000..a4fa9297
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/logo.png b/subsonic-main/src/main/webapp/icons/denim/logo.png
new file mode 100644
index 00000000..82191180
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/more.png b/subsonic-main/src/main/webapp/icons/denim/more.png
new file mode 100644
index 00000000..bfc8dbb8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/now_playing.png b/subsonic-main/src/main/webapp/icons/denim/now_playing.png
new file mode 100644
index 00000000..42931eff
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/paypal.gif b/subsonic-main/src/main/webapp/icons/denim/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/phone.png b/subsonic-main/src/main/webapp/icons/denim/phone.png
new file mode 100644
index 00000000..693c8620
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/play.png b/subsonic-main/src/main/webapp/icons/denim/play.png
new file mode 100644
index 00000000..cbcb8d40
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/playing.png b/subsonic-main/src/main/webapp/icons/denim/playing.png
new file mode 100644
index 00000000..8b620c2a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/podcast.png b/subsonic-main/src/main/webapp/icons/denim/podcast.png
new file mode 100644
index 00000000..033d32c7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/podcast_small.png b/subsonic-main/src/main/webapp/icons/denim/podcast_small.png
new file mode 100644
index 00000000..87d2fe25
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/progress.png b/subsonic-main/src/main/webapp/icons/denim/progress.png
new file mode 100644
index 00000000..13b465ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/random.png b/subsonic-main/src/main/webapp/icons/denim/random.png
new file mode 100644
index 00000000..2b0fdb06
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/rating_half.png b/subsonic-main/src/main/webapp/icons/denim/rating_half.png
new file mode 100644
index 00000000..08ab0d39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/rating_off.png b/subsonic-main/src/main/webapp/icons/denim/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/rating_on.png b/subsonic-main/src/main/webapp/icons/denim/rating_on.png
new file mode 100644
index 00000000..8c4d6602
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/remove.png b/subsonic-main/src/main/webapp/icons/denim/remove.png
new file mode 100644
index 00000000..c0afb187
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/search.png b/subsonic-main/src/main/webapp/icons/denim/search.png
new file mode 100644
index 00000000..6dea8731
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/settings.png b/subsonic-main/src/main/webapp/icons/denim/settings.png
new file mode 100644
index 00000000..547bef7b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/status.png b/subsonic-main/src/main/webapp/icons/denim/status.png
new file mode 100644
index 00000000..f9c4503f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/up.png b/subsonic-main/src/main/webapp/icons/denim/up.png
new file mode 100644
index 00000000..ee5ad884
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/denim/upload.png b/subsonic-main/src/main/webapp/icons/denim/upload.png
new file mode 100644
index 00000000..cf3f38da
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/denim/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/donate.png b/subsonic-main/src/main/webapp/icons/donate.png
new file mode 100644
index 00000000..3b00577e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/donate_small.png b/subsonic-main/src/main/webapp/icons/donate_small.png
new file mode 100644
index 00000000..3a8c311d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/donate_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/down.gif b/subsonic-main/src/main/webapp/icons/down.gif
new file mode 100644
index 00000000..091805f6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/down.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/download.gif b/subsonic-main/src/main/webapp/icons/download.gif
new file mode 100644
index 00000000..3fcdfd00
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/download.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/error.png b/subsonic-main/src/main/webapp/icons/error.png
new file mode 100644
index 00000000..45b64a79
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/favicon.ico b/subsonic-main/src/main/webapp/icons/favicon.ico
new file mode 100644
index 00000000..d2c13383
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/forward.gif b/subsonic-main/src/main/webapp/icons/forward.gif
new file mode 100644
index 00000000..dfccecb9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/forward.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/gpl.png b/subsonic-main/src/main/webapp/icons/gpl.png
new file mode 100644
index 00000000..b06e0439
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/gpl.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/add.png b/subsonic-main/src/main/webapp/icons/groove/add.png
new file mode 100644
index 00000000..10657ea1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/back.png b/subsonic-main/src/main/webapp/icons/groove/back.png
new file mode 100644
index 00000000..7e1355fd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/background_main.png b/subsonic-main/src/main/webapp/icons/groove/background_main.png
new file mode 100644
index 00000000..27c223fa
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/background_main.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/background_main_blank.png b/subsonic-main/src/main/webapp/icons/groove/background_main_blank.png
new file mode 100644
index 00000000..d0866f7e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/background_main_blank.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/clear_rating.png b/subsonic-main/src/main/webapp/icons/groove/clear_rating.png
new file mode 100644
index 00000000..8cbdc24d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/donate.png b/subsonic-main/src/main/webapp/icons/groove/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/down.png b/subsonic-main/src/main/webapp/icons/groove/down.png
new file mode 100644
index 00000000..748c5143
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/download.png b/subsonic-main/src/main/webapp/icons/groove/download.png
new file mode 100644
index 00000000..748c5143
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/error.png b/subsonic-main/src/main/webapp/icons/groove/error.png
new file mode 100644
index 00000000..decb2c1d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/favicon.ico b/subsonic-main/src/main/webapp/icons/groove/favicon.ico
new file mode 100644
index 00000000..82690e7a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/forward.png b/subsonic-main/src/main/webapp/icons/groove/forward.png
new file mode 100644
index 00000000..4ac6e864
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/help.png b/subsonic-main/src/main/webapp/icons/groove/help.png
new file mode 100644
index 00000000..b9e23c4c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/help_small.png b/subsonic-main/src/main/webapp/icons/groove/help_small.png
new file mode 100644
index 00000000..80a8da30
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/home.png b/subsonic-main/src/main/webapp/icons/groove/home.png
new file mode 100644
index 00000000..f79e99d9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/log.png b/subsonic-main/src/main/webapp/icons/groove/log.png
new file mode 100644
index 00000000..37f2bcd6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/logo.png b/subsonic-main/src/main/webapp/icons/groove/logo.png
new file mode 100644
index 00000000..9f1f5a86
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/more.png b/subsonic-main/src/main/webapp/icons/groove/more.png
new file mode 100644
index 00000000..4ed374dc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/now_playing.png b/subsonic-main/src/main/webapp/icons/groove/now_playing.png
new file mode 100644
index 00000000..4fa0b813
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/paypal.gif b/subsonic-main/src/main/webapp/icons/groove/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/phone.png b/subsonic-main/src/main/webapp/icons/groove/phone.png
new file mode 100644
index 00000000..693c8620
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/play.png b/subsonic-main/src/main/webapp/icons/groove/play.png
new file mode 100644
index 00000000..1735f2b4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/playing.png b/subsonic-main/src/main/webapp/icons/groove/playing.png
new file mode 100644
index 00000000..739cbf92
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/podcast.png b/subsonic-main/src/main/webapp/icons/groove/podcast.png
new file mode 100644
index 00000000..bd9f0364
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/podcast_small.png b/subsonic-main/src/main/webapp/icons/groove/podcast_small.png
new file mode 100644
index 00000000..9379394d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/progress.png b/subsonic-main/src/main/webapp/icons/groove/progress.png
new file mode 100644
index 00000000..7c760d04
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/random.png b/subsonic-main/src/main/webapp/icons/groove/random.png
new file mode 100644
index 00000000..d8e3a679
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/rating_half.png b/subsonic-main/src/main/webapp/icons/groove/rating_half.png
new file mode 100644
index 00000000..08c51e26
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/rating_off.png b/subsonic-main/src/main/webapp/icons/groove/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/rating_on.png b/subsonic-main/src/main/webapp/icons/groove/rating_on.png
new file mode 100644
index 00000000..1c1239fa
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/remove.png b/subsonic-main/src/main/webapp/icons/groove/remove.png
new file mode 100644
index 00000000..e4d8e203
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/search.png b/subsonic-main/src/main/webapp/icons/groove/search.png
new file mode 100644
index 00000000..9e350fb0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/settings.png b/subsonic-main/src/main/webapp/icons/groove/settings.png
new file mode 100644
index 00000000..7bca5d5a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/status.png b/subsonic-main/src/main/webapp/icons/groove/status.png
new file mode 100644
index 00000000..69e94efd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/up.png b/subsonic-main/src/main/webapp/icons/groove/up.png
new file mode 100644
index 00000000..2f730914
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/groove/upload.png b/subsonic-main/src/main/webapp/icons/groove/upload.png
new file mode 100644
index 00000000..2f730914
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/groove/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hd/background.png b/subsonic-main/src/main/webapp/icons/hd/background.png
new file mode 100644
index 00000000..fa6dc078
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hd/background.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/help.png b/subsonic-main/src/main/webapp/icons/help.png
new file mode 100644
index 00000000..22801b8f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/help_small.png b/subsonic-main/src/main/webapp/icons/help_small.png
new file mode 100644
index 00000000..f25fc3fb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/favicon.ico b/subsonic-main/src/main/webapp/icons/hicon/favicon.ico
new file mode 100644
index 00000000..65dd49b4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/help.png b/subsonic-main/src/main/webapp/icons/hicon/help.png
new file mode 100644
index 00000000..5104c843
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/home.png b/subsonic-main/src/main/webapp/icons/hicon/home.png
new file mode 100644
index 00000000..ddb635ee
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/more.png b/subsonic-main/src/main/webapp/icons/hicon/more.png
new file mode 100644
index 00000000..c2d88744
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/now_playing.png b/subsonic-main/src/main/webapp/icons/hicon/now_playing.png
new file mode 100644
index 00000000..96d8166e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/podcast_large.png b/subsonic-main/src/main/webapp/icons/hicon/podcast_large.png
new file mode 100644
index 00000000..cc1e8863
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/podcast_large.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/settings.png b/subsonic-main/src/main/webapp/icons/hicon/settings.png
new file mode 100644
index 00000000..d253f9e8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/status.png b/subsonic-main/src/main/webapp/icons/hicon/status.png
new file mode 100644
index 00000000..c478dc9c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hicon/subsonic.png b/subsonic-main/src/main/webapp/icons/hicon/subsonic.png
new file mode 100644
index 00000000..083f929e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hicon/subsonic.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/Untitled-1.ico b/subsonic-main/src/main/webapp/icons/hiconi/Untitled-1.ico
new file mode 100644
index 00000000..10aecae7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/Untitled-1.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/help.png b/subsonic-main/src/main/webapp/icons/hiconi/help.png
new file mode 100644
index 00000000..c62172ff
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/home.png b/subsonic-main/src/main/webapp/icons/hiconi/home.png
new file mode 100644
index 00000000..0899225c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/more.png b/subsonic-main/src/main/webapp/icons/hiconi/more.png
new file mode 100644
index 00000000..5b021d97
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/now_playing.png b/subsonic-main/src/main/webapp/icons/hiconi/now_playing.png
new file mode 100644
index 00000000..901d7f22
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/podcast_large.png b/subsonic-main/src/main/webapp/icons/hiconi/podcast_large.png
new file mode 100644
index 00000000..258794dd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/podcast_large.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/settings.png b/subsonic-main/src/main/webapp/icons/hiconi/settings.png
new file mode 100644
index 00000000..a889eabc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/status.png b/subsonic-main/src/main/webapp/icons/hiconi/status.png
new file mode 100644
index 00000000..6934303d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hiconi/subsonic.png b/subsonic-main/src/main/webapp/icons/hiconi/subsonic.png
new file mode 100644
index 00000000..d3aa3d7b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hiconi/subsonic.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/bg.jpg b/subsonic-main/src/main/webapp/icons/hitech/bg.jpg
new file mode 100644
index 00000000..38e290bc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/bg.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/bg2.jpg b/subsonic-main/src/main/webapp/icons/hitech/bg2.jpg
new file mode 100644
index 00000000..4d86e3a1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/bg2.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/favicon.ico b/subsonic-main/src/main/webapp/icons/hitech/favicon.ico
new file mode 100644
index 00000000..3b94d2bd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/help.png b/subsonic-main/src/main/webapp/icons/hitech/help.png
new file mode 100644
index 00000000..d3558111
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/home.png b/subsonic-main/src/main/webapp/icons/hitech/home.png
new file mode 100644
index 00000000..99703bee
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/more.png b/subsonic-main/src/main/webapp/icons/hitech/more.png
new file mode 100644
index 00000000..e98c9875
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/now_playing.png b/subsonic-main/src/main/webapp/icons/hitech/now_playing.png
new file mode 100644
index 00000000..1682aac2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/podcast_large.png b/subsonic-main/src/main/webapp/icons/hitech/podcast_large.png
new file mode 100644
index 00000000..0cd41713
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/podcast_large.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/settings.png b/subsonic-main/src/main/webapp/icons/hitech/settings.png
new file mode 100644
index 00000000..f736b988
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/status.png b/subsonic-main/src/main/webapp/icons/hitech/status.png
new file mode 100644
index 00000000..2cfebe96
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/hitech/subsonic.png b/subsonic-main/src/main/webapp/icons/hitech/subsonic.png
new file mode 100644
index 00000000..44305b32
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/hitech/subsonic.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/home.png b/subsonic-main/src/main/webapp/icons/home.png
new file mode 100644
index 00000000..7e87c701
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/log.png b/subsonic-main/src/main/webapp/icons/log.png
new file mode 100644
index 00000000..8b2c6f4a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnight/back.png b/subsonic-main/src/main/webapp/icons/midnight/back.png
new file mode 100644
index 00000000..a07f3d89
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnight/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnight/forward.png b/subsonic-main/src/main/webapp/icons/midnight/forward.png
new file mode 100644
index 00000000..000658bc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnight/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/error.png b/subsonic-main/src/main/webapp/icons/midnightfun/error.png
new file mode 100644
index 00000000..e0440842
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/favicon.ico b/subsonic-main/src/main/webapp/icons/midnightfun/favicon.ico
new file mode 100644
index 00000000..17730887
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_Now_Playing.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_Now_Playing.png
new file mode 100644
index 00000000..30997a6c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_Now_Playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_add.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_add.png
new file mode 100644
index 00000000..e1fa11f8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_back.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_back.png
new file mode 100644
index 00000000..5b07c5e5
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_background.gif b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_background.gif
new file mode 100644
index 00000000..3b697cea
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_background.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_clear_Rating.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_clear_Rating.png
new file mode 100644
index 00000000..bc59f297
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_clear_Rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_donate.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_donate.png
new file mode 100644
index 00000000..2453adbe
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_down.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_down.png
new file mode 100644
index 00000000..2dd28e91
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_download.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_download.png
new file mode 100644
index 00000000..18ab592a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls.jpg b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls.jpg
new file mode 100644
index 00000000..6a2219b8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls_hover.jpg b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls_hover.jpg
new file mode 100644
index 00000000..d37fcc92
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_form_controls_hover.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_forward.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_forward.png
new file mode 100644
index 00000000..70cfd5f2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help.png
new file mode 100644
index 00000000..209f1d28
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help_small.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help_small.png
new file mode 100644
index 00000000..352fd541
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home.png
new file mode 100644
index 00000000..32727e76
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home_hover.jpg b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home_hover.jpg
new file mode 100644
index 00000000..b06edcfb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_home_hover.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_log.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_log.png
new file mode 100644
index 00000000..722ff9f1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo.png
new file mode 100644
index 00000000..d3cbbdf1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo_favicon.ico b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo_favicon.ico
new file mode 100644
index 00000000..db665a4e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_logo_favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_more.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_more.png
new file mode 100644
index 00000000..4ed5365e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_paypal.gif b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_phone.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_phone.png
new file mode 100644
index 00000000..28e2c561
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_play.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_play.png
new file mode 100644
index 00000000..bbb0f473
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_playing.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_playing.png
new file mode 100644
index 00000000..a1d9feda
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast.png
new file mode 100644
index 00000000..27845212
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast_small.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast_small.png
new file mode 100644
index 00000000..bd1d73d7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_random.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_random.png
new file mode 100644
index 00000000..05032585
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_remove.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_remove.png
new file mode 100644
index 00000000..278879fd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_search.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_search.png
new file mode 100644
index 00000000..130094f2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_settings.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_settings.png
new file mode 100644
index 00000000..a0c28f21
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_status.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_status.png
new file mode 100644
index 00000000..6f8e175b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_table.jpg b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_table.jpg
new file mode 100644
index 00000000..2f53038a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_table.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_text_back.jpg b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_text_back.jpg
new file mode 100644
index 00000000..c70073e2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_text_back.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_up.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_up.png
new file mode 100644
index 00000000..c6bd1fb2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_upload.png b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_upload.png
new file mode 100644
index 00000000..c4488ad4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/midnightfun_upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/progress.png b/subsonic-main/src/main/webapp/icons/midnightfun/progress.png
new file mode 100644
index 00000000..ddd5a913
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/ratingHalf.png b/subsonic-main/src/main/webapp/icons/midnightfun/ratingHalf.png
new file mode 100644
index 00000000..ba171709
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/ratingHalf.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/ratingOff.png b/subsonic-main/src/main/webapp/icons/midnightfun/ratingOff.png
new file mode 100644
index 00000000..bdd052ab
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/ratingOff.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/midnightfun/ratingOn.png b/subsonic-main/src/main/webapp/icons/midnightfun/ratingOn.png
new file mode 100644
index 00000000..d9588501
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/midnightfun/ratingOn.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/monochrome/subdot.png b/subsonic-main/src/main/webapp/icons/monochrome/subdot.png
new file mode 100644
index 00000000..54623ab6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/monochrome/subdot.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/more.png b/subsonic-main/src/main/webapp/icons/more.png
new file mode 100644
index 00000000..cd5a20f6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/now_playing.png b/subsonic-main/src/main/webapp/icons/now_playing.png
new file mode 100644
index 00000000..a63e630d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/paypal.gif b/subsonic-main/src/main/webapp/icons/paypal.gif
new file mode 100644
index 00000000..d017250a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/add.png b/subsonic-main/src/main/webapp/icons/pinkpanther/add.png
new file mode 100644
index 00000000..26e12f87
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/back.png b/subsonic-main/src/main/webapp/icons/pinkpanther/back.png
new file mode 100644
index 00000000..295ec91b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/clear_rating.png b/subsonic-main/src/main/webapp/icons/pinkpanther/clear_rating.png
new file mode 100644
index 00000000..64d78601
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/donate.png b/subsonic-main/src/main/webapp/icons/pinkpanther/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/down.png b/subsonic-main/src/main/webapp/icons/pinkpanther/down.png
new file mode 100644
index 00000000..5635af51
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/download.png b/subsonic-main/src/main/webapp/icons/pinkpanther/download.png
new file mode 100644
index 00000000..1b24ae83
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/error.png b/subsonic-main/src/main/webapp/icons/pinkpanther/error.png
new file mode 100644
index 00000000..c7d60958
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/favicon.ico b/subsonic-main/src/main/webapp/icons/pinkpanther/favicon.ico
new file mode 100644
index 00000000..4348b09e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/forward.png b/subsonic-main/src/main/webapp/icons/pinkpanther/forward.png
new file mode 100644
index 00000000..b88d5106
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/help.png b/subsonic-main/src/main/webapp/icons/pinkpanther/help.png
new file mode 100644
index 00000000..de1a0f6d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/help_small.png b/subsonic-main/src/main/webapp/icons/pinkpanther/help_small.png
new file mode 100644
index 00000000..824519e9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/home.png b/subsonic-main/src/main/webapp/icons/pinkpanther/home.png
new file mode 100644
index 00000000..7f4a4d58
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/log.png b/subsonic-main/src/main/webapp/icons/pinkpanther/log.png
new file mode 100644
index 00000000..a4fa9297
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/logo.png b/subsonic-main/src/main/webapp/icons/pinkpanther/logo.png
new file mode 100644
index 00000000..9bf1eb83
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/more.png b/subsonic-main/src/main/webapp/icons/pinkpanther/more.png
new file mode 100644
index 00000000..bfc8dbb8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/now_playing.png b/subsonic-main/src/main/webapp/icons/pinkpanther/now_playing.png
new file mode 100644
index 00000000..42931eff
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/paypal.gif b/subsonic-main/src/main/webapp/icons/pinkpanther/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/phone.png b/subsonic-main/src/main/webapp/icons/pinkpanther/phone.png
new file mode 100644
index 00000000..693c8620
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/play.png b/subsonic-main/src/main/webapp/icons/pinkpanther/play.png
new file mode 100644
index 00000000..cbcb8d40
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/playing.png b/subsonic-main/src/main/webapp/icons/pinkpanther/playing.png
new file mode 100644
index 00000000..8b620c2a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/podcast.png b/subsonic-main/src/main/webapp/icons/pinkpanther/podcast.png
new file mode 100644
index 00000000..033d32c7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/podcast_small.png b/subsonic-main/src/main/webapp/icons/pinkpanther/podcast_small.png
new file mode 100644
index 00000000..87d2fe25
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/progress.png b/subsonic-main/src/main/webapp/icons/pinkpanther/progress.png
new file mode 100644
index 00000000..13b465ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/random.png b/subsonic-main/src/main/webapp/icons/pinkpanther/random.png
new file mode 100644
index 00000000..2b0fdb06
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/rating_half.png b/subsonic-main/src/main/webapp/icons/pinkpanther/rating_half.png
new file mode 100644
index 00000000..08ab0d39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/rating_off.png b/subsonic-main/src/main/webapp/icons/pinkpanther/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/rating_on.png b/subsonic-main/src/main/webapp/icons/pinkpanther/rating_on.png
new file mode 100644
index 00000000..8c4d6602
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/remove.png b/subsonic-main/src/main/webapp/icons/pinkpanther/remove.png
new file mode 100644
index 00000000..c0afb187
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/search.png b/subsonic-main/src/main/webapp/icons/pinkpanther/search.png
new file mode 100644
index 00000000..6dea8731
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/settings.png b/subsonic-main/src/main/webapp/icons/pinkpanther/settings.png
new file mode 100644
index 00000000..547bef7b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/status.png b/subsonic-main/src/main/webapp/icons/pinkpanther/status.png
new file mode 100644
index 00000000..f9c4503f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/up.png b/subsonic-main/src/main/webapp/icons/pinkpanther/up.png
new file mode 100644
index 00000000..ee5ad884
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/pinkpanther/upload.png b/subsonic-main/src/main/webapp/icons/pinkpanther/upload.png
new file mode 100644
index 00000000..cf3f38da
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/pinkpanther/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/play.gif b/subsonic-main/src/main/webapp/icons/play.gif
new file mode 100644
index 00000000..d1ccbcf4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/play.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/podcast.png b/subsonic-main/src/main/webapp/icons/podcast.png
new file mode 100644
index 00000000..c8afc597
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/podcast_large.png b/subsonic-main/src/main/webapp/icons/podcast_large.png
new file mode 100644
index 00000000..e95eb343
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/podcast_large.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/progress.png b/subsonic-main/src/main/webapp/icons/progress.png
new file mode 100644
index 00000000..ddd5a913
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/random.png b/subsonic-main/src/main/webapp/icons/random.png
new file mode 100644
index 00000000..f25fc3fb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ratingHalf.png b/subsonic-main/src/main/webapp/icons/ratingHalf.png
new file mode 100644
index 00000000..d8550838
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ratingHalf.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ratingOff.png b/subsonic-main/src/main/webapp/icons/ratingOff.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ratingOff.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ratingOn.png b/subsonic-main/src/main/webapp/icons/ratingOn.png
new file mode 100644
index 00000000..b3e99d1d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ratingOn.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/remove.gif b/subsonic-main/src/main/webapp/icons/remove.gif
new file mode 100644
index 00000000..b7656a88
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/remove.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/add.gif b/subsonic-main/src/main/webapp/icons/ripserver/add.gif
new file mode 100644
index 00000000..3902057c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/add.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/back.gif b/subsonic-main/src/main/webapp/icons/ripserver/back.gif
new file mode 100644
index 00000000..c91d92c0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/back.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/background.png b/subsonic-main/src/main/webapp/icons/ripserver/background.png
new file mode 100644
index 00000000..2d4a5c7a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/background.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/clearRating.png b/subsonic-main/src/main/webapp/icons/ripserver/clearRating.png
new file mode 100644
index 00000000..59a5ca41
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/clearRating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/current.gif b/subsonic-main/src/main/webapp/icons/ripserver/current.gif
new file mode 100644
index 00000000..8591e1b6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/current.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/donate.png b/subsonic-main/src/main/webapp/icons/ripserver/donate.png
new file mode 100644
index 00000000..87fb25e1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/down.gif b/subsonic-main/src/main/webapp/icons/ripserver/down.gif
new file mode 100644
index 00000000..903bfc29
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/down.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/download.gif b/subsonic-main/src/main/webapp/icons/ripserver/download.gif
new file mode 100644
index 00000000..b9623b25
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/download.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/error.png b/subsonic-main/src/main/webapp/icons/ripserver/error.png
new file mode 100644
index 00000000..00d9f2c6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/favicon.ico b/subsonic-main/src/main/webapp/icons/ripserver/favicon.ico
new file mode 100644
index 00000000..127d5078
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/forward.gif b/subsonic-main/src/main/webapp/icons/ripserver/forward.gif
new file mode 100644
index 00000000..44199a6d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/forward.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/help.png b/subsonic-main/src/main/webapp/icons/ripserver/help.png
new file mode 100644
index 00000000..cee3d4a0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/help_small.png b/subsonic-main/src/main/webapp/icons/ripserver/help_small.png
new file mode 100644
index 00000000..36a20b64
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/home.png b/subsonic-main/src/main/webapp/icons/ripserver/home.png
new file mode 100644
index 00000000..36eb1c58
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/log.png b/subsonic-main/src/main/webapp/icons/ripserver/log.png
new file mode 100644
index 00000000..8b2c6f4a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/more.png b/subsonic-main/src/main/webapp/icons/ripserver/more.png
new file mode 100644
index 00000000..aee57a10
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/now_playing.png b/subsonic-main/src/main/webapp/icons/ripserver/now_playing.png
new file mode 100644
index 00000000..671c4074
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/paypal.gif b/subsonic-main/src/main/webapp/icons/ripserver/paypal.gif
new file mode 100644
index 00000000..d017250a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/play.gif b/subsonic-main/src/main/webapp/icons/ripserver/play.gif
new file mode 100644
index 00000000..e42ed160
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/play.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/podcast.png b/subsonic-main/src/main/webapp/icons/ripserver/podcast.png
new file mode 100644
index 00000000..f4574525
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/podcast_large.png b/subsonic-main/src/main/webapp/icons/ripserver/podcast_large.png
new file mode 100644
index 00000000..f4574525
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/podcast_large.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/progress.png b/subsonic-main/src/main/webapp/icons/ripserver/progress.png
new file mode 100644
index 00000000..ddd5a913
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/random.png b/subsonic-main/src/main/webapp/icons/ripserver/random.png
new file mode 100644
index 00000000..ee9750f0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/ratingHalf.png b/subsonic-main/src/main/webapp/icons/ripserver/ratingHalf.png
new file mode 100644
index 00000000..ba171709
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/ratingHalf.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/ratingOff.png b/subsonic-main/src/main/webapp/icons/ripserver/ratingOff.png
new file mode 100644
index 00000000..bdd052ab
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/ratingOff.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/ratingOn.png b/subsonic-main/src/main/webapp/icons/ripserver/ratingOn.png
new file mode 100644
index 00000000..d9588501
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/ratingOn.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/remove.gif b/subsonic-main/src/main/webapp/icons/ripserver/remove.gif
new file mode 100644
index 00000000..33b9414f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/remove.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/search.png b/subsonic-main/src/main/webapp/icons/ripserver/search.png
new file mode 100644
index 00000000..b6dd2039
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/settings.png b/subsonic-main/src/main/webapp/icons/ripserver/settings.png
new file mode 100644
index 00000000..c6f2ed15
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/status.png b/subsonic-main/src/main/webapp/icons/ripserver/status.png
new file mode 100644
index 00000000..fdbc7fd5
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/subsonic_black.png b/subsonic-main/src/main/webapp/icons/ripserver/subsonic_black.png
new file mode 100644
index 00000000..421b61d1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/subsonic_black.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/subsonic_white.png b/subsonic-main/src/main/webapp/icons/ripserver/subsonic_white.png
new file mode 100644
index 00000000..3089427f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/subsonic_white.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/up.gif b/subsonic-main/src/main/webapp/icons/ripserver/up.gif
new file mode 100644
index 00000000..7474ee2e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/up.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/upload.gif b/subsonic-main/src/main/webapp/icons/ripserver/upload.gif
new file mode 100644
index 00000000..43b98d96
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/upload.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/ripserver/wap.png b/subsonic-main/src/main/webapp/icons/ripserver/wap.png
new file mode 100644
index 00000000..0d67e2de
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/ripserver/wap.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/search.png b/subsonic-main/src/main/webapp/icons/search.png
new file mode 100644
index 00000000..7fbfb6f5
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/settings.png b/subsonic-main/src/main/webapp/icons/settings.png
new file mode 100644
index 00000000..9173cbf7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/share_facebook.png b/subsonic-main/src/main/webapp/icons/share_facebook.png
new file mode 100644
index 00000000..30f4d6ce
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/share_facebook.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/share_googleplus.png b/subsonic-main/src/main/webapp/icons/share_googleplus.png
new file mode 100644
index 00000000..fff340f2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/share_googleplus.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/share_twitter.png b/subsonic-main/src/main/webapp/icons/share_twitter.png
new file mode 100644
index 00000000..8b5eeace
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/share_twitter.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/add.png b/subsonic-main/src/main/webapp/icons/simplify/add.png
new file mode 100644
index 00000000..9428fc87
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/back.png b/subsonic-main/src/main/webapp/icons/simplify/back.png
new file mode 100644
index 00000000..05df3b64
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/clear_rating.png b/subsonic-main/src/main/webapp/icons/simplify/clear_rating.png
new file mode 100644
index 00000000..4d028588
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/donate.png b/subsonic-main/src/main/webapp/icons/simplify/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/down.png b/subsonic-main/src/main/webapp/icons/simplify/down.png
new file mode 100644
index 00000000..fe798e45
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/download.png b/subsonic-main/src/main/webapp/icons/simplify/download.png
new file mode 100644
index 00000000..dbebf431
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/error.png b/subsonic-main/src/main/webapp/icons/simplify/error.png
new file mode 100644
index 00000000..c7d60958
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/favicon.ico b/subsonic-main/src/main/webapp/icons/simplify/favicon.ico
new file mode 100644
index 00000000..685c131f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/forward.png b/subsonic-main/src/main/webapp/icons/simplify/forward.png
new file mode 100644
index 00000000..99b0ec04
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/help.png b/subsonic-main/src/main/webapp/icons/simplify/help.png
new file mode 100644
index 00000000..fafc0ac5
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/help_small.png b/subsonic-main/src/main/webapp/icons/simplify/help_small.png
new file mode 100644
index 00000000..063af3e4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/home.png b/subsonic-main/src/main/webapp/icons/simplify/home.png
new file mode 100644
index 00000000..5bbee5cd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/log.png b/subsonic-main/src/main/webapp/icons/simplify/log.png
new file mode 100644
index 00000000..60d07c2b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/logo.png b/subsonic-main/src/main/webapp/icons/simplify/logo.png
new file mode 100644
index 00000000..553a2bf0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/more.png b/subsonic-main/src/main/webapp/icons/simplify/more.png
new file mode 100644
index 00000000..378e05f2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/now_playing.png b/subsonic-main/src/main/webapp/icons/simplify/now_playing.png
new file mode 100644
index 00000000..10cc8cfc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/paypal.gif b/subsonic-main/src/main/webapp/icons/simplify/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/phone.png b/subsonic-main/src/main/webapp/icons/simplify/phone.png
new file mode 100644
index 00000000..693c8620
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/play.png b/subsonic-main/src/main/webapp/icons/simplify/play.png
new file mode 100644
index 00000000..76bc8586
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/playing.png b/subsonic-main/src/main/webapp/icons/simplify/playing.png
new file mode 100644
index 00000000..e46576de
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/podcast.png b/subsonic-main/src/main/webapp/icons/simplify/podcast.png
new file mode 100644
index 00000000..51004d5f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/podcast_small.png b/subsonic-main/src/main/webapp/icons/simplify/podcast_small.png
new file mode 100644
index 00000000..9af49252
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/progress.png b/subsonic-main/src/main/webapp/icons/simplify/progress.png
new file mode 100644
index 00000000..13b465ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/random.png b/subsonic-main/src/main/webapp/icons/simplify/random.png
new file mode 100644
index 00000000..dfb9ec6e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/rating_half.png b/subsonic-main/src/main/webapp/icons/simplify/rating_half.png
new file mode 100644
index 00000000..08ab0d39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/rating_off.png b/subsonic-main/src/main/webapp/icons/simplify/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/rating_on.png b/subsonic-main/src/main/webapp/icons/simplify/rating_on.png
new file mode 100644
index 00000000..8c4d6602
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/remove.png b/subsonic-main/src/main/webapp/icons/simplify/remove.png
new file mode 100644
index 00000000..c0afb187
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/search.png b/subsonic-main/src/main/webapp/icons/simplify/search.png
new file mode 100644
index 00000000..4607c1bd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/settings.png b/subsonic-main/src/main/webapp/icons/simplify/settings.png
new file mode 100644
index 00000000..ff2efcd9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/status.png b/subsonic-main/src/main/webapp/icons/simplify/status.png
new file mode 100644
index 00000000..487c309b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/up.png b/subsonic-main/src/main/webapp/icons/simplify/up.png
new file mode 100644
index 00000000..0b2a5f0b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/simplify/upload.png b/subsonic-main/src/main/webapp/icons/simplify/upload.png
new file mode 100644
index 00000000..078db7c2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/simplify/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/favicon.ico b/subsonic-main/src/main/webapp/icons/slick/favicon.ico
new file mode 100644
index 00000000..c7eb8409
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/help.png b/subsonic-main/src/main/webapp/icons/slick/help.png
new file mode 100644
index 00000000..bb22b95d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/home.png b/subsonic-main/src/main/webapp/icons/slick/home.png
new file mode 100644
index 00000000..6b729b81
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/more.png b/subsonic-main/src/main/webapp/icons/slick/more.png
new file mode 100644
index 00000000..5244ad4a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/now_playing.png b/subsonic-main/src/main/webapp/icons/slick/now_playing.png
new file mode 100644
index 00000000..0c9836a4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/podcast_large.png b/subsonic-main/src/main/webapp/icons/slick/podcast_large.png
new file mode 100644
index 00000000..694636ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/podcast_large.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/settings.png b/subsonic-main/src/main/webapp/icons/slick/settings.png
new file mode 100644
index 00000000..d62f6f98
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/status.png b/subsonic-main/src/main/webapp/icons/slick/status.png
new file mode 100644
index 00000000..f1dcd989
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/subsonic.png b/subsonic-main/src/main/webapp/icons/slick/subsonic.png
new file mode 100644
index 00000000..7d839665
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/subsonic.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/slick/top_bg.jpg b/subsonic-main/src/main/webapp/icons/slick/top_bg.jpg
new file mode 100644
index 00000000..a095ae2e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/slick/top_bg.jpg
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/add.png b/subsonic-main/src/main/webapp/icons/sonic/add.png
new file mode 100644
index 00000000..14695eb2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/back.png b/subsonic-main/src/main/webapp/icons/sonic/back.png
new file mode 100644
index 00000000..3aa8dce5
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/clear_rating.png b/subsonic-main/src/main/webapp/icons/sonic/clear_rating.png
new file mode 100644
index 00000000..5d0104e7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/donate.png b/subsonic-main/src/main/webapp/icons/sonic/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/down.png b/subsonic-main/src/main/webapp/icons/sonic/down.png
new file mode 100644
index 00000000..064a3659
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/download.png b/subsonic-main/src/main/webapp/icons/sonic/download.png
new file mode 100644
index 00000000..752ad374
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/error.png b/subsonic-main/src/main/webapp/icons/sonic/error.png
new file mode 100644
index 00000000..c7d60958
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/favicon.ico b/subsonic-main/src/main/webapp/icons/sonic/favicon.ico
new file mode 100644
index 00000000..ef507a55
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/forward.png b/subsonic-main/src/main/webapp/icons/sonic/forward.png
new file mode 100644
index 00000000..7af499e1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/help.png b/subsonic-main/src/main/webapp/icons/sonic/help.png
new file mode 100644
index 00000000..4f00214f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/help_small.png b/subsonic-main/src/main/webapp/icons/sonic/help_small.png
new file mode 100644
index 00000000..cf98d072
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/home.png b/subsonic-main/src/main/webapp/icons/sonic/home.png
new file mode 100644
index 00000000..50fe549d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/log.png b/subsonic-main/src/main/webapp/icons/sonic/log.png
new file mode 100644
index 00000000..7764ae53
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/logo.png b/subsonic-main/src/main/webapp/icons/sonic/logo.png
new file mode 100644
index 00000000..68aca05c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/more.png b/subsonic-main/src/main/webapp/icons/sonic/more.png
new file mode 100644
index 00000000..66775f30
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/now_playing.png b/subsonic-main/src/main/webapp/icons/sonic/now_playing.png
new file mode 100644
index 00000000..da58451e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/paypal.gif b/subsonic-main/src/main/webapp/icons/sonic/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/phone.png b/subsonic-main/src/main/webapp/icons/sonic/phone.png
new file mode 100644
index 00000000..c2c4f494
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/play.png b/subsonic-main/src/main/webapp/icons/sonic/play.png
new file mode 100644
index 00000000..782ceae1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/playing.png b/subsonic-main/src/main/webapp/icons/sonic/playing.png
new file mode 100644
index 00000000..4983700a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/podcast.png b/subsonic-main/src/main/webapp/icons/sonic/podcast.png
new file mode 100644
index 00000000..98511ad4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/podcast_small.png b/subsonic-main/src/main/webapp/icons/sonic/podcast_small.png
new file mode 100644
index 00000000..d5fa56bd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/progress.png b/subsonic-main/src/main/webapp/icons/sonic/progress.png
new file mode 100644
index 00000000..13b465ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/random.png b/subsonic-main/src/main/webapp/icons/sonic/random.png
new file mode 100644
index 00000000..6289938c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/rating_half.png b/subsonic-main/src/main/webapp/icons/sonic/rating_half.png
new file mode 100644
index 00000000..08ab0d39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/rating_off.png b/subsonic-main/src/main/webapp/icons/sonic/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/rating_on.png b/subsonic-main/src/main/webapp/icons/sonic/rating_on.png
new file mode 100644
index 00000000..8c4d6602
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/remove.png b/subsonic-main/src/main/webapp/icons/sonic/remove.png
new file mode 100644
index 00000000..4f9c28a0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/search.png b/subsonic-main/src/main/webapp/icons/sonic/search.png
new file mode 100644
index 00000000..4228f6bc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/settings.png b/subsonic-main/src/main/webapp/icons/sonic/settings.png
new file mode 100644
index 00000000..9897b183
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/status.png b/subsonic-main/src/main/webapp/icons/sonic/status.png
new file mode 100644
index 00000000..21ce5615
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/up.png b/subsonic-main/src/main/webapp/icons/sonic/up.png
new file mode 100644
index 00000000..19155c33
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic/upload.png b/subsonic-main/src/main/webapp/icons/sonic/upload.png
new file mode 100644
index 00000000..eb082cbc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/add.png b/subsonic-main/src/main/webapp/icons/sonic_blue/add.png
new file mode 100644
index 00000000..d16e3473
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/back.png b/subsonic-main/src/main/webapp/icons/sonic_blue/back.png
new file mode 100644
index 00000000..abf1177f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/clear_rating.png b/subsonic-main/src/main/webapp/icons/sonic_blue/clear_rating.png
new file mode 100644
index 00000000..64d78601
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/donate.png b/subsonic-main/src/main/webapp/icons/sonic_blue/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/down.png b/subsonic-main/src/main/webapp/icons/sonic_blue/down.png
new file mode 100644
index 00000000..16efb09d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/download.png b/subsonic-main/src/main/webapp/icons/sonic_blue/download.png
new file mode 100644
index 00000000..6667c702
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/error.png b/subsonic-main/src/main/webapp/icons/sonic_blue/error.png
new file mode 100644
index 00000000..c7d60958
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/favicon.ico b/subsonic-main/src/main/webapp/icons/sonic_blue/favicon.ico
new file mode 100644
index 00000000..ef507a55
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/forward.png b/subsonic-main/src/main/webapp/icons/sonic_blue/forward.png
new file mode 100644
index 00000000..d288c0ce
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/help.png b/subsonic-main/src/main/webapp/icons/sonic_blue/help.png
new file mode 100644
index 00000000..1ca1f3de
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/help_small.png b/subsonic-main/src/main/webapp/icons/sonic_blue/help_small.png
new file mode 100644
index 00000000..88283157
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/home.png b/subsonic-main/src/main/webapp/icons/sonic_blue/home.png
new file mode 100644
index 00000000..47acba54
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/log.png b/subsonic-main/src/main/webapp/icons/sonic_blue/log.png
new file mode 100644
index 00000000..a4fa9297
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/logo.png b/subsonic-main/src/main/webapp/icons/sonic_blue/logo.png
new file mode 100644
index 00000000..947d6e25
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/more.png b/subsonic-main/src/main/webapp/icons/sonic_blue/more.png
new file mode 100644
index 00000000..53fb8bb8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/now_playing.png b/subsonic-main/src/main/webapp/icons/sonic_blue/now_playing.png
new file mode 100644
index 00000000..42931eff
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/paypal.gif b/subsonic-main/src/main/webapp/icons/sonic_blue/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/phone.png b/subsonic-main/src/main/webapp/icons/sonic_blue/phone.png
new file mode 100644
index 00000000..83b16afb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/play.png b/subsonic-main/src/main/webapp/icons/sonic_blue/play.png
new file mode 100644
index 00000000..a6e692a2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/playing.png b/subsonic-main/src/main/webapp/icons/sonic_blue/playing.png
new file mode 100644
index 00000000..44882736
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/podcast.png b/subsonic-main/src/main/webapp/icons/sonic_blue/podcast.png
new file mode 100644
index 00000000..d5cd0f46
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/podcast_small.png b/subsonic-main/src/main/webapp/icons/sonic_blue/podcast_small.png
new file mode 100644
index 00000000..87d2fe25
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/progress.png b/subsonic-main/src/main/webapp/icons/sonic_blue/progress.png
new file mode 100644
index 00000000..13b465ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/random.png b/subsonic-main/src/main/webapp/icons/sonic_blue/random.png
new file mode 100644
index 00000000..2b0fdb06
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/rating_half.png b/subsonic-main/src/main/webapp/icons/sonic_blue/rating_half.png
new file mode 100644
index 00000000..08ab0d39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/rating_off.png b/subsonic-main/src/main/webapp/icons/sonic_blue/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/rating_on.png b/subsonic-main/src/main/webapp/icons/sonic_blue/rating_on.png
new file mode 100644
index 00000000..8c4d6602
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/remove.png b/subsonic-main/src/main/webapp/icons/sonic_blue/remove.png
new file mode 100644
index 00000000..56599a81
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/search.png b/subsonic-main/src/main/webapp/icons/sonic_blue/search.png
new file mode 100644
index 00000000..9c9b3e73
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/settings.png b/subsonic-main/src/main/webapp/icons/sonic_blue/settings.png
new file mode 100644
index 00000000..e3a528a0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/status.png b/subsonic-main/src/main/webapp/icons/sonic_blue/status.png
new file mode 100644
index 00000000..f99cba27
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/up.png b/subsonic-main/src/main/webapp/icons/sonic_blue/up.png
new file mode 100644
index 00000000..98b4a78c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_blue/upload.png b/subsonic-main/src/main/webapp/icons/sonic_blue/upload.png
new file mode 100644
index 00000000..ecfad1a9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_blue/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/add.png b/subsonic-main/src/main/webapp/icons/sonic_white/add.png
new file mode 100644
index 00000000..3579c354
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/add.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/back.png b/subsonic-main/src/main/webapp/icons/sonic_white/back.png
new file mode 100644
index 00000000..0fa80d9a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/back.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/clear_rating.png b/subsonic-main/src/main/webapp/icons/sonic_white/clear_rating.png
new file mode 100644
index 00000000..5d0104e7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/clear_rating.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/donate.png b/subsonic-main/src/main/webapp/icons/sonic_white/donate.png
new file mode 100644
index 00000000..f9c17ceb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/donate.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/down.png b/subsonic-main/src/main/webapp/icons/sonic_white/down.png
new file mode 100644
index 00000000..f509be10
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/down.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/download.png b/subsonic-main/src/main/webapp/icons/sonic_white/download.png
new file mode 100644
index 00000000..ee5ebb58
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/download.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/error.png b/subsonic-main/src/main/webapp/icons/sonic_white/error.png
new file mode 100644
index 00000000..c7d60958
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/error.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/favicon.ico b/subsonic-main/src/main/webapp/icons/sonic_white/favicon.ico
new file mode 100644
index 00000000..ef507a55
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/favicon.ico
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/forward.png b/subsonic-main/src/main/webapp/icons/sonic_white/forward.png
new file mode 100644
index 00000000..cce6e6f4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/forward.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/help.png b/subsonic-main/src/main/webapp/icons/sonic_white/help.png
new file mode 100644
index 00000000..7380ec10
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/help.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/help_small.png b/subsonic-main/src/main/webapp/icons/sonic_white/help_small.png
new file mode 100644
index 00000000..cf98d072
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/help_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/home.png b/subsonic-main/src/main/webapp/icons/sonic_white/home.png
new file mode 100644
index 00000000..882b7f4a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/home.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/log.png b/subsonic-main/src/main/webapp/icons/sonic_white/log.png
new file mode 100644
index 00000000..7764ae53
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/log.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/logo.png b/subsonic-main/src/main/webapp/icons/sonic_white/logo.png
new file mode 100644
index 00000000..68aca05c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/logo.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/more.png b/subsonic-main/src/main/webapp/icons/sonic_white/more.png
new file mode 100644
index 00000000..17274a6b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/more.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/now_playing.png b/subsonic-main/src/main/webapp/icons/sonic_white/now_playing.png
new file mode 100644
index 00000000..da58451e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/now_playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/paypal.gif b/subsonic-main/src/main/webapp/icons/sonic_white/paypal.gif
new file mode 100644
index 00000000..2a587165
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/paypal.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/phone.png b/subsonic-main/src/main/webapp/icons/sonic_white/phone.png
new file mode 100644
index 00000000..c2c4f494
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/phone.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/play.png b/subsonic-main/src/main/webapp/icons/sonic_white/play.png
new file mode 100644
index 00000000..7ee33876
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/play.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/playing.png b/subsonic-main/src/main/webapp/icons/sonic_white/playing.png
new file mode 100644
index 00000000..554d0404
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/playing.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/podcast.png b/subsonic-main/src/main/webapp/icons/sonic_white/podcast.png
new file mode 100644
index 00000000..9d7b47dd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/podcast.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/podcast_small.png b/subsonic-main/src/main/webapp/icons/sonic_white/podcast_small.png
new file mode 100644
index 00000000..d5fa56bd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/podcast_small.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/progress.png b/subsonic-main/src/main/webapp/icons/sonic_white/progress.png
new file mode 100644
index 00000000..13b465ba
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/progress.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/random.png b/subsonic-main/src/main/webapp/icons/sonic_white/random.png
new file mode 100644
index 00000000..6289938c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/random.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/rating_half.png b/subsonic-main/src/main/webapp/icons/sonic_white/rating_half.png
new file mode 100644
index 00000000..08ab0d39
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/rating_half.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/rating_off.png b/subsonic-main/src/main/webapp/icons/sonic_white/rating_off.png
new file mode 100644
index 00000000..a0d9bbb1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/rating_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/rating_on.png b/subsonic-main/src/main/webapp/icons/sonic_white/rating_on.png
new file mode 100644
index 00000000..8c4d6602
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/rating_on.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/remove.png b/subsonic-main/src/main/webapp/icons/sonic_white/remove.png
new file mode 100644
index 00000000..7c63ab7e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/remove.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/search.png b/subsonic-main/src/main/webapp/icons/sonic_white/search.png
new file mode 100644
index 00000000..6b885f91
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/search.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/settings.png b/subsonic-main/src/main/webapp/icons/sonic_white/settings.png
new file mode 100644
index 00000000..707e0a20
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/settings.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/status.png b/subsonic-main/src/main/webapp/icons/sonic_white/status.png
new file mode 100644
index 00000000..53f13830
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/up.png b/subsonic-main/src/main/webapp/icons/sonic_white/up.png
new file mode 100644
index 00000000..1e7260ca
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/up.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/sonic_white/upload.png b/subsonic-main/src/main/webapp/icons/sonic_white/upload.png
new file mode 100644
index 00000000..8dc0cfcb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/sonic_white/upload.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/spinner.gif b/subsonic-main/src/main/webapp/icons/spinner.gif
new file mode 100644
index 00000000..e192ca89
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/spinner.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/star_off.png b/subsonic-main/src/main/webapp/icons/star_off.png
new file mode 100644
index 00000000..ada53fc3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/star_off.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/starred.png b/subsonic-main/src/main/webapp/icons/starred.png
new file mode 100644
index 00000000..5476c401
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/starred.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/status.png b/subsonic-main/src/main/webapp/icons/status.png
new file mode 100644
index 00000000..8df38463
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/status.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/subair.png b/subsonic-main/src/main/webapp/icons/subair.png
new file mode 100644
index 00000000..a8a36dbe
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/subair.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/subsonic_black.png b/subsonic-main/src/main/webapp/icons/subsonic_black.png
new file mode 100644
index 00000000..21448a8e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/subsonic_black.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/subsonic_white.png b/subsonic-main/src/main/webapp/icons/subsonic_white.png
new file mode 100644
index 00000000..65db730a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/subsonic_white.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/up.gif b/subsonic-main/src/main/webapp/icons/up.gif
new file mode 100644
index 00000000..20223146
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/up.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/upload.gif b/subsonic-main/src/main/webapp/icons/upload.gif
new file mode 100644
index 00000000..b1605639
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/upload.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/icons/wap.png b/subsonic-main/src/main/webapp/icons/wap.png
new file mode 100644
index 00000000..ea734215
--- /dev/null
+++ b/subsonic-main/src/main/webapp/icons/wap.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/index.html b/subsonic-main/src/main/webapp/index.html
new file mode 100644
index 00000000..5c6cb4b7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/index.html
@@ -0,0 +1,10 @@
+<html>
+
+<head>
+ <meta http-equiv="refresh" content="0;URL=index.view">
+</head>
+
+<body>
+</body>
+
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/index.jsp b/subsonic-main/src/main/webapp/index.jsp
new file mode 100644
index 00000000..f9f5cd30
--- /dev/null
+++ b/subsonic-main/src/main/webapp/index.jsp
@@ -0,0 +1,10 @@
+<html>
+
+<head>
+ <meta http-equiv="refresh" content="0;URL=index.view">
+</head>
+
+<body>
+</body>
+
+</html> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/AC_OETags.js b/subsonic-main/src/main/webapp/script/AC_OETags.js
new file mode 100644
index 00000000..6482d168
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/AC_OETags.js
@@ -0,0 +1,269 @@
+// Flash Player Version Detection - Rev 1.5
+// Detect Client Browser type
+// Copyright(c) 2005-2006 Adobe Macromedia Software, LLC. All rights reserved.
+var isIE = (navigator.appVersion.indexOf("MSIE") != -1) ? true : false;
+var isWin = (navigator.appVersion.toLowerCase().indexOf("win") != -1) ? true : false;
+var isOpera = (navigator.userAgent.indexOf("Opera") != -1) ? true : false;
+
+function ControlVersion()
+{
+ var version;
+ var axo;
+ var e;
+
+ // NOTE : new ActiveXObject(strFoo) throws an exception if strFoo isn't in the registry
+
+ try {
+ // version will be set for 7.X or greater players
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
+ version = axo.GetVariable("$version");
+ } catch (e) {
+ }
+
+ if (!version)
+ {
+ try {
+ // version will be set for 6.X players only
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
+
+ // installed player is some revision of 6.0
+ // GetVariable("$version") crashes for versions 6.0.22 through 6.0.29,
+ // so we have to be careful.
+
+ // default to the first public version
+ version = "WIN 6,0,21,0";
+
+ // throws if AllowScripAccess does not exist (introduced in 6.0r47)
+ axo.AllowScriptAccess = "always";
+
+ // safe to call for 6.0r47 or greater
+ version = axo.GetVariable("$version");
+
+ } catch (e) {
+ }
+ }
+
+ if (!version)
+ {
+ try {
+ // version will be set for 4.X or 5.X player
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3");
+ version = axo.GetVariable("$version");
+ } catch (e) {
+ }
+ }
+
+ if (!version)
+ {
+ try {
+ // version will be set for 3.X player
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3");
+ version = "WIN 3,0,18,0";
+ } catch (e) {
+ }
+ }
+
+ if (!version)
+ {
+ try {
+ // version will be set for 2.X player
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
+ version = "WIN 2,0,0,11";
+ } catch (e) {
+ version = -1;
+ }
+ }
+
+ return version;
+}
+
+// JavaScript helper required to detect Flash Player PlugIn version information
+function GetSwfVer(){
+ // NS/Opera version >= 3 check for Flash plugin in plugin array
+ var flashVer = -1;
+
+ if (navigator.plugins != null && navigator.plugins.length > 0) {
+ if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) {
+ var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : "";
+ var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description;
+ var descArray = flashDescription.split(" ");
+ var tempArrayMajor = descArray[2].split(".");
+ var versionMajor = tempArrayMajor[0];
+ var versionMinor = tempArrayMajor[1];
+ if ( descArray[3] != "" ) {
+ tempArrayMinor = descArray[3].split("r");
+ } else {
+ tempArrayMinor = descArray[4].split("r");
+ }
+ var versionRevision = tempArrayMinor[1] > 0 ? tempArrayMinor[1] : 0;
+ var flashVer = versionMajor + "." + versionMinor + "." + versionRevision;
+ }
+ }
+ // MSN/WebTV 2.6 supports Flash 4
+ else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.6") != -1) flashVer = 4;
+ // WebTV 2.5 supports Flash 3
+ else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.5") != -1) flashVer = 3;
+ // older WebTV supports Flash 2
+ else if (navigator.userAgent.toLowerCase().indexOf("webtv") != -1) flashVer = 2;
+ else if ( isIE && isWin && !isOpera ) {
+ flashVer = ControlVersion();
+ }
+ return flashVer;
+}
+
+// When called with reqMajorVer, reqMinorVer, reqRevision returns true if that version or greater is available
+function DetectFlashVer(reqMajorVer, reqMinorVer, reqRevision)
+{
+ versionStr = GetSwfVer();
+ if (versionStr == -1 ) {
+ return false;
+ } else if (versionStr != 0) {
+ if(isIE && isWin && !isOpera) {
+ // Given "WIN 2,0,0,11"
+ tempArray = versionStr.split(" "); // ["WIN", "2,0,0,11"]
+ tempString = tempArray[1]; // "2,0,0,11"
+ versionArray = tempString.split(","); // ['2', '0', '0', '11']
+ } else {
+ versionArray = versionStr.split(".");
+ }
+ var versionMajor = versionArray[0];
+ var versionMinor = versionArray[1];
+ var versionRevision = versionArray[2];
+
+ // is the major.revision >= requested major.revision AND the minor version >= requested minor
+ if (versionMajor > parseFloat(reqMajorVer)) {
+ return true;
+ } else if (versionMajor == parseFloat(reqMajorVer)) {
+ if (versionMinor > parseFloat(reqMinorVer))
+ return true;
+ else if (versionMinor == parseFloat(reqMinorVer)) {
+ if (versionRevision >= parseFloat(reqRevision))
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+function AC_AddExtension(src, ext)
+{
+ if (src.indexOf('?') != -1)
+ return src.replace(/\?/, ext+'?');
+ else
+ return src + ext;
+}
+
+function AC_Generateobj(objAttrs, params, embedAttrs)
+{
+ var str = '';
+ if (isIE && isWin && !isOpera)
+ {
+ str += '<object ';
+ for (var i in objAttrs)
+ str += i + '="' + objAttrs[i] + '" ';
+ for (var i in params)
+ str += '><param name="' + i + '" value="' + params[i] + '" /> ';
+ str += '></object>';
+ } else {
+ str += '<embed ';
+ for (var i in embedAttrs)
+ str += i + '="' + embedAttrs[i] + '" ';
+ str += '> </embed>';
+ }
+
+ document.write(str);
+}
+
+function AC_FL_RunContent(){
+ var ret =
+ AC_GetArgs
+ ( arguments, ".swf", "movie", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
+ , "application/x-shockwave-flash"
+ );
+ AC_Generateobj(ret.objAttrs, ret.params, ret.embedAttrs);
+}
+
+function AC_GetArgs(args, ext, srcParamName, classid, mimeType){
+ var ret = new Object();
+ ret.embedAttrs = new Object();
+ ret.params = new Object();
+ ret.objAttrs = new Object();
+ for (var i=0; i < args.length; i=i+2){
+ var currArg = args[i].toLowerCase();
+
+ switch (currArg){
+ case "classid":
+ break;
+ case "pluginspage":
+ ret.embedAttrs[args[i]] = args[i+1];
+ break;
+ case "src":
+ case "movie":
+ args[i+1] = AC_AddExtension(args[i+1], ext);
+ ret.embedAttrs["src"] = args[i+1];
+ ret.params[srcParamName] = args[i+1];
+ break;
+ case "onafterupdate":
+ case "onbeforeupdate":
+ case "onblur":
+ case "oncellchange":
+ case "onclick":
+ case "ondblClick":
+ case "ondrag":
+ case "ondragend":
+ case "ondragenter":
+ case "ondragleave":
+ case "ondragover":
+ case "ondrop":
+ case "onfinish":
+ case "onfocus":
+ case "onhelp":
+ case "onmousedown":
+ case "onmouseup":
+ case "onmouseover":
+ case "onmousemove":
+ case "onmouseout":
+ case "onkeypress":
+ case "onkeydown":
+ case "onkeyup":
+ case "onload":
+ case "onlosecapture":
+ case "onpropertychange":
+ case "onreadystatechange":
+ case "onrowsdelete":
+ case "onrowenter":
+ case "onrowexit":
+ case "onrowsinserted":
+ case "onstart":
+ case "onscroll":
+ case "onbeforeeditfocus":
+ case "onactivate":
+ case "onbeforedeactivate":
+ case "ondeactivate":
+ case "type":
+ case "codebase":
+ ret.objAttrs[args[i]] = args[i+1];
+ break;
+ case "id":
+ case "width":
+ case "height":
+ case "align":
+ case "vspace":
+ case "hspace":
+ case "class":
+ case "title":
+ case "accesskey":
+ case "name":
+ case "tabindex":
+ ret.embedAttrs[args[i]] = ret.objAttrs[args[i]] = args[i+1];
+ break;
+ default:
+ ret.embedAttrs[args[i]] = ret.params[args[i]] = args[i+1];
+ }
+ }
+ ret.objAttrs["classid"] = classid;
+ if (mimeType) ret.embedAttrs["type"] = mimeType;
+ return ret;
+}
+
+
diff --git a/subsonic-main/src/main/webapp/script/builder.js b/subsonic-main/src/main/webapp/script/builder.js
new file mode 100644
index 00000000..dba8beca
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/builder.js
@@ -0,0 +1,136 @@
+// script.aculo.us builder.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008
+
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+var Builder = {
+ NODEMAP: {
+ AREA: 'map',
+ CAPTION: 'table',
+ COL: 'table',
+ COLGROUP: 'table',
+ LEGEND: 'fieldset',
+ OPTGROUP: 'select',
+ OPTION: 'select',
+ PARAM: 'object',
+ TBODY: 'table',
+ TD: 'table',
+ TFOOT: 'table',
+ TH: 'table',
+ THEAD: 'table',
+ TR: 'table'
+ },
+ // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+ // due to a Firefox bug
+ node: function(elementName) {
+ elementName = elementName.toUpperCase();
+
+ // try innerHTML approach
+ var parentTag = this.NODEMAP[elementName] || 'div';
+ var parentElement = document.createElement(parentTag);
+ try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+ parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+ } catch(e) {}
+ var element = parentElement.firstChild || null;
+
+ // see if browser added wrapping tags
+ if(element && (element.tagName.toUpperCase() != elementName))
+ element = element.getElementsByTagName(elementName)[0];
+
+ // fallback to createElement approach
+ if(!element) element = document.createElement(elementName);
+
+ // abort if nothing could be created
+ if(!element) return;
+
+ // attributes (or text)
+ if(arguments[1])
+ if(this._isStringOrNumber(arguments[1]) ||
+ (arguments[1] instanceof Array) ||
+ arguments[1].tagName) {
+ this._children(element, arguments[1]);
+ } else {
+ var attrs = this._attributes(arguments[1]);
+ if(attrs.length) {
+ try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+ parentElement.innerHTML = "<" +elementName + " " +
+ attrs + "></" + elementName + ">";
+ } catch(e) {}
+ element = parentElement.firstChild || null;
+ // workaround firefox 1.0.X bug
+ if(!element) {
+ element = document.createElement(elementName);
+ for(attr in arguments[1])
+ element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+ }
+ if(element.tagName.toUpperCase() != elementName)
+ element = parentElement.getElementsByTagName(elementName)[0];
+ }
+ }
+
+ // text, or array of children
+ if(arguments[2])
+ this._children(element, arguments[2]);
+
+ return $(element);
+ },
+ _text: function(text) {
+ return document.createTextNode(text);
+ },
+
+ ATTR_MAP: {
+ 'className': 'class',
+ 'htmlFor': 'for'
+ },
+
+ _attributes: function(attributes) {
+ var attrs = [];
+ for(attribute in attributes)
+ attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
+ '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
+ return attrs.join(" ");
+ },
+ _children: function(element, children) {
+ if(children.tagName) {
+ element.appendChild(children);
+ return;
+ }
+ if(typeof children=='object') { // array can hold nodes and text
+ children.flatten().each( function(e) {
+ if(typeof e=='object')
+ element.appendChild(e);
+ else
+ if(Builder._isStringOrNumber(e))
+ element.appendChild(Builder._text(e));
+ });
+ } else
+ if(Builder._isStringOrNumber(children))
+ element.appendChild(Builder._text(children));
+ },
+ _isStringOrNumber: function(param) {
+ return(typeof param=='string' || typeof param=='number');
+ },
+ build: function(html) {
+ var element = this.node('div');
+ $(element).update(html.strip());
+ return element.down();
+ },
+ dump: function(scope) {
+ if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope
+
+ var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
+ "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
+ "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
+ "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
+ "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
+ "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
+
+ tags.each( function(tag){
+ scope[tag] = function() {
+ return Builder.node.apply(Builder, [tag].concat($A(arguments)));
+ };
+ });
+ }
+}; \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/controls.js b/subsonic-main/src/main/webapp/script/controls.js
new file mode 100644
index 00000000..c56ccb79
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/controls.js
@@ -0,0 +1,965 @@
+// script.aculo.us controls.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008
+
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+// Richard Livsey
+// Rahul Bhargava
+// Rob Wills
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+if(typeof Effect == 'undefined')
+ throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element);
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ if(this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = options || { };
+
+ this.options.paramName = this.options.paramName || this.element.name;
+ this.options.tokens = this.options.tokens || [];
+ this.options.frequency = this.options.frequency || 0.4;
+ this.options.minChars = this.options.minChars || 1;
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
+ Position.clone(element, update, {
+ setHeight: false,
+ offsetTop: element.offsetHeight
+ });
+ }
+ Effect.Appear(update,{duration:0.15});
+ };
+ this.options.onHide = this.options.onHide ||
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+ if(typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (Prototype.Browser.IE) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+ this.iefix.style.zIndex = 1;
+ this.update.style.zIndex = 2;
+ Element.show(this.iefix);
+ },
+
+ hide: function() {
+ this.stopIndicator();
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+ if(this.iefix) Element.hide(this.iefix);
+ },
+
+ startIndicator: function() {
+ if(this.options.indicator) Element.show(this.options.indicator);
+ },
+
+ stopIndicator: function() {
+ if(this.options.indicator) Element.hide(this.options.indicator);
+ },
+
+ onKeyPress: function(event) {
+ if(this.active)
+ switch(event.keyCode) {
+ case Event.KEY_TAB:
+ case Event.KEY_RETURN:
+ this.selectEntry();
+ Event.stop(event);
+ case Event.KEY_ESC:
+ this.hide();
+ this.active = false;
+ Event.stop(event);
+ return;
+ case Event.KEY_LEFT:
+ case Event.KEY_RIGHT:
+ return;
+ case Event.KEY_UP:
+ this.markPrevious();
+ this.render();
+ Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
+
+ this.changed = true;
+ this.hasFocus = true;
+
+ if(this.observer) clearTimeout(this.observer);
+ this.observer =
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+ },
+
+ activate: function() {
+ this.changed = false;
+ this.hasFocus = true;
+ this.getUpdatedChoices();
+ },
+
+ onHover: function(event) {
+ var element = Event.findElement(event, 'LI');
+ if(this.index != element.autocompleteIndex)
+ {
+ this.index = element.autocompleteIndex;
+ this.render();
+ }
+ Event.stop(event);
+ },
+
+ onClick: function(event) {
+ var element = Event.findElement(event, 'LI');
+ this.index = element.autocompleteIndex;
+ this.selectEntry();
+ this.hide();
+ },
+
+ onBlur: function(event) {
+ // needed to make click events working
+ setTimeout(this.hide.bind(this), 250);
+ this.hasFocus = false;
+ this.active = false;
+ },
+
+ render: function() {
+ if(this.entryCount > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--;
+ else this.index = this.entryCount-1;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < this.entryCount-1) this.index++;
+ else this.index = 0;
+ this.getEntry(this.index).scrollIntoView(false);
+ },
+
+ getEntry: function(index) {
+ return this.update.firstChild.childNodes[index];
+ },
+
+ getCurrentEntry: function() {
+ return this.getEntry(this.index);
+ },
+
+ selectEntry: function() {
+ this.active = false;
+ this.updateElement(this.getCurrentEntry());
+ },
+
+ updateElement: function(selectedElement) {
+ if (this.options.updateElement) {
+ this.options.updateElement(selectedElement);
+ return;
+ }
+ var value = '';
+ if (this.options.select) {
+ var nodes = $(selectedElement).select('.' + this.options.select) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
+ } else {
+ this.element.value = value;
+ }
+ this.oldElementValue = this.element.value;
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+ this.index = 0;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ this.render();
+ }
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ this.tokenBounds = null;
+ if(this.getToken().length>=this.options.minChars) {
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ this.oldElementValue = this.element.value;
+ },
+
+ getToken: function() {
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
+ }
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
+ }
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, url, options) {
+ this.baseInitialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this);
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ this.startIndicator();
+
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+ setTimeout(function() {
+ Field.activate(field);
+ }, 1);
+};
+
+Ajax.InPlaceEditor = Class.create({
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
+ }
+ if (this.options.externalControl)
+ this.options.externalControl = $(this.options.externalControl);
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+ this.element.title = this.options.clickToEditText;
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
+ }
+ },
+ createEditField: function() {
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
+ var size = this.options.size || this.options.cols || 0;
+ if (0 < size) fld.size = size;
+ } else {
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
+ }
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
+ },
+ getText: function() {
+ return this.element.innerHTML.unescapeHTML();
+ },
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('onFailure', transport);
+ if (this._oldInnerHTML) {
+ this.element.innerHTML = this._oldInnerHTML;
+ this._oldInnerHTML = null;
+ }
+ },
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.url, options);
+ }
+ if (e) Event.stop(e);
+ },
+ leaveEditMode: function() {
+ this.element.removeClassName(this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ if (this.options.externalControl)
+ this.options.externalControl.show();
+ this._saving = false;
+ this._editing = false;
+ this._oldInnerHTML = null;
+ this.triggerCallback('onLeaveEditMode');
+ },
+ leaveHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.removeClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onLeaveHover');
+ },
+ loadExternalText: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this._controls.editor.disabled = true;
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._form.removeClassName(this.options.loadingClassName);
+ var text = transport.responseText;
+ if (this.options.stripLoadedTextTags)
+ text = text.stripTags();
+ this._controls.editor.value = text;
+ this._controls.editor.disabled = false;
+ this.postProcessEditField();
+ }.bind(this),
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+ postProcessEditField: function() {
+ var fpc = this.options.fieldPostCreation;
+ if (fpc)
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+ },
+ prepareOptions: function() {
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+ Object.extend(this.options, defs);
+ }.bind(this));
+ },
+ prepareSubmission: function() {
+ this._saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ registerListeners: function() {
+ this._listeners = { };
+ var listener;
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+ listener = this[pair.value].bind(this);
+ this._listeners[pair.key] = listener;
+ if (!this.options.externalControlOnly)
+ this.element.observe(pair.key, listener);
+ if (this.options.externalControl)
+ this.options.externalControl.observe(pair.key, listener);
+ }.bind(this));
+ },
+ removeForm: function() {
+ if (!this._form) return;
+ this._form.remove();
+ this._form = null;
+ this._controls = { };
+ },
+ showSaving: function() {
+ this._oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ this.element.addClassName(this.options.savingClassName);
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ },
+ triggerCallback: function(cbName, arg) {
+ if ('function' == typeof this.options[cbName]) {
+ this.options[cbName](this, arg);
+ }
+ },
+ unregisterListeners: function() {
+ $H(this._listeners).each(function(pair) {
+ if (!this.options.externalControlOnly)
+ this.element.stopObserving(pair.key, pair.value);
+ if (this.options.externalControl)
+ this.options.externalControl.stopObserving(pair.key, pair.value);
+ }.bind(this));
+ },
+ wrapUp: function(transport) {
+ this.leaveEditMode();
+ // Can't use triggerCallback due to backward compatibility: requires
+ // binding + direct element
+ this._boundComplete(transport, this.element);
+ }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+ dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+ initialize: function($super, element, url, options) {
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+ $super(element, url, options);
+ },
+
+ createEditField: function() {
+ var list = document.createElement('select');
+ list.name = this.options.paramName;
+ list.size = 1;
+ this._controls.editor = list;
+ this._collection = this.options.collection || [];
+ if (this.options.loadCollectionURL)
+ this.loadCollection();
+ else
+ this.checkForExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+
+ loadCollection: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this.showLoadingText(this.options.loadingCollectionText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ var js = transport.responseText.strip();
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+ throw('Server returned an invalid collection representation.');
+ this._collection = eval(js);
+ this.checkForExternalText();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadCollectionURL, options);
+ },
+
+ showLoadingText: function(text) {
+ this._controls.editor.disabled = true;
+ var tempOption = this._controls.editor.firstChild;
+ if (!tempOption) {
+ tempOption = document.createElement('option');
+ tempOption.value = '';
+ this._controls.editor.appendChild(tempOption);
+ tempOption.selected = true;
+ }
+ tempOption.update((text || '').stripScripts().stripTags());
+ },
+
+ checkForExternalText: function() {
+ this._text = this.getText();
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ else
+ this.buildOptionList();
+ },
+
+ loadExternalText: function() {
+ this.showLoadingText(this.options.loadingText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._text = transport.responseText.strip();
+ this.buildOptionList();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+
+ buildOptionList: function() {
+ this._form.removeClassName(this.options.loadingClassName);
+ this._collection = this._collection.map(function(entry) {
+ return 2 === entry.length ? entry : [entry, entry].flatten();
+ });
+ var marker = ('value' in this.options) ? this.options.value : this._text;
+ var textFound = this._collection.any(function(entry) {
+ return entry[0] == marker;
+ }.bind(this));
+ this._controls.editor.update('');
+ var option;
+ this._collection.each(function(entry, index) {
+ option = document.createElement('option');
+ option.value = entry[0];
+ option.selected = textFound ? entry[0] == marker : 0 == index;
+ option.appendChild(document.createTextNode(entry[1]));
+ this._controls.editor.appendChild(option);
+ }.bind(this));
+ this._controls.editor.disabled = false;
+ Field.scrollFreeActivate(this._controls.editor);
+ }
+});
+
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only exists for a while, in order to let ****
+//**** users adapt to the new API. Read up on the new ****
+//**** API and convert your code to it ASAP! ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+ if (!options) return;
+ function fallback(name, expr) {
+ if (name in options || expr === undefined) return;
+ options[name] = expr;
+ };
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+ options.okLink == options.okButton == false ? false : undefined)));
+ fallback('highlightColor', options.highlightcolor);
+ fallback('highlightEndColor', options.highlightendcolor);
+};
+
+Object.extend(Ajax.InPlaceEditor, {
+ DefaultOptions: {
+ ajaxOptions: { },
+ autoRows: 3, // Use when multi-line w/ rows == 1
+ cancelControl: 'link', // 'link'|'button'|false
+ cancelText: 'cancel',
+ clickToEditText: 'Click to edit',
+ externalControl: null, // id|elt
+ externalControlOnly: false,
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
+ formClassName: 'inplaceeditor-form',
+ formId: null, // id|elt
+ highlightColor: '#ffff99',
+ highlightEndColor: '#ffffff',
+ hoverClassName: '',
+ htmlResponse: true,
+ loadingClassName: 'inplaceeditor-loading',
+ loadingText: 'Loading...',
+ okControl: 'button', // 'link'|'button'|false
+ okText: 'ok',
+ paramName: 'value',
+ rows: 1, // If 1 and multi-line, uses autoRows
+ savingClassName: 'inplaceeditor-saving',
+ savingText: 'Saving...',
+ size: 0,
+ stripLoadedTextTags: false,
+ submitOnBlur: false,
+ textAfterControls: '',
+ textBeforeControls: '',
+ textBetweenControls: ''
+ },
+ DefaultCallbacks: {
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ onComplete: function(transport, element) {
+ // For backward compatibility, this one is bound to the IPE, and passes
+ // the element directly. It was too often customized, so we don't break it.
+ new Effect.Highlight(element, {
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
+ },
+ onEnterEditMode: null,
+ onEnterHover: function(ipe) {
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
+ if (ipe._effect)
+ ipe._effect.cancel();
+ },
+ onFailure: function(transport, ipe) {
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
+ },
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+ onLeaveEditMode: null,
+ onLeaveHover: function(ipe) {
+ ipe._effect = new Effect.Highlight(ipe.element, {
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
+ });
+ }
+ },
+ Listeners: {
+ click: 'enterEditMode',
+ keydown: 'checkForEscapeOrReturn',
+ mouseover: 'enterHover',
+ mouseout: 'leaveHover'
+ }
+});
+
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+ loadingCollectionText: 'Loading options...'
+};
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create({
+ initialize: function(element, delay, callback) {
+ this.delay = delay || 0.5;
+ this.element = $(element);
+ this.callback = callback;
+ this.timer = null;
+ this.lastValue = $F(this.element);
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+ },
+ delayedListener: function(event) {
+ if(this.lastValue == $F(this.element)) return;
+ if(this.timer) clearTimeout(this.timer);
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+ this.lastValue = $F(this.element);
+ },
+ onTimerEvent: function() {
+ this.timer = null;
+ this.callback(this.element, $F(this.element));
+ }
+}); \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/effects.js b/subsonic-main/src/main/webapp/script/effects.js
new file mode 100644
index 00000000..f31a81a0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/effects.js
@@ -0,0 +1,1130 @@
+// script.aculo.us effects.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008
+
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+// Justin Palmer (http://encytemedia.com/)
+// Mark Pilgrim (http://diveintomark.org/)
+// Martin Bialasinki
+//
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// converts rgb() and #xxx to #xxxxxx format,
+// returns self (or first argument) if not convertable
+String.prototype.parseColor = function() {
+ var color = '#';
+ if (this.slice(0,4) == 'rgb(') {
+ var cols = this.slice(4,this.length-1).split(',');
+ var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
+ } else {
+ if (this.slice(0,1) == '#') {
+ if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if (this.length==7) color = this.toLowerCase();
+ }
+ }
+ return (color.length==7 ? color : (arguments[0] || this));
+};
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+ }).flatten().join('');
+};
+
+Element.collectTextNodesIgnoreClass = function(element, className) {
+ return $A($(element).childNodes).collect( function(node) {
+ return (node.nodeType==3 ? node.nodeValue :
+ ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+};
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if (Prototype.Browser.WebKit) window.scrollBy(0,0);
+ return element;
+};
+
+Element.getInlineOpacity = function(element){
+ return $(element).style.opacity || '';
+};
+
+Element.forceRerendering = function(element) {
+ try {
+ element = $(element);
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ Transitions: {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + .5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
+ return pos > 1 ? 1 : pos;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
+ },
+ pulse: function(pos, pulses) {
+ return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
+ },
+ spring: function(pos) {
+ return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+ },
+ DefaultOptions: {
+ duration: 1.0, // seconds
+ fps: 100, // 100= assume 66fps max.
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+ },
+ tagifyText: function(element) {
+ var tagifyStyle = 'position:relative';
+ if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if (child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ new Element('span', {style: tagifyStyle}).update(
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if (((typeof element == 'object') ||
+ Object.isFunction(element)) &&
+ (element.length))
+ elements = element;
+ else
+ elements = $(element).childNodes;
+
+ var options = Object.extend({
+ speed: 0.1,
+ delay: 0.0
+ }, arguments[2] || { });
+ var masterDelay = options.delay;
+
+ $A(elements).each( function(element, index) {
+ new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+ });
+ },
+ PAIRS: {
+ 'slide': ['SlideDown','SlideUp'],
+ 'blind': ['BlindDown','BlindUp'],
+ 'appear': ['Appear','Fade']
+ },
+ toggle: function(element, effect) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+ var options = Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, arguments[2] || { });
+ Effect[element.visible() ?
+ Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+ }
+};
+
+Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create(Enumerable, {
+ initialize: function() {
+ this.effects = [];
+ this.interval = null;
+ },
+ _each: function(iterator) {
+ this.effects._each(iterator);
+ },
+ add: function(effect) {
+ var timestamp = new Date().getTime();
+
+ var position = Object.isString(effect.options.queue) ?
+ effect.options.queue : effect.options.queue.position;
+
+ switch(position) {
+ case 'front':
+ // move unstarted effects after this effect
+ this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+ e.startOn += effect.finishOn;
+ e.finishOn += effect.finishOn;
+ });
+ break;
+ case 'with-last':
+ timestamp = this.effects.pluck('startOn').max() || timestamp;
+ break;
+ case 'end':
+ // start effect after last queued effect has finished
+ timestamp = this.effects.pluck('finishOn').max() || timestamp;
+ break;
+ }
+
+ effect.startOn += timestamp;
+ effect.finishOn += timestamp;
+
+ if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if (!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
+ },
+ remove: function(effect) {
+ this.effects = this.effects.reject(function(e) { return e==effect });
+ if (this.effects.length == 0) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+ },
+ loop: function() {
+ var timePos = new Date().getTime();
+ for(var i=0, len=this.effects.length;i<len;i++)
+ this.effects[i] && this.effects[i].loop(timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if (!Object.isString(queueName)) return queueName;
+
+ return this.instances.get(queueName) ||
+ this.instances.set(queueName, new Effect.ScopedQueue());
+ }
+};
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.Base = Class.create({
+ position: null,
+ start: function(options) {
+ function codeForEvent(options,eventName){
+ return (
+ (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
+ (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
+ );
+ }
+ if (options && options.transition === false) options.transition = Effect.Transitions.linear;
+ this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
+ this.currentFrame = 0;
+ this.state = 'idle';
+ this.startOn = this.options.delay*1000;
+ this.finishOn = this.startOn+(this.options.duration*1000);
+ this.fromToDelta = this.options.to-this.options.from;
+ this.totalTime = this.finishOn-this.startOn;
+ this.totalFrames = this.options.fps*this.options.duration;
+
+ this.render = (function() {
+ function dispatch(effect, eventName) {
+ if (effect.options[eventName + 'Internal'])
+ effect.options[eventName + 'Internal'](effect);
+ if (effect.options[eventName])
+ effect.options[eventName](effect);
+ }
+
+ return function(pos) {
+ if (this.state === "idle") {
+ this.state = "running";
+ dispatch(this, 'beforeSetup');
+ if (this.setup) this.setup();
+ dispatch(this, 'afterSetup');
+ }
+ if (this.state === "running") {
+ pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
+ this.position = pos;
+ dispatch(this, 'beforeUpdate');
+ if (this.update) this.update(pos);
+ dispatch(this, 'afterUpdate');
+ }
+ };
+ })();
+
+ this.event('beforeStart');
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if (timePos >= this.startOn) {
+ if (timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if (this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / this.totalTime,
+ frame = (pos * this.totalFrames).round();
+ if (frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ cancel: function() {
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).remove(this);
+ this.state = 'finished';
+ },
+ event: function(eventName) {
+ if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if (this.options[eventName]) this.options[eventName](this);
+ },
+ inspect: function() {
+ var data = $H();
+ for(property in this)
+ if (!Object.isFunction(this[property])) data.set(property, this[property]);
+ return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+});
+
+Effect.Parallel = Class.create(Effect.Base, {
+ initialize: function(effects) {
+ this.effects = effects || [];
+ this.start(arguments[1]);
+ },
+ update: function(position) {
+ this.effects.invoke('render', position);
+ },
+ finish: function(position) {
+ this.effects.each( function(effect) {
+ effect.render(1.0);
+ effect.cancel();
+ effect.event('beforeFinish');
+ if (effect.finish) effect.finish(position);
+ effect.event('afterFinish');
+ });
+ }
+});
+
+Effect.Tween = Class.create(Effect.Base, {
+ initialize: function(object, from, to) {
+ object = Object.isString(object) ? $(object) : object;
+ var args = $A(arguments), method = args.last(),
+ options = args.length == 5 ? args[3] : null;
+ this.method = Object.isFunction(method) ? method.bind(object) :
+ Object.isFunction(object[method]) ? object[method].bind(object) :
+ function(value) { object[method] = value };
+ this.start(Object.extend({ from: from, to: to }, options || { }));
+ },
+ update: function(position) {
+ this.method(position);
+ }
+});
+
+Effect.Event = Class.create(Effect.Base, {
+ initialize: function() {
+ this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ var options = Object.extend({
+ from: this.element.getOpacity() || 0.0,
+ to: 1.0
+ }, arguments[1] || { });
+ this.start(options);
+ },
+ update: function(position) {
+ this.element.setOpacity(position);
+ }
+});
+
+Effect.Move = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ x: 0,
+ y: 0,
+ mode: 'relative'
+ }, arguments[1] || { });
+ this.start(options);
+ },
+ setup: function() {
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if (this.options.mode == 'absolute') {
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: (this.options.x * position + this.originalLeft).round() + 'px',
+ top: (this.options.y * position + this.originalTop).round() + 'px'
+ });
+ }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+ return new Effect.Move(element,
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
+};
+
+Effect.Scale = Class.create(Effect.Base, {
+ initialize: function(element, percent) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ scaleX: true,
+ scaleY: true,
+ scaleContent: true,
+ scaleFromCenter: false,
+ scaleMode: 'box', // 'box' or 'contents' or { } with provided values
+ scaleFrom: 100.0,
+ scaleTo: percent
+ }, arguments[2] || { });
+ this.start(options);
+ },
+ setup: function() {
+ this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+ this.elementPositioning = this.element.getStyle('position');
+
+ this.originalStyle = { };
+ ['top','left','width','height','fontSize'].each( function(k) {
+ this.originalStyle[k] = this.element.style[k];
+ }.bind(this));
+
+ this.originalTop = this.element.offsetTop;
+ this.originalLeft = this.element.offsetLeft;
+
+ var fontSize = this.element.getStyle('font-size') || '100%';
+ ['em','px','%','pt'].each( function(fontSizeType) {
+ if (fontSize.indexOf(fontSizeType)>0) {
+ this.fontSize = parseFloat(fontSize);
+ this.fontSizeType = fontSizeType;
+ }
+ }.bind(this));
+
+ this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+
+ this.dims = null;
+ if (this.options.scaleMode=='box')
+ this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+ if (/^content/.test(this.options.scaleMode))
+ this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+ if (!this.dims)
+ this.dims = [this.options.scaleMode.originalHeight,
+ this.options.scaleMode.originalWidth];
+ },
+ update: function(position) {
+ var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+ if (this.options.scaleContent && this.fontSize)
+ this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+ this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+ },
+ finish: function(position) {
+ if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+ },
+ setDimensions: function(height, width) {
+ var d = { };
+ if (this.options.scaleX) d.width = width.round() + 'px';
+ if (this.options.scaleY) d.height = height.round() + 'px';
+ if (this.options.scaleFromCenter) {
+ var topd = (height - this.dims[0])/2;
+ var leftd = (width - this.dims[1])/2;
+ if (this.elementPositioning == 'absolute') {
+ if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
+ if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+ } else {
+ if (this.options.scaleY) d.top = -topd + 'px';
+ if (this.options.scaleX) d.left = -leftd + 'px';
+ }
+ }
+ this.element.setStyle(d);
+ }
+});
+
+Effect.Highlight = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
+ this.start(options);
+ },
+ setup: function() {
+ // Prevent executing on elements not in the layout flow
+ if (this.element.getStyle('display')=='none') { this.cancel(); return; }
+ // Disable background image during the effect
+ this.oldStyle = { };
+ if (!this.options.keepBackgroundImage) {
+ this.oldStyle.backgroundImage = this.element.getStyle('background-image');
+ this.element.setStyle({backgroundImage: 'none'});
+ }
+ if (!this.options.endcolor)
+ this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+ if (!this.options.restorecolor)
+ this.options.restorecolor = this.element.getStyle('background-color');
+ // init color calculations
+ this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+ this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+ },
+ update: function(position) {
+ this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = function(element) {
+ var options = arguments[1] || { },
+ scrollOffsets = document.viewport.getScrollOffsets(),
+ elementOffsets = $(element).cumulativeOffset();
+
+ if (options.offset) elementOffsets[1] += options.offset;
+
+ return new Effect.Tween(null,
+ scrollOffsets.top,
+ elementOffsets[1],
+ options,
+ function(p){ scrollTo(scrollOffsets.left, p.round()); }
+ );
+};
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ var options = Object.extend({
+ from: element.getOpacity() || 1.0,
+ to: 0.0,
+ afterFinishInternal: function(effect) {
+ if (effect.options.to!=0) return;
+ effect.element.hide().setStyle({opacity: oldOpacity});
+ }
+ }, arguments[1] || { });
+ return new Effect.Opacity(element,options);
+};
+
+Effect.Appear = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+ to: 1.0,
+ // force Safari to render floated elements properly
+ afterFinishInternal: function(effect) {
+ effect.element.forceRerendering();
+ },
+ beforeSetup: function(effect) {
+ effect.element.setOpacity(effect.options.from).show();
+ }}, arguments[1] || { });
+ return new Effect.Opacity(element,options);
+};
+
+Effect.Puff = function(element) {
+ element = $(element);
+ var oldStyle = {
+ opacity: element.getInlineOpacity(),
+ position: element.getStyle('position'),
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height
+ };
+ return new Effect.Parallel(
+ [ new Effect.Scale(element, 200,
+ { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
+ Object.extend({ duration: 1.0,
+ beforeSetupInternal: function(effect) {
+ Position.absolutize(effect.effects[0].element);
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().setStyle(oldStyle); }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.BlindUp = function(element) {
+ element = $(element);
+ element.makeClipping();
+ return new Effect.Scale(element, 0,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ restoreAfterFinish: true,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.BlindDown = function(element) {
+ element = $(element);
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: 0,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping();
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.SwitchOff = function(element) {
+ element = $(element);
+ var oldOpacity = element.getInlineOpacity();
+ return new Effect.Appear(element, Object.extend({
+ duration: 0.4,
+ from: 0,
+ transition: Effect.Transitions.flicker,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(effect.element, 1, {
+ duration: 0.3, scaleFromCenter: true,
+ scaleX: false, scaleContent: false, restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+ }
+ });
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.DropOut = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left'),
+ opacity: element.getInlineOpacity() };
+ return new Effect.Parallel(
+ [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
+ new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+ Object.extend(
+ { duration: 0.5,
+ beforeSetup: function(effect) {
+ effect.effects[0].element.makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+ }
+ }, arguments[1] || { }));
+};
+
+Effect.Shake = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ distance: 20,
+ duration: 0.5
+ }, arguments[1] || {});
+ var distance = parseFloat(options.distance);
+ var split = parseFloat(options.duration) / 10.0;
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ effect.element.undoPositioned().setStyle(oldStyle);
+ }}); }}); }}); }}); }}); }});
+};
+
+Effect.SlideDown = function(element) {
+ element = $(element).cleanWhitespace();
+ // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, 100, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ scaleFrom: window.opera ? 0 : 1,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if (window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().setStyle({height: '0px'}).show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+ }, arguments[1] || { })
+ );
+};
+
+Effect.SlideUp = function(element) {
+ element = $(element).cleanWhitespace();
+ var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+ restoreAfterFinish: true,
+ afterSetup: function(effect) {
+ effect.element.makePositioned();
+ effect.element.down().makePositioned();
+ if (window.opera) effect.element.setStyle({top: ''});
+ effect.element.makeClipping().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
+ }
+ }, arguments[1] || { })
+ );
+};
+
+// Bug in opera makes the TD containing this element expand for a instance after finish
+Effect.Squish = function(element) {
+ return new Effect.Scale(element, window.opera ? 1 : 0, {
+ restoreAfterFinish: true,
+ beforeSetup: function(effect) {
+ effect.element.makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping();
+ }
+ });
+};
+
+Effect.Grow = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.full
+ }, arguments[1] || { });
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var initialMoveX, initialMoveY;
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ initialMoveX = initialMoveY = moveX = moveY = 0;
+ break;
+ case 'top-right':
+ initialMoveX = dims.width;
+ initialMoveY = moveY = 0;
+ moveX = -dims.width;
+ break;
+ case 'bottom-left':
+ initialMoveX = moveX = 0;
+ initialMoveY = dims.height;
+ moveY = -dims.height;
+ break;
+ case 'bottom-right':
+ initialMoveX = dims.width;
+ initialMoveY = dims.height;
+ moveX = -dims.width;
+ moveY = -dims.height;
+ break;
+ case 'center':
+ initialMoveX = dims.width / 2;
+ initialMoveY = dims.height / 2;
+ moveX = -dims.width / 2;
+ moveY = -dims.height / 2;
+ break;
+ }
+
+ return new Effect.Move(element, {
+ x: initialMoveX,
+ y: initialMoveY,
+ duration: 0.01,
+ beforeSetup: function(effect) {
+ effect.element.hide().makeClipping().makePositioned();
+ },
+ afterFinishInternal: function(effect) {
+ new Effect.Parallel(
+ [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+ new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+ new Effect.Scale(effect.element, 100, {
+ scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
+ sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+ ], Object.extend({
+ beforeSetup: function(effect) {
+ effect.effects[0].element.setStyle({height: '0px'}).show();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
+ }
+ }, options)
+ );
+ }
+ });
+};
+
+Effect.Shrink = function(element) {
+ element = $(element);
+ var options = Object.extend({
+ direction: 'center',
+ moveTransition: Effect.Transitions.sinoidal,
+ scaleTransition: Effect.Transitions.sinoidal,
+ opacityTransition: Effect.Transitions.none
+ }, arguments[1] || { });
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ height: element.style.height,
+ width: element.style.width,
+ opacity: element.getInlineOpacity() };
+
+ var dims = element.getDimensions();
+ var moveX, moveY;
+
+ switch (options.direction) {
+ case 'top-left':
+ moveX = moveY = 0;
+ break;
+ case 'top-right':
+ moveX = dims.width;
+ moveY = 0;
+ break;
+ case 'bottom-left':
+ moveX = 0;
+ moveY = dims.height;
+ break;
+ case 'bottom-right':
+ moveX = dims.width;
+ moveY = dims.height;
+ break;
+ case 'center':
+ moveX = dims.width / 2;
+ moveY = dims.height / 2;
+ break;
+ }
+
+ return new Effect.Parallel(
+ [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+ new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+ new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+ ], Object.extend({
+ beforeStartInternal: function(effect) {
+ effect.effects[0].element.makePositioned().makeClipping();
+ },
+ afterFinishInternal: function(effect) {
+ effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+ }, options)
+ );
+};
+
+Effect.Pulsate = function(element) {
+ element = $(element);
+ var options = arguments[1] || { },
+ oldOpacity = element.getInlineOpacity(),
+ transition = options.transition || Effect.Transitions.linear,
+ reverser = function(pos){
+ return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
+ };
+
+ return new Effect.Opacity(element,
+ Object.extend(Object.extend({ duration: 2.0, from: 0,
+ afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+ }, options), {transition: reverser}));
+};
+
+Effect.Fold = function(element) {
+ element = $(element);
+ var oldStyle = {
+ top: element.style.top,
+ left: element.style.left,
+ width: element.style.width,
+ height: element.style.height };
+ element.makeClipping();
+ return new Effect.Scale(element, 5, Object.extend({
+ scaleContent: false,
+ scaleX: false,
+ afterFinishInternal: function(effect) {
+ new Effect.Scale(element, 1, {
+ scaleContent: false,
+ scaleY: false,
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().setStyle(oldStyle);
+ } });
+ }}, arguments[1] || { }));
+};
+
+Effect.Morph = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: { }
+ }, arguments[1] || { });
+
+ if (!Object.isString(options.style)) this.style = $H(options.style);
+ else {
+ if (options.style.include(':'))
+ this.style = options.style.parseStyle();
+ else {
+ this.element.addClassName(options.style);
+ this.style = $H(this.element.getStyles());
+ this.element.removeClassName(options.style);
+ var css = this.element.getStyles();
+ this.style = this.style.reject(function(style) {
+ return style.value == css[style.key];
+ });
+ options.afterFinishInternal = function(effect) {
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ effect.element.style[transform.style] = '';
+ });
+ };
+ }
+ }
+ this.start(options);
+ },
+
+ setup: function(){
+ function parseColor(color){
+ if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ color = color.parseColor();
+ return $R(0,2).map(function(i){
+ return parseInt( color.slice(i*2+1,i*2+3), 16 );
+ });
+ }
+ this.transforms = this.style.map(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if (value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if (property == 'opacity') {
+ value = parseFloat(value);
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if (Element.CSS_LENGTH.test(value)) {
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+ value = parseFloat(components[1]);
+ unit = (components.length == 3) ? components[2] : null;
+ }
+
+ var originalValue = this.element.getStyle(property);
+ return {
+ style: property.camelize(),
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ };
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ );
+ });
+ },
+ update: function(position) {
+ var style = { }, transform, i = this.transforms.length;
+ while(i--)
+ style[(transform = this.transforms[i]).style] =
+ transform.unit=='color' ? '#'+
+ (Math.round(transform.originalValue[0]+
+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+ (Math.round(transform.originalValue[1]+
+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+ (Math.round(transform.originalValue[2]+
+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+ (transform.originalValue +
+ (transform.targetValue - transform.originalValue) * position).toFixed(3) +
+ (transform.unit === null ? '' : transform.unit);
+ this.element.setStyle(style, true);
+ }
+});
+
+Effect.Transform = Class.create({
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || { };
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ track = $H(track);
+ var data = track.values().first();
+ this.tracks.push($H({
+ ids: track.keys().first(),
+ effect: Effect.Morph,
+ options: { style: data }
+ }));
+ }.bind(this));
+ return this;
+ },
+ play: function(){
+ return new Effect.Parallel(
+ this.tracks.map(function(track){
+ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
+ var elements = [$(ids) || $$(ids)].flatten();
+ return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.__parseStyleElement = document.createElement('div');
+String.prototype.parseStyle = function(){
+ var style, styleRules = $H();
+ if (Prototype.Browser.WebKit)
+ style = new Element('div',{style:this}).style;
+ else {
+ String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
+ style = String.__parseStyleElement.childNodes[0].style;
+ }
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if (style[property]) styleRules.set(property, style[property]);
+ });
+
+ if (Prototype.Browser.IE && this.include('opacity'))
+ styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
+
+ return styleRules;
+};
+
+if (document.defaultView && document.defaultView.getComputedStyle) {
+ Element.getStyles = function(element) {
+ var css = document.defaultView.getComputedStyle($(element), null);
+ return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
+ styles[property] = css[property];
+ return styles;
+ });
+ };
+} else {
+ Element.getStyles = function(element) {
+ element = $(element);
+ var css = element.currentStyle, styles;
+ styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
+ results[property] = css[property];
+ return results;
+ });
+ if (!styles.opacity) styles.opacity = element.getOpacity();
+ return styles;
+ };
+}
+
+Effect.Methods = {
+ morph: function(element, style) {
+ element = $(element);
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
+ return element;
+ },
+ visualEffect: function(element, effect, options) {
+ element = $(element);
+ var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[klass](element, options);
+ return element;
+ },
+ highlight: function(element, options) {
+ element = $(element);
+ new Effect.Highlight(element, options);
+ return element;
+ }
+};
+
+$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
+ 'pulsate shake puff squish switchOff dropOut').each(
+ function(effect) {
+ Effect.Methods[effect] = function(element, options){
+ element = $(element);
+ Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
+ return element;
+ };
+ }
+);
+
+$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
+ function(f) { Effect.Methods[f] = Element[f]; }
+);
+
+Element.addMethods(Effect.Methods); \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/FancyZoom.js b/subsonic-main/src/main/webapp/script/fancyzoom/FancyZoom.js
new file mode 100644
index 00000000..c98624fa
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/FancyZoom.js
@@ -0,0 +1,761 @@
+// FancyZoom.js - v1.1 - http://www.fancyzoom.com
+//
+// Copyright (c) 2008 Cabel Sasser / Panic Inc
+// All rights reserved.
+//
+// Requires: FancyZoomHTML.js
+// Instructions: Include JS files in page, call setupZoom() in onLoad. That's it!
+// Any <a href> links to images will be updated to zoom inline.
+// Add rel="nozoom" to your <a href> to disable zooming for an image.
+//
+// Redistribution and use of this effect in source form, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// * USE OF SOURCE ON COMMERCIAL (FOR-PROFIT) WEBSITE REQUIRES ONE-TIME LICENSE FEE PER DOMAIN.
+// Reasonably priced! Visit www.fancyzoom.com for licensing instructions. Thanks!
+//
+// * Non-commercial (personal) website use is permitted without license/payment!
+//
+// * Redistribution of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+//
+// * Redistribution of source code and derived works cannot be sold without specific
+// written prior permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+var includeCaption = true; // Turn on the "caption" feature, and write out the caption HTML
+var zoomTime = 5; // Milliseconds between frames of zoom animation
+var zoomSteps = 15; // Number of zoom animation frames
+var includeFade = 1; // Set to 1 to fade the image in / out as it zooms
+var minBorder = 90; // Amount of padding between large, scaled down images, and the window edges
+var shadowSettings = '0px 5px 25px rgba(0, 0, 0, '; // Blur, radius, color of shadow for compatible browsers
+
+// Location of the zoom and shadow images
+var zoomImagesURI;
+
+// Init. Do not add anything below this line, unless it's something awesome.
+
+var myWidth = 0, myHeight = 0, myScroll = 0; myScrollWidth = 0; myScrollHeight = 0;
+var zoomOpen = false, preloadFrame = 1, preloadActive = false, preloadTime = 0, imgPreload = new Image();
+var preloadAnimTimer = 0;
+
+var zoomActive = new Array(); var zoomTimer = new Array();
+var zoomOrigW = new Array(); var zoomOrigH = new Array();
+var zoomOrigX = new Array(); var zoomOrigY = new Array();
+
+var zoomID = "ZoomBox";
+var theID = "ZoomImage";
+var zoomCaption = "ZoomCaption";
+var zoomCaptionDiv = "ZoomCapDiv";
+
+if (navigator.userAgent.indexOf("MSIE") != -1) {
+ var browserIsIE = true;
+}
+
+// Zoom: Setup The Page! Called in your <body>'s onLoad handler.
+
+function setupZoom(baseURI) {
+ zoomImagesURI = baseURI + 'script/fancyzoom/images/';
+
+ prepZooms();
+ insertZoomHTML();
+ zoomdiv = document.getElementById(zoomID);
+ zoomimg = document.getElementById(theID);
+}
+
+// Zoom: Inject Javascript functions into hrefs with a "zoom" rel.
+// This is done at page load time via an onLoad() handler.
+
+function prepZooms() {
+ if (! document.getElementsByTagName) {
+ return;
+ }
+ var links = document.getElementsByTagName("a");
+ for (i = 0; i < links.length; i++) {
+ if (links[i].getAttribute("href")) {
+ if (links[i].getAttribute("rel") == "zoom") {
+ links[i].onclick = function (event) { return zoomClick(this, event); };
+ links[i].onmouseover = function () { zoomPreload(this); };
+ }
+ }
+ }
+}
+
+// Zoom: Load an image into an image object. When done loading, function sets preloadActive to false,
+// so other bits know that they can proceed with the zoom.
+// Preloaded image is stored in imgPreload and swapped out in the zoom function.
+
+function zoomPreload(from) {
+
+ var theimage = from.getAttribute("href");
+
+ // Only preload if we have to, i.e. the image isn't this image already
+
+ if (imgPreload.src.indexOf(from.getAttribute("href").substr(from.getAttribute("href").lastIndexOf("/"))) == -1) {
+ preloadActive = true;
+ imgPreload = new Image();
+
+ // Set a function to fire when the preload is complete, setting flags along the way.
+
+ imgPreload.onload = function() {
+ preloadActive = false;
+ }
+
+ // Load it!
+ imgPreload.src = theimage;
+ }
+}
+
+// Zoom: Start the preloading animation cycle.
+
+function preloadAnimStart() {
+ preloadTime = new Date();
+ document.getElementById("ZoomSpin").style.left = (myWidth / 2) + 'px';
+ document.getElementById("ZoomSpin").style.top = ((myHeight / 2) + myScroll) + 'px';
+ document.getElementById("ZoomSpin").style.visibility = "visible";
+ preloadFrame = 1;
+ document.getElementById("SpinImage").src = zoomImagesURI+'zoom-spin-'+preloadFrame+'.png';
+ preloadAnimTimer = setInterval("preloadAnim()", 100);
+}
+
+// Zoom: Display and ANIMATE the jibber-jabber widget. Once preloadActive is false, bail and zoom it up!
+
+function preloadAnim(from) {
+ if (preloadActive != false) {
+ document.getElementById("SpinImage").src = zoomImagesURI+'zoom-spin-'+preloadFrame+'.png';
+ preloadFrame++;
+ if (preloadFrame > 12) preloadFrame = 1;
+ } else {
+ document.getElementById("ZoomSpin").style.visibility = "hidden";
+ clearInterval(preloadAnimTimer);
+ preloadAnimTimer = 0;
+ zoomIn(preloadFrom);
+ }
+}
+
+// ZOOM CLICK: We got a click! Should we do the zoom? Or wait for the preload to complete?
+// todo?: Double check that imgPreload src = clicked src
+
+function zoomClick(from, evt) {
+
+ var shift = getShift(evt);
+
+ // Check for Command / Alt key. If pressed, pass them through -- don't zoom!
+ if (! evt && window.event && (window.event.metaKey || window.event.altKey)) {
+ return true;
+ } else if (evt && (evt.metaKey|| evt.altKey)) {
+ return true;
+ }
+
+ // Get browser dimensions
+ getSize();
+
+ // If preloading still, wait, and display the spinner.
+ if (preloadActive == true) {
+ // But only display the spinner if it's not already being displayed!
+ if (preloadAnimTimer == 0) {
+ preloadFrom = from;
+ preloadAnimStart();
+ }
+ } else {
+ // Otherwise, we're loaded: do the zoom!
+ zoomIn(from, shift);
+ }
+
+ return false;
+
+}
+
+// Zoom: Move an element in to endH endW, using zoomHost as a starting point.
+// "from" is an object reference to the href that spawned the zoom.
+
+function zoomIn(from, shift) {
+
+ zoomimg.src = from.getAttribute("href");
+
+ // Determine the zoom settings from where we came from, the element in the <a>.
+ // If there's no element in the <a>, or we can't get the width, make stuff up
+
+ if (from.childNodes[0].width) {
+ startW = from.childNodes[0].width;
+ startH = from.childNodes[0].height;
+ startPos = findElementPos(from.childNodes[0]);
+ } else {
+ startW = 50;
+ startH = 12;
+ startPos = findElementPos(from);
+ }
+
+ hostX = startPos[0];
+ hostY = startPos[1];
+
+ // Make up for a scrolled containing div.
+ // TODO: This HAS to move into findElementPos.
+
+ if (document.getElementById('scroller')) {
+ hostX = hostX - document.getElementById('scroller').scrollLeft;
+ }
+
+ // Determine the target zoom settings from the preloaded image object
+
+ endW = imgPreload.width;
+ endH = imgPreload.height;
+
+ // Start! But only if we're not zooming already!
+
+ if (zoomActive[theID] != true) {
+
+ // Clear everything out just in case something is already open
+
+ if (document.getElementById("ShadowBox")) {
+ document.getElementById("ShadowBox").style.visibility = "hidden";
+ } else if (! browserIsIE) {
+
+ // Wipe timer if shadow is fading in still
+ if (fadeActive["ZoomImage"]) {
+ clearInterval(fadeTimer["ZoomImage"]);
+ fadeActive["ZoomImage"] = false;
+ fadeTimer["ZoomImage"] = false;
+ }
+
+ document.getElementById("ZoomImage").style.webkitBoxShadow = shadowSettings + '0.0)';
+ }
+
+ document.getElementById("ZoomClose").style.visibility = "hidden";
+
+ // Setup the CAPTION, if existing. Hide it first, set the text.
+
+ if (includeCaption) {
+ document.getElementById(zoomCaptionDiv).style.visibility = "hidden";
+ if (from.getAttribute('title') && includeCaption) {
+ // Yes, there's a caption, set it up
+ document.getElementById(zoomCaption).innerHTML = from.getAttribute('title');
+ } else {
+ document.getElementById(zoomCaption).innerHTML = "";
+ }
+ }
+
+ // Store original position in an array for future zoomOut.
+
+ zoomOrigW[theID] = startW;
+ zoomOrigH[theID] = startH;
+ zoomOrigX[theID] = hostX;
+ zoomOrigY[theID] = hostY;
+
+ // Now set the starting dimensions
+
+ zoomimg.style.width = startW + 'px';
+ zoomimg.style.height = startH + 'px';
+ zoomdiv.style.left = hostX + 'px';
+ zoomdiv.style.top = hostY + 'px';
+
+ // Show the zooming image container, make it invisible
+
+ if (includeFade == 1) {
+ setOpacity(0, zoomID);
+ }
+ zoomdiv.style.visibility = "visible";
+
+ // If it's too big to fit in the window, shrink the width and height to fit (with ratio).
+
+ sizeRatio = endW / endH;
+ if (endW > myWidth - minBorder) {
+ endW = myWidth - minBorder;
+ endH = endW / sizeRatio;
+ }
+ if (endH > myHeight - minBorder) {
+ endH = myHeight - minBorder;
+ endW = endH * sizeRatio;
+ }
+
+ zoomChangeX = ((myWidth / 2) - (endW / 2) - hostX);
+ zoomChangeY = (((myHeight / 2) - (endH / 2) - hostY) + myScroll);
+ zoomChangeW = (endW - startW);
+ zoomChangeH = (endH - startH);
+
+ // Shift key?
+
+ if (shift) {
+ tempSteps = zoomSteps * 7;
+ } else {
+ tempSteps = zoomSteps;
+ }
+
+ // Setup Zoom
+
+ zoomCurrent = 0;
+
+ // Setup Fade with Zoom, If Requested
+
+ if (includeFade == 1) {
+ fadeCurrent = 0;
+ fadeAmount = (0 - 100) / tempSteps;
+ } else {
+ fadeAmount = 0;
+ }
+
+ // Do It!
+
+ zoomTimer[theID] = setInterval("zoomElement('"+zoomID+"', '"+theID+"', "+zoomCurrent+", "+startW+", "+zoomChangeW+", "+startH+", "+zoomChangeH+", "+hostX+", "+zoomChangeX+", "+hostY+", "+zoomChangeY+", "+tempSteps+", "+includeFade+", "+fadeAmount+", 'zoomDoneIn(zoomID)')", zoomTime);
+ zoomActive[theID] = true;
+ }
+}
+
+// Zoom it back out.
+
+function zoomOut(from, evt) {
+
+ // Get shift key status.
+ // IE events don't seem to get passed through the function, so grab it from the window.
+
+ if (getShift(evt)) {
+ tempSteps = zoomSteps * 7;
+ } else {
+ tempSteps = zoomSteps;
+ }
+
+ // Check to see if something is happening/open
+
+ if (zoomActive[theID] != true) {
+
+ // First, get rid of the shadow if necessary.
+
+ if (document.getElementById("ShadowBox")) {
+ document.getElementById("ShadowBox").style.visibility = "hidden";
+ } else if (! browserIsIE) {
+
+ // Wipe timer if shadow is fading in still
+ if (fadeActive["ZoomImage"]) {
+ clearInterval(fadeTimer["ZoomImage"]);
+ fadeActive["ZoomImage"] = false;
+ fadeTimer["ZoomImage"] = false;
+ }
+
+ document.getElementById("ZoomImage").style.webkitBoxShadow = shadowSettings + '0.0)';
+ }
+
+ // ..and the close box...
+
+ document.getElementById("ZoomClose").style.visibility = "hidden";
+
+ // ...and the caption if necessary!
+
+ if (includeCaption && document.getElementById(zoomCaption).innerHTML != "") {
+ // fadeElementSetup(zoomCaptionDiv, 100, 0, 5, 1);
+ document.getElementById(zoomCaptionDiv).style.visibility = "hidden";
+ }
+
+ // Now, figure out where we came from, to get back there
+
+ startX = parseInt(zoomdiv.style.left);
+ startY = parseInt(zoomdiv.style.top);
+ startW = zoomimg.width;
+ startH = zoomimg.height;
+ zoomChangeX = zoomOrigX[theID] - startX;
+ zoomChangeY = zoomOrigY[theID] - startY;
+ zoomChangeW = zoomOrigW[theID] - startW;
+ zoomChangeH = zoomOrigH[theID] - startH;
+
+ // Setup Zoom
+
+ zoomCurrent = 0;
+
+ // Setup Fade with Zoom, If Requested
+
+ if (includeFade == 1) {
+ fadeCurrent = 0;
+ fadeAmount = (100 - 0) / tempSteps;
+ } else {
+ fadeAmount = 0;
+ }
+
+ // Do It!
+
+ zoomTimer[theID] = setInterval("zoomElement('"+zoomID+"', '"+theID+"', "+zoomCurrent+", "+startW+", "+zoomChangeW+", "+startH+", "+zoomChangeH+", "+startX+", "+zoomChangeX+", "+startY+", "+zoomChangeY+", "+tempSteps+", "+includeFade+", "+fadeAmount+", 'zoomDone(zoomID, theID)')", zoomTime);
+ zoomActive[theID] = true;
+ }
+}
+
+// Finished Zooming In
+
+function zoomDoneIn(zoomdiv, theID) {
+
+ // Note that it's open
+
+ zoomOpen = true;
+ zoomdiv = document.getElementById(zoomdiv);
+
+ // Position the table shadow behind the zoomed in image, and display it
+
+ if (document.getElementById("ShadowBox")) {
+
+ setOpacity(0, "ShadowBox");
+ shadowdiv = document.getElementById("ShadowBox");
+
+ shadowLeft = parseInt(zoomdiv.style.left) - 13;
+ shadowTop = parseInt(zoomdiv.style.top) - 8;
+ shadowWidth = zoomdiv.offsetWidth + 26;
+ shadowHeight = zoomdiv.offsetHeight + 26;
+
+ shadowdiv.style.width = shadowWidth + 'px';
+ shadowdiv.style.height = shadowHeight + 'px';
+ shadowdiv.style.left = shadowLeft + 'px';
+ shadowdiv.style.top = shadowTop + 'px';
+
+ document.getElementById("ShadowBox").style.visibility = "visible";
+ fadeElementSetup("ShadowBox", 0, 100, 5);
+
+ } else if (! browserIsIE) {
+ // Or, do a fade of the modern shadow
+ fadeElementSetup("ZoomImage", 0, .8, 5, 0, "shadow");
+ }
+
+ // Position and display the CAPTION, if existing
+
+ if (includeCaption && document.getElementById(zoomCaption).innerHTML != "") {
+ // setOpacity(0, zoomCaptionDiv);
+ zoomcapd = document.getElementById(zoomCaptionDiv);
+ zoomcapd.style.top = parseInt(zoomdiv.style.top) + (zoomdiv.offsetHeight + 15) + 'px';
+ zoomcapd.style.left = (myWidth / 2) - (zoomcapd.offsetWidth / 2) + 'px';
+ zoomcapd.style.visibility = "visible";
+ // fadeElementSetup(zoomCaptionDiv, 0, 100, 5);
+ }
+
+ // Display Close Box (fade it if it's not IE)
+
+ if (!browserIsIE) setOpacity(0, "ZoomClose");
+ document.getElementById("ZoomClose").style.visibility = "visible";
+ if (!browserIsIE) fadeElementSetup("ZoomClose", 0, 100, 5);
+
+ // Get keypresses
+ document.onkeypress = getKey;
+
+}
+
+// Finished Zooming Out
+
+function zoomDone(zoomdiv, theID) {
+
+ // No longer open
+
+ zoomOpen = false;
+
+ // Clear stuff out, clean up
+
+ zoomOrigH[theID] = "";
+ zoomOrigW[theID] = "";
+ document.getElementById(zoomdiv).style.visibility = "hidden";
+ zoomActive[theID] == false;
+
+ // Stop getting keypresses
+
+ document.onkeypress = null;
+
+}
+
+// Actually zoom the element
+
+function zoomElement(zoomdiv, theID, zoomCurrent, zoomStartW, zoomChangeW, zoomStartH, zoomChangeH, zoomStartX, zoomChangeX, zoomStartY, zoomChangeY, zoomSteps, includeFade, fadeAmount, execWhenDone) {
+
+ // console.log("Zooming Step #"+zoomCurrent+ " of "+zoomSteps+" (zoom " + zoomStartW + "/" + zoomChangeW + ") (zoom " + zoomStartH + "/" + zoomChangeH + ") (zoom " + zoomStartX + "/" + zoomChangeX + ") (zoom " + zoomStartY + "/" + zoomChangeY + ") Fade: "+fadeAmount);
+
+ // Test if we're done, or if we continue
+
+ if (zoomCurrent == (zoomSteps + 1)) {
+ zoomActive[theID] = false;
+ clearInterval(zoomTimer[theID]);
+
+ if (execWhenDone != "") {
+ eval(execWhenDone);
+ }
+ } else {
+
+ // Do the Fade!
+
+ if (includeFade == 1) {
+ if (fadeAmount < 0) {
+ setOpacity(Math.abs(zoomCurrent * fadeAmount), zoomdiv);
+ } else {
+ setOpacity(100 - (zoomCurrent * fadeAmount), zoomdiv);
+ }
+ }
+
+ // Calculate this step's difference, and move it!
+
+ moveW = cubicInOut(zoomCurrent, zoomStartW, zoomChangeW, zoomSteps);
+ moveH = cubicInOut(zoomCurrent, zoomStartH, zoomChangeH, zoomSteps);
+ moveX = cubicInOut(zoomCurrent, zoomStartX, zoomChangeX, zoomSteps);
+ moveY = cubicInOut(zoomCurrent, zoomStartY, zoomChangeY, zoomSteps);
+
+ document.getElementById(zoomdiv).style.left = moveX + 'px';
+ document.getElementById(zoomdiv).style.top = moveY + 'px';
+ zoomimg.style.width = moveW + 'px';
+ zoomimg.style.height = moveH + 'px';
+
+ zoomCurrent++;
+
+ clearInterval(zoomTimer[theID]);
+ zoomTimer[theID] = setInterval("zoomElement('"+zoomdiv+"', '"+theID+"', "+zoomCurrent+", "+zoomStartW+", "+zoomChangeW+", "+zoomStartH+", "+zoomChangeH+", "+zoomStartX+", "+zoomChangeX+", "+zoomStartY+", "+zoomChangeY+", "+zoomSteps+", "+includeFade+", "+fadeAmount+", '"+execWhenDone+"')", zoomTime);
+ }
+}
+
+// Zoom Utility: Get Key Press when image is open, and act accordingly
+
+function getKey(evt) {
+ if (! evt) {
+ theKey = event.keyCode;
+ } else {
+ theKey = evt.keyCode;
+ }
+
+ if (theKey == 27) { // ESC
+ zoomOut(this, evt);
+ }
+}
+
+////////////////////////////
+//
+// FADE Functions
+//
+
+function fadeOut(elem) {
+ if (elem.id) {
+ fadeElementSetup(elem.id, 100, 0, 10);
+ }
+}
+
+function fadeIn(elem) {
+ if (elem.id) {
+ fadeElementSetup(elem.id, 0, 100, 10);
+ }
+}
+
+// Fade: Initialize the fade function
+
+var fadeActive = new Array();
+var fadeQueue = new Array();
+var fadeTimer = new Array();
+var fadeClose = new Array();
+var fadeMode = new Array();
+
+function fadeElementSetup(theID, fdStart, fdEnd, fdSteps, fdClose, fdMode) {
+
+ // alert("Fading: "+theID+" Steps: "+fdSteps+" Mode: "+fdMode);
+
+ if (fadeActive[theID] == true) {
+ // Already animating, queue up this command
+ fadeQueue[theID] = new Array(theID, fdStart, fdEnd, fdSteps);
+ } else {
+ fadeSteps = fdSteps;
+ fadeCurrent = 0;
+ fadeAmount = (fdStart - fdEnd) / fadeSteps;
+ fadeTimer[theID] = setInterval("fadeElement('"+theID+"', '"+fadeCurrent+"', '"+fadeAmount+"', '"+fadeSteps+"')", 15);
+ fadeActive[theID] = true;
+ fadeMode[theID] = fdMode;
+
+ if (fdClose == 1) {
+ fadeClose[theID] = true;
+ } else {
+ fadeClose[theID] = false;
+ }
+ }
+}
+
+// Fade: Do the fade. This function will call itself, modifying the parameters, so
+// many instances can run concurrently. Can fade using opacity, or fade using a box-shadow.
+
+function fadeElement(theID, fadeCurrent, fadeAmount, fadeSteps) {
+
+ if (fadeCurrent == fadeSteps) {
+
+ // We're done, so clear.
+
+ clearInterval(fadeTimer[theID]);
+ fadeActive[theID] = false;
+ fadeTimer[theID] = false;
+
+ // Should we close it once the fade is complete?
+
+ if (fadeClose[theID] == true) {
+ document.getElementById(theID).style.visibility = "hidden";
+ }
+
+ // Hang on.. did a command queue while we were working? If so, make it happen now
+
+ if (fadeQueue[theID] && fadeQueue[theID] != false) {
+ fadeElementSetup(fadeQueue[theID][0], fadeQueue[theID][1], fadeQueue[theID][2], fadeQueue[theID][3]);
+ fadeQueue[theID] = false;
+ }
+ } else {
+
+ fadeCurrent++;
+
+ // Now actually do the fade adjustment.
+
+ if (fadeMode[theID] == "shadow") {
+
+ // Do a special fade on the webkit-box-shadow of the object
+
+ if (fadeAmount < 0) {
+ document.getElementById(theID).style.webkitBoxShadow = shadowSettings + (Math.abs(fadeCurrent * fadeAmount)) + ')';
+ } else {
+ document.getElementById(theID).style.webkitBoxShadow = shadowSettings + (100 - (fadeCurrent * fadeAmount)) + ')';
+ }
+
+ } else {
+
+ // Set the opacity depending on if we're adding or subtracting (pos or neg)
+
+ if (fadeAmount < 0) {
+ setOpacity(Math.abs(fadeCurrent * fadeAmount), theID);
+ } else {
+ setOpacity(100 - (fadeCurrent * fadeAmount), theID);
+ }
+ }
+
+ // Keep going, and send myself the updated variables
+ clearInterval(fadeTimer[theID]);
+ fadeTimer[theID] = setInterval("fadeElement('"+theID+"', '"+fadeCurrent+"', '"+fadeAmount+"', '"+fadeSteps+"')", 15);
+ }
+}
+
+////////////////////////////
+//
+// UTILITY functions
+//
+
+// Utility: Set the opacity, compatible with a number of browsers. Value from 0 to 100.
+
+function setOpacity(opacity, theID) {
+
+ var object = document.getElementById(theID).style;
+
+ // If it's 100, set it to 99 for Firefox.
+
+ if (navigator.userAgent.indexOf("Firefox") != -1) {
+ if (opacity == 100) { opacity = 99.9999; } // This is majorly awkward
+ }
+
+ // Multi-browser opacity setting
+
+ object.filter = "alpha(opacity=" + opacity + ")"; // IE/Win
+ object.opacity = (opacity / 100); // Safari 1.2, Firefox+Mozilla
+
+}
+
+// Utility: Math functions for animation calucations - From http://www.robertpenner.com/easing/
+//
+// t = time, b = begin, c = change, d = duration
+// time = current frame, begin is fixed, change is basically finish - begin, duration is fixed (frames),
+
+function linear(t, b, c, d)
+{
+ return c*t/d + b;
+}
+
+function sineInOut(t, b, c, d)
+{
+ return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
+}
+
+function cubicIn(t, b, c, d) {
+ return c*(t/=d)*t*t + b;
+}
+
+function cubicOut(t, b, c, d) {
+ return c*((t=t/d-1)*t*t + 1) + b;
+}
+
+function cubicInOut(t, b, c, d)
+{
+ if ((t/=d/2) < 1) return c/2*t*t*t + b;
+ return c/2*((t-=2)*t*t + 2) + b;
+}
+
+function bounceOut(t, b, c, d)
+{
+ if ((t/=d) < (1/2.75)){
+ return c*(7.5625*t*t) + b;
+ } else if (t < (2/2.75)){
+ return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
+ } else if (t < (2.5/2.75)){
+ return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
+ } else {
+ return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
+ }
+}
+
+
+// Utility: Get the size of the window, and set myWidth and myHeight
+// Credit to quirksmode.org
+
+function getSize() {
+
+ // Window Size
+
+ if (self.innerHeight) { // Everyone but IE
+ myWidth = window.innerWidth;
+ myHeight = window.innerHeight;
+ myScroll = window.pageYOffset;
+ } else if (document.documentElement && document.documentElement.clientHeight) { // IE6 Strict
+ myWidth = document.documentElement.clientWidth;
+ myHeight = document.documentElement.clientHeight;
+ myScroll = document.documentElement.scrollTop;
+ } else if (document.body) { // Other IE, such as IE7
+ myWidth = document.body.clientWidth;
+ myHeight = document.body.clientHeight;
+ myScroll = document.body.scrollTop;
+ }
+
+ // Page size w/offscreen areas
+
+ if (window.innerHeight && window.scrollMaxY) {
+ myScrollWidth = document.body.scrollWidth;
+ myScrollHeight = window.innerHeight + window.scrollMaxY;
+ } else if (document.body.scrollHeight > document.body.offsetHeight) { // All but Explorer Mac
+ myScrollWidth = document.body.scrollWidth;
+ myScrollHeight = document.body.scrollHeight;
+ } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
+ myScrollWidth = document.body.offsetWidth;
+ myScrollHeight = document.body.offsetHeight;
+ }
+}
+
+// Utility: Get Shift Key Status
+// IE events don't seem to get passed through the function, so grab it from the window.
+
+function getShift(evt) {
+ var shift = false;
+ if (! evt && window.event) {
+ shift = window.event.shiftKey;
+ } else if (evt) {
+ shift = evt.shiftKey;
+ if (shift) evt.stopPropagation(); // Prevents Firefox from doing shifty things
+ }
+ return shift;
+}
+
+// Utility: Find the Y position of an element on a page. Return Y and X as an array
+
+function findElementPos(elemFind)
+{
+ var elemX = 0;
+ var elemY = 0;
+ do {
+ elemX += elemFind.offsetLeft;
+ elemY += elemFind.offsetTop;
+ } while ( elemFind = elemFind.offsetParent )
+
+ return Array(elemX, elemY);
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/FancyZoomHTML.js b/subsonic-main/src/main/webapp/script/fancyzoom/FancyZoomHTML.js
new file mode 100644
index 00000000..7644a9a8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/FancyZoomHTML.js
@@ -0,0 +1,318 @@
+// FancyZoomHTML.js - v1.0
+// Used to draw necessary HTML elements for FancyZoom
+//
+// Copyright (c) 2008 Cabel Sasser / Panic Inc
+// All rights reserved.
+
+function insertZoomHTML() {
+
+ // All of this junk creates the three <div>'s used to hold the closebox, image, and zoom shadow.
+
+ var inBody = document.getElementsByTagName("body").item(0);
+
+ // WAIT SPINNER
+
+ var inSpinbox = document.createElement("div");
+ inSpinbox.setAttribute('id', 'ZoomSpin');
+ inSpinbox.style.position = 'absolute';
+ inSpinbox.style.left = '10px';
+ inSpinbox.style.top = '10px';
+ inSpinbox.style.visibility = 'hidden';
+ inSpinbox.style.zIndex = '525';
+ inBody.insertBefore(inSpinbox, inBody.firstChild);
+
+ var inSpinImage = document.createElement("img");
+ inSpinImage.setAttribute('id', 'SpinImage');
+ inSpinImage.setAttribute('src', zoomImagesURI+'zoom-spin-1.png');
+ inSpinbox.appendChild(inSpinImage);
+
+ // ZOOM IMAGE
+ //
+ // <div id="ZoomBox">
+ // <a href="javascript:zoomOut();"><img src="/images/spacer.gif" id="ZoomImage" border="0"></a> <!-- THE IMAGE -->
+ // <div id="ZoomClose">
+ // <a href="javascript:zoomOut();"><img src="/images/closebox.png" width="30" height="30" border="0"></a>
+ // </div>
+ // </div>
+
+ var inZoombox = document.createElement("div");
+ inZoombox.setAttribute('id', 'ZoomBox');
+
+ inZoombox.style.position = 'absolute';
+ inZoombox.style.left = '10px';
+ inZoombox.style.top = '10px';
+ inZoombox.style.visibility = 'hidden';
+ inZoombox.style.zIndex = '499';
+
+ inBody.insertBefore(inZoombox, inSpinbox.nextSibling);
+
+ var inImage1 = document.createElement("img");
+ inImage1.onclick = function (event) { zoomOut(this, event); return false; };
+ inImage1.setAttribute('src',zoomImagesURI+'spacer.gif');
+ inImage1.setAttribute('id','ZoomImage');
+ inImage1.setAttribute('border', '0');
+ // inImage1.setAttribute('onMouseOver', 'zoomMouseOver();')
+ // inImage1.setAttribute('onMouseOut', 'zoomMouseOut();')
+
+ // This must be set first, so we can later test it using webkitBoxShadow.
+ inImage1.setAttribute('style', '-webkit-box-shadow: '+shadowSettings+'0.0)');
+ inImage1.style.display = 'block';
+ inImage1.style.width = '10px';
+ inImage1.style.height = '10px';
+ inImage1.style.cursor = 'pointer'; // -webkit-zoom-out?
+ inZoombox.appendChild(inImage1);
+
+ var inClosebox = document.createElement("div");
+ inClosebox.setAttribute('id', 'ZoomClose');
+ inClosebox.style.position = 'absolute';
+
+ // In MSIE, we need to put the close box inside the image.
+ // It's 2008 and I'm having to do a browser detect? Sigh.
+ if (browserIsIE) {
+ inClosebox.style.left = '-1px';
+ inClosebox.style.top = '0px';
+ } else {
+ inClosebox.style.left = '-15px';
+ inClosebox.style.top = '-15px';
+ }
+
+ inClosebox.style.visibility = 'hidden';
+ inZoombox.appendChild(inClosebox);
+
+ var inImage2 = document.createElement("img");
+ inImage2.onclick = function (event) { zoomOut(this, event); return false; };
+ inImage2.setAttribute('src',zoomImagesURI+'closebox.png');
+ inImage2.setAttribute('width','30');
+ inImage2.setAttribute('height','30');
+ inImage2.setAttribute('border','0');
+ inImage2.style.cursor = 'pointer';
+ inClosebox.appendChild(inImage2);
+
+ // SHADOW
+ // Only draw the table-based shadow if the programatic webkitBoxShadow fails!
+ // Also, don't draw it if we're IE -- it wouldn't look quite right anyway.
+
+ if (! document.getElementById('ZoomImage').style.webkitBoxShadow && ! browserIsIE) {
+
+ // SHADOW BASE
+
+ var inFixedBox = document.createElement("div");
+ inFixedBox.setAttribute('id', 'ShadowBox');
+ inFixedBox.style.position = 'absolute';
+ inFixedBox.style.left = '50px';
+ inFixedBox.style.top = '50px';
+ inFixedBox.style.width = '100px';
+ inFixedBox.style.height = '100px';
+ inFixedBox.style.visibility = 'hidden';
+ inFixedBox.style.zIndex = '498';
+ inBody.insertBefore(inFixedBox, inZoombox.nextSibling);
+
+ // SHADOW
+ // Now, the shadow table. Skip if not compatible, or irrevelant with -box-shadow.
+
+ // <div id="ShadowBox"><table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0"> X
+ // <tr height="25">
+ // <td width="27"><img src="/images/zoom-shadow1.png" width="27" height="25"></td>
+ // <td background="/images/zoom-shadow2.png">&nbsp;</td>
+ // <td width="27"><img src="/images/zoom-shadow3.png" width="27" height="25"></td>
+ // </tr>
+
+ var inShadowTable = document.createElement("table");
+ inShadowTable.setAttribute('border', '0');
+ inShadowTable.setAttribute('width', '100%');
+ inShadowTable.setAttribute('height', '100%');
+ inShadowTable.setAttribute('cellpadding', '0');
+ inShadowTable.setAttribute('cellspacing', '0');
+ inFixedBox.appendChild(inShadowTable);
+
+ var inShadowTbody = document.createElement("tbody"); // Needed for IE (for HTML4).
+ inShadowTable.appendChild(inShadowTbody);
+
+ var inRow1 = document.createElement("tr");
+ inRow1.style.height = '25px';
+ inShadowTbody.appendChild(inRow1);
+
+ var inCol1 = document.createElement("td");
+ inCol1.style.width = '27px';
+ inRow1.appendChild(inCol1);
+ var inShadowImg1 = document.createElement("img");
+ inShadowImg1.setAttribute('src', zoomImagesURI+'zoom-shadow1.png');
+ inShadowImg1.setAttribute('width', '27');
+ inShadowImg1.setAttribute('height', '25');
+ inShadowImg1.style.display = 'block';
+ inCol1.appendChild(inShadowImg1);
+
+ var inCol2 = document.createElement("td");
+ inCol2.setAttribute('background', zoomImagesURI+'zoom-shadow2.png');
+ inRow1.appendChild(inCol2);
+ // inCol2.innerHTML = '<img src=';
+ var inSpacer1 = document.createElement("img");
+ inSpacer1.setAttribute('src',zoomImagesURI+'spacer.gif');
+ inSpacer1.setAttribute('height', '1');
+ inSpacer1.setAttribute('width', '1');
+ inSpacer1.style.display = 'block';
+ inCol2.appendChild(inSpacer1);
+
+ var inCol3 = document.createElement("td");
+ inCol3.style.width = '27px';
+ inRow1.appendChild(inCol3);
+ var inShadowImg3 = document.createElement("img");
+ inShadowImg3.setAttribute('src', zoomImagesURI+'zoom-shadow3.png');
+ inShadowImg3.setAttribute('width', '27');
+ inShadowImg3.setAttribute('height', '25');
+ inShadowImg3.style.display = 'block';
+ inCol3.appendChild(inShadowImg3);
+
+ // <tr>
+ // <td background="/images/zoom-shadow4.png">&nbsp;</td>
+ // <td bgcolor="#ffffff">&nbsp;</td>
+ // <td background="/images/zoom-shadow5.png">&nbsp;</td>
+ // </tr>
+
+ inRow2 = document.createElement("tr");
+ inShadowTbody.appendChild(inRow2);
+
+ var inCol4 = document.createElement("td");
+ inCol4.setAttribute('background', zoomImagesURI+'zoom-shadow4.png');
+ inRow2.appendChild(inCol4);
+ // inCol4.innerHTML = '&nbsp;';
+ var inSpacer2 = document.createElement("img");
+ inSpacer2.setAttribute('src',zoomImagesURI+'spacer.gif');
+ inSpacer2.setAttribute('height', '1');
+ inSpacer2.setAttribute('width', '1');
+ inSpacer2.style.display = 'block';
+ inCol4.appendChild(inSpacer2);
+
+ var inCol5 = document.createElement("td");
+ inCol5.setAttribute('bgcolor', '#ffffff');
+ inRow2.appendChild(inCol5);
+ // inCol5.innerHTML = '&nbsp;';
+ var inSpacer3 = document.createElement("img");
+ inSpacer3.setAttribute('src',zoomImagesURI+'spacer.gif');
+ inSpacer3.setAttribute('height', '1');
+ inSpacer3.setAttribute('width', '1');
+ inSpacer3.style.display = 'block';
+ inCol5.appendChild(inSpacer3);
+
+ var inCol6 = document.createElement("td");
+ inCol6.setAttribute('background', zoomImagesURI+'zoom-shadow5.png');
+ inRow2.appendChild(inCol6);
+ // inCol6.innerHTML = '&nbsp;';
+ var inSpacer4 = document.createElement("img");
+ inSpacer4.setAttribute('src',zoomImagesURI+'spacer.gif');
+ inSpacer4.setAttribute('height', '1');
+ inSpacer4.setAttribute('width', '1');
+ inSpacer4.style.display = 'block';
+ inCol6.appendChild(inSpacer4);
+
+ // <tr height="26">
+ // <td width="27"><img src="/images/zoom-shadow6.png" width="27" height="26"</td>
+ // <td background="/images/zoom-shadow7.png">&nbsp;</td>
+ // <td width="27"><img src="/images/zoom-shadow8.png" width="27" height="26"></td>
+ // </tr>
+ // </table>
+
+ var inRow3 = document.createElement("tr");
+ inRow3.style.height = '26px';
+ inShadowTbody.appendChild(inRow3);
+
+ var inCol7 = document.createElement("td");
+ inCol7.style.width = '27px';
+ inRow3.appendChild(inCol7);
+ var inShadowImg7 = document.createElement("img");
+ inShadowImg7.setAttribute('src', zoomImagesURI+'zoom-shadow6.png');
+ inShadowImg7.setAttribute('width', '27');
+ inShadowImg7.setAttribute('height', '26');
+ inShadowImg7.style.display = 'block';
+ inCol7.appendChild(inShadowImg7);
+
+ var inCol8 = document.createElement("td");
+ inCol8.setAttribute('background', zoomImagesURI+'zoom-shadow7.png');
+ inRow3.appendChild(inCol8);
+ // inCol8.innerHTML = '&nbsp;';
+ var inSpacer5 = document.createElement("img");
+ inSpacer5.setAttribute('src',zoomImagesURI+'spacer.gif');
+ inSpacer5.setAttribute('height', '1');
+ inSpacer5.setAttribute('width', '1');
+ inSpacer5.style.display = 'block';
+ inCol8.appendChild(inSpacer5);
+
+ var inCol9 = document.createElement("td");
+ inCol9.style.width = '27px';
+ inRow3.appendChild(inCol9);
+ var inShadowImg9 = document.createElement("img");
+ inShadowImg9.setAttribute('src', zoomImagesURI+'zoom-shadow8.png');
+ inShadowImg9.setAttribute('width', '27');
+ inShadowImg9.setAttribute('height', '26');
+ inShadowImg9.style.display = 'block';
+ inCol9.appendChild(inShadowImg9);
+ }
+
+ if (includeCaption) {
+
+ // CAPTION
+ //
+ // <div id="ZoomCapDiv" style="margin-left: 13px; margin-right: 13px;">
+ // <table border="1" cellpadding="0" cellspacing="0">
+ // <tr height="26">
+ // <td><img src="zoom-caption-l.png" width="13" height="26"></td>
+ // <td rowspan="3" background="zoom-caption-fill.png"><div id="ZoomCaption"></div></td>
+ // <td><img src="zoom-caption-r.png" width="13" height="26"></td>
+ // </tr>
+ // </table>
+ // </div>
+
+ var inCapDiv = document.createElement("div");
+ inCapDiv.setAttribute('id', 'ZoomCapDiv');
+ inCapDiv.style.position = 'absolute';
+ inCapDiv.style.visibility = 'hidden';
+ inCapDiv.style.marginLeft = 'auto';
+ inCapDiv.style.marginRight = 'auto';
+ inCapDiv.style.zIndex = '501';
+
+ inBody.insertBefore(inCapDiv, inZoombox.nextSibling);
+
+ var inCapTable = document.createElement("table");
+ inCapTable.setAttribute('border', '0');
+ inCapTable.setAttribute('cellPadding', '0'); // Wow. These honestly need to
+ inCapTable.setAttribute('cellSpacing', '0'); // be intercapped to work in IE. WTF?
+ inCapDiv.appendChild(inCapTable);
+
+ var inTbody = document.createElement("tbody"); // Needed for IE (for HTML4).
+ inCapTable.appendChild(inTbody);
+
+ var inCapRow1 = document.createElement("tr");
+ inTbody.appendChild(inCapRow1);
+
+ var inCapCol1 = document.createElement("td");
+ inCapCol1.setAttribute('align', 'right');
+ inCapRow1.appendChild(inCapCol1);
+ var inCapImg1 = document.createElement("img");
+ inCapImg1.setAttribute('src', zoomImagesURI+'zoom-caption-l.png');
+ inCapImg1.setAttribute('width', '13');
+ inCapImg1.setAttribute('height', '26');
+ inCapImg1.style.display = 'block';
+ inCapCol1.appendChild(inCapImg1);
+
+ var inCapCol2 = document.createElement("td");
+ inCapCol2.setAttribute('background', zoomImagesURI+'zoom-caption-fill.png');
+ inCapCol2.setAttribute('id', 'ZoomCaption');
+ inCapCol2.setAttribute('valign', 'middle');
+ inCapCol2.style.fontSize = '14px';
+ inCapCol2.style.fontFamily = 'Helvetica';
+ inCapCol2.style.fontWeight = 'bold';
+ inCapCol2.style.color = '#ffffff';
+ inCapCol2.style.textShadow = '0px 2px 4px #000000';
+ inCapCol2.style.whiteSpace = 'nowrap';
+ inCapRow1.appendChild(inCapCol2);
+
+ var inCapCol3 = document.createElement("td");
+ inCapRow1.appendChild(inCapCol3);
+ var inCapImg2 = document.createElement("img");
+ inCapImg2.setAttribute('src', zoomImagesURI+'zoom-caption-r.png');
+ inCapImg2.setAttribute('width', '13');
+ inCapImg2.setAttribute('height', '26');
+ inCapImg2.style.display = 'block';
+ inCapCol3.appendChild(inCapImg2);
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/closebox.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/closebox.png
new file mode 100644
index 00000000..4de4396d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/closebox.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/spacer.gif b/subsonic-main/src/main/webapp/script/fancyzoom/images/spacer.gif
new file mode 100644
index 00000000..5bfd67a2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/spacer.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-fill.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-fill.png
new file mode 100644
index 00000000..1e341533
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-fill.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-l.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-l.png
new file mode 100644
index 00000000..a63ea481
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-l.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-r.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-r.png
new file mode 100644
index 00000000..15980d58
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-caption-r.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow1.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow1.png
new file mode 100644
index 00000000..8b48000b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow1.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow2.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow2.png
new file mode 100644
index 00000000..09209f37
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow2.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow3.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow3.png
new file mode 100644
index 00000000..7636fec2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow3.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow4.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow4.png
new file mode 100644
index 00000000..c7f148d9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow4.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow5.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow5.png
new file mode 100644
index 00000000..2a75b82a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow5.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow6.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow6.png
new file mode 100644
index 00000000..65801aa0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow6.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow7.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow7.png
new file mode 100644
index 00000000..cb447608
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow7.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow8.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow8.png
new file mode 100644
index 00000000..f1c6acdb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-shadow8.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-1.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-1.png
new file mode 100644
index 00000000..5615629c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-1.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-10.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-10.png
new file mode 100644
index 00000000..77595dbd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-10.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-11.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-11.png
new file mode 100644
index 00000000..c2147d56
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-11.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-12.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-12.png
new file mode 100644
index 00000000..cf027248
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-12.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-2.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-2.png
new file mode 100644
index 00000000..95eaae89
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-2.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-3.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-3.png
new file mode 100644
index 00000000..9e2b9cb3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-3.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-4.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-4.png
new file mode 100644
index 00000000..a39c0fc0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-4.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-5.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-5.png
new file mode 100644
index 00000000..80bea63b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-5.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-6.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-6.png
new file mode 100644
index 00000000..b962e5f7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-6.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-7.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-7.png
new file mode 100644
index 00000000..9b6e489f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-7.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-8.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-8.png
new file mode 100644
index 00000000..fe147d5f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-8.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-9.png b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-9.png
new file mode 100644
index 00000000..b321b1c7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/fancyzoom/images/zoom-spin-9.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/jquery-1.7.1.min.js b/subsonic-main/src/main/webapp/script/jquery-1.7.1.min.js
new file mode 100644
index 00000000..198b3ff0
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/jquery-1.7.1.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.7.1 jquery.com | jquery.org/license */
+(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function cb(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function ca(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bE.test(a)?d(a,e):ca(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)ca(a+"["+e+"]",b[e],c,d);else d(a,b)}function b_(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function b$(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bT,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=b$(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=b$(a,c,d,e,"*",g));return l}function bZ(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bP),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bC(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bx:by,g=0,h=e.length;if(d>0){if(c!=="border")for(;g<h;g++)c||(d-=parseFloat(f.css(a,"padding"+e[g]))||0),c==="margin"?d+=parseFloat(f.css(a,c+e[g]))||0:d-=parseFloat(f.css(a,"border"+e[g]+"Width"))||0;return d+"px"}d=bz(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0;if(c)for(;g<h;g++)d+=parseFloat(f.css(a,"padding"+e[g]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+e[g]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+e[g]))||0);return d+"px"}function bp(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c+(i[c][d].namespace?".":"")+i[c][d].namespace,i[c][d],i[c][d].data)}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?m(g):h==="function"&&(!a.unique||!o.has(g))&&c.push(g)},n=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,l=j||0,j=0,k=c.length;for(;c&&l<k;l++)if(c[l].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}i=!1,c&&(a.once?e===!0?o.disable():c=[]:d&&d.length&&(e=d.shift(),o.fireWith(e[0],e[1])))},o={add:function(){if(c){var a=c.length;m(arguments),i?k=c.length:e&&e!==!0&&(j=a,n(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){i&&f<=k&&(k--,f<=l&&l--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&o.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(i?a.once||d.push([b,c]):(!a.once||!e)&&n(b,c));return this},fire:function(){o.fireWith(this,arguments);return this},fired:function(){return!!e}};return o};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p,q=c.createElement("div"),r=c.documentElement;q.setAttribute("className","t"),q.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="<div "+n+"><div></div></div>"+"<table "+n+" cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="<div style='width:4px;'></div>",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h=null;if(typeof a=="undefined"){if(this.length){h=f.data(this[0]);if(this[0].nodeType===1&&!f._data(this[0],"parsedAttrs")){e=this[0].attributes;for(var i=0,j=e.length;i<j;i++)g=e[i].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),l(this[0],g,h[g]));f._data(this[0],"parsedAttrs",!0)}}return h}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split("."),d[1]=d[1]?"."+d[1]:"";if(c===b){h=this.triggerHandler("getData"+d[1]+"!",[d[0]]),h===b&&this.length&&(h=f.data(this[0],a),h=l(this[0],a,h));return h===b&&d[1]?this.data(d[0]):h}return this.each(function(){var b=f(this),e=[d[0],c];b.triggerHandler("setData"+d[1]+"!",e),f.data(this,a,c),b.triggerHandler("changeData"+d[1]+"!",e)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise()}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h<g;h++)e=d[h],e&&(c=f.propFix[e]||e,f.attr(a,e,""),a.removeAttribute(v?e:c),u.test(e)&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};
+f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=[],j,k,l,m,n,o,p,q,r,s,t;g[0]=c,c.delegateTarget=this;if(e&&!c.target.disabled&&(!c.button||c.type!=="click")){m=f(this),m.context=this.ownerDocument||this;for(l=c.target;l!=this;l=l.parentNode||this){o={},q=[],m[0]=l;for(j=0;j<e;j++)r=d[j],s=r.selector,o[s]===b&&(o[s]=r.quick?H(l,r.quick):m.is(s)),o[s]&&q.push(r);q.length&&i.push({elem:l,matches:q})}}d.length>e&&i.push({elem:this,matches:d.slice(e)});for(j=0;j<i.length&&!c.isPropagationStopped();j++){p=i[j],c.currentTarget=p.elem;for(k=0;k<p.matches.length&&!c.isImmediatePropagationStopped();k++){r=p.matches[k];if(h||!c.namespace&&!r.namespace||c.namespace_re&&c.namespace_re.test(r.namespace))c.data=r.data,c.handleObj=r,n=((f.event.special[r.origType]||{}).handle||r.handler).apply(p.elem,g),n!==b&&(c.result=n,n===!1&&(c.preventDefault(),c.stopPropagation()))}}return c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0)}),d._submit_attached=!0)})},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on.call(this,a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.type+"."+e.namespace:e.type,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.POS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function()
+{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bp)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1></$2>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bn(k[i]);else bn(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||be.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bq=/alpha\([^)]*\)/i,br=/opacity=([^)]*)/,bs=/([A-Z]|^ms)/g,bt=/^-?\d+(?:px)?$/i,bu=/^-?\d/,bv=/^([\-+])=([\-+.\de]+)/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Left","Right"],by=["Top","Bottom"],bz,bA,bB;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bz(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bv.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bz)return bz(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bC(a,b,d);f.swap(a,bw,function(){e=bC(a,b,d)});return e}},set:function(a,b){if(!bt.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cv(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cu("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cu("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cv(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cn.test(h)?(o=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),o?(f._data(this,"toggle"+i,o==="show"?"hide":"show"),j[o]()):j[h]()):(k=co.exec(h),l=j.cur(),k?(m=parseFloat(k[2]),n=k[3]||(f.cssNumber[i]?"":"px"),n!=="px"&&(f.style(this,i,(m||1)+n),l=(m||1)/j.cur()*l,f.style(this,i,l+n)),k[1]&&(m=(k[1]==="-="?-1:1)*m+l),j.custom(l,m,n)):j.custom(l,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:cu("show",1),slideUp:cu("hide",1),slideToggle:cu("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cr||cs(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){e.options.hide&&f._data(e.elem,"fxshow"+e.prop)===b&&f._data(e.elem,"fxshow"+e.prop,e.start)},h()&&f.timers.push(h)&&!cp&&(cp=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cr||cs(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cp),cp=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(["width","height"],function(a,b){f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cy(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.support.fixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.support.fixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/jquery-ui-1.8.18.custom.min.js b/subsonic-main/src/main/webapp/script/jquery-ui-1.8.18.custom.min.js
new file mode 100644
index 00000000..f00a62f1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/jquery-ui-1.8.18.custom.min.js
@@ -0,0 +1,356 @@
+/*!
+ * jQuery UI 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI
+ */(function(a,b){function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;if(!b.href||!g||f.nodeName.toLowerCase()!=="map")return!1;h=a("img[usemap=#"+g+"]")[0];return!!h&&d(h)}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}a.ui=a.ui||{};a.ui.version||(a.extend(a.ui,{version:"1.8.18",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a.each(["Width","Height"],function(c,d){function h(b,c,d,f){a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)});return c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){if(c===b)return g["inner"+d].call(this);return this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){if(typeof b!="number")return g["outer"+d].call(this,b);return this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!!d&&!!a.element[0].parentNode)for(var e=0;e<d.length;e++)a.options[d[e][0]]&&d[e][1].apply(a.element,c)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(b,c){if(a(b).css("overflow")==="hidden")return!1;var d=c&&c==="left"?"scrollLeft":"scrollTop",e=!1;if(b[d]>0)return!0;b[d]=1,e=b[d]>0,b[d]=0;return e},isOverAxis:function(a,b,c){return a>b&&a<b+c},isOver:function(b,c,d,e,f,g){return a.ui.isOverAxis(b,d,f)&&a.ui.isOverAxis(c,e,g)}}))})(jQuery);/*!
+ * jQuery UI Widget 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Widget
+ */(function(a,b){if(a.cleanData){var c=a.cleanData;a.cleanData=function(b){for(var d=0,e;(e=b[d])!=null;d++)try{a(e).triggerHandler("remove")}catch(f){}c(b)}}else{var d=a.fn.remove;a.fn.remove=function(b,c){return this.each(function(){c||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){try{a(this).triggerHandler("remove")}catch(b){}});return d.call(a(this),b,c)})}}a.widget=function(b,c,d){var e=b.split(".")[0],f;b=b.split(".")[1],f=e+"-"+b,d||(d=c,c=a.Widget),a.expr[":"][f]=function(c){return!!a.data(c,b)},a[e]=a[e]||{},a[e][b]=function(a,b){arguments.length&&this._createWidget(a,b)};var g=new c;g.options=a.extend(!0,{},g.options),a[e][b].prototype=a.extend(!0,g,{namespace:e,widgetName:b,widgetEventPrefix:a[e][b].prototype.widgetEventPrefix||b,widgetBaseClass:f},d),a.widget.bridge(b,a[e][b])},a.widget.bridge=function(c,d){a.fn[c]=function(e){var f=typeof e=="string",g=Array.prototype.slice.call(arguments,1),h=this;e=!f&&g.length?a.extend.apply(null,[!0,e].concat(g)):e;if(f&&e.charAt(0)==="_")return h;f?this.each(function(){var d=a.data(this,c),f=d&&a.isFunction(d[e])?d[e].apply(d,g):d;if(f!==d&&f!==b){h=f;return!1}}):this.each(function(){var b=a.data(this,c);b?b.option(e||{})._init():a.data(this,c,new d(e,this))});return h}},a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)},a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:!1},_createWidget:function(b,c){a.data(c,this.widgetName,this),this.element=a(c),this.options=a.extend(!0,{},this.options,this._getCreateOptions(),b);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()}),this._create(),this._trigger("create"),this._init()},_getCreateOptions:function(){return a.metadata&&a.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName),this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled "+"ui-state-disabled")},widget:function(){return this.element},option:function(c,d){var e=c;if(arguments.length===0)return a.extend({},this.options);if(typeof c=="string"){if(d===b)return this.options[c];e={},e[c]=d}this._setOptions(e);return this},_setOptions:function(b){var c=this;a.each(b,function(a,b){c._setOption(a,b)});return this},_setOption:function(a,b){this.options[a]=b,a==="disabled"&&this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled"+" "+"ui-state-disabled").attr("aria-disabled",b);return this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_trigger:function(b,c,d){var e,f,g=this.options[b];d=d||{},c=a.Event(c),c.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase(),c.target=this.element[0],f=c.originalEvent;if(f)for(e in f)e in c||(c[e]=f[e]);this.element.trigger(c,d);return!(a.isFunction(g)&&g.call(this.element[0],c,d)===!1||c.isDefaultPrevented())}}})(jQuery);/*!
+ * jQuery UI Mouse 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Mouse
+ *
+ * Depends:
+ * jquery.ui.widget.js
+ */(function(a,b){var c=!1;a(document).mouseup(function(a){c=!1}),a.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var b=this;this.element.bind("mousedown."+this.widgetName,function(a){return b._mouseDown(a)}).bind("click."+this.widgetName,function(c){if(!0===a.data(c.target,b.widgetName+".preventClickEvent")){a.removeData(c.target,b.widgetName+".preventClickEvent"),c.stopImmediatePropagation();return!1}}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName)},_mouseDown:function(b){if(!c){this._mouseStarted&&this._mouseUp(b),this._mouseDownEvent=b;var d=this,e=b.which==1,f=typeof this.options.cancel=="string"&&b.target.nodeName?a(b.target).closest(this.options.cancel).length:!1;if(!e||f||!this._mouseCapture(b))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){d.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)){this._mouseStarted=this._mouseStart(b)!==!1;if(!this._mouseStarted){b.preventDefault();return!0}}!0===a.data(b.target,this.widgetName+".preventClickEvent")&&a.removeData(b.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(a){return d._mouseMove(a)},this._mouseUpDelegate=function(a){return d._mouseUp(a)},a(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),b.preventDefault(),c=!0;return!0}},_mouseMove:function(b){if(a.browser.msie&&!(document.documentMode>=9)&&!b.button)return this._mouseUp(b);if(this._mouseStarted){this._mouseDrag(b);return b.preventDefault()}this._mouseDistanceMet(b)&&this._mouseDelayMet(b)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,b)!==!1,this._mouseStarted?this._mouseDrag(b):this._mouseUp(b));return!this._mouseStarted},_mouseUp:function(b){a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,b.target==this._mouseDownEvent.target&&a.data(b.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(b));return!1},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(a){return this.mouseDelayMet},_mouseStart:function(a){},_mouseDrag:function(a){},_mouseStop:function(a){},_mouseCapture:function(a){return!0}})})(jQuery);/*
+ * jQuery UI Position 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Position
+ */(function(a,b){a.ui=a.ui||{};var c=/left|center|right/,d=/top|center|bottom/,e="center",f={},g=a.fn.position,h=a.fn.offset;a.fn.position=function(b){if(!b||!b.of)return g.apply(this,arguments);b=a.extend({},b);var h=a(b.of),i=h[0],j=(b.collision||"flip").split(" "),k=b.offset?b.offset.split(" "):[0,0],l,m,n;i.nodeType===9?(l=h.width(),m=h.height(),n={top:0,left:0}):i.setTimeout?(l=h.width(),m=h.height(),n={top:h.scrollTop(),left:h.scrollLeft()}):i.preventDefault?(b.at="left top",l=m=0,n={top:b.of.pageY,left:b.of.pageX}):(l=h.outerWidth(),m=h.outerHeight(),n=h.offset()),a.each(["my","at"],function(){var a=(b[this]||"").split(" ");a.length===1&&(a=c.test(a[0])?a.concat([e]):d.test(a[0])?[e].concat(a):[e,e]),a[0]=c.test(a[0])?a[0]:e,a[1]=d.test(a[1])?a[1]:e,b[this]=a}),j.length===1&&(j[1]=j[0]),k[0]=parseInt(k[0],10)||0,k.length===1&&(k[1]=k[0]),k[1]=parseInt(k[1],10)||0,b.at[0]==="right"?n.left+=l:b.at[0]===e&&(n.left+=l/2),b.at[1]==="bottom"?n.top+=m:b.at[1]===e&&(n.top+=m/2),n.left+=k[0],n.top+=k[1];return this.each(function(){var c=a(this),d=c.outerWidth(),g=c.outerHeight(),h=parseInt(a.curCSS(this,"marginLeft",!0))||0,i=parseInt(a.curCSS(this,"marginTop",!0))||0,o=d+h+(parseInt(a.curCSS(this,"marginRight",!0))||0),p=g+i+(parseInt(a.curCSS(this,"marginBottom",!0))||0),q=a.extend({},n),r;b.my[0]==="right"?q.left-=d:b.my[0]===e&&(q.left-=d/2),b.my[1]==="bottom"?q.top-=g:b.my[1]===e&&(q.top-=g/2),f.fractions||(q.left=Math.round(q.left),q.top=Math.round(q.top)),r={left:q.left-h,top:q.top-i},a.each(["left","top"],function(c,e){a.ui.position[j[c]]&&a.ui.position[j[c]][e](q,{targetWidth:l,targetHeight:m,elemWidth:d,elemHeight:g,collisionPosition:r,collisionWidth:o,collisionHeight:p,offset:k,my:b.my,at:b.at})}),a.fn.bgiframe&&c.bgiframe(),c.offset(a.extend(q,{using:b.using}))})},a.ui.position={fit:{left:function(b,c){var d=a(window),e=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft();b.left=e>0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]!==e){var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0}},top:function(b,c){if(c.at[1]!==e){var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];if(!c||!c.ownerDocument)return null;if(b)return this.each(function(){a.offset.setOffset(this,b)});return h.call(this)}),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&a.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()})(jQuery);/*
+ * jQuery UI Draggable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Draggables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */(function(a,b){a.widget("ui.draggable",a.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},destroy:function(){if(!!this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy();return this}},_mouseCapture:function(b){var c=this.options;if(this.helper||c.disabled||a(b.target).is(".ui-resizable-handle"))return!1;this.handle=this._getHandle(b);if(!this.handle)return!1;c.iframeFix&&a(c.iframeFix===!0?"iframe":c.iframeFix).each(function(){a('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(a(this).offset()).appendTo("body")});return!0},_mouseStart:function(b){var c=this.options;this.helper=this._createHelper(b),this._cacheHelperProportions(),a.ui.ddmanager&&(a.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,c.cursorAt&&this._adjustOffsetFromHelper(c.cursorAt),c.containment&&this._setContainment();if(this._trigger("start",b)===!1){this._clear();return!1}this._cacheHelperProportions(),a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this.helper.addClass("ui-draggable-dragging"),this._mouseDrag(b,!0),a.ui.ddmanager&&a.ui.ddmanager.dragStart(this,b);return!0},_mouseDrag:function(b,c){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute");if(!c){var d=this._uiHash();if(this._trigger("drag",b,d)===!1){this._mouseUp({});return!1}this.position=d.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";a.ui.ddmanager&&a.ui.ddmanager.drag(this,b);return!1},_mouseStop:function(b){var c=!1;a.ui.ddmanager&&!this.options.dropBehaviour&&(c=a.ui.ddmanager.drop(this,b)),this.dropped&&(c=this.dropped,this.dropped=!1);if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return!1;if(this.options.revert=="invalid"&&!c||this.options.revert=="valid"&&c||this.options.revert===!0||a.isFunction(this.options.revert)&&this.options.revert.call(this.element,c)){var d=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){d._trigger("stop",b)!==!1&&d._clear()})}else this._trigger("stop",b)!==!1&&this._clear();return!1},_mouseUp:function(b){this.options.iframeFix===!0&&a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),a.ui.ddmanager&&a.ui.ddmanager.dragStop(this,b);return a.ui.mouse.prototype._mouseUp.call(this,b)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?!0:!1;a(this.options.handle,this.element).find("*").andSelf().each(function(){this==b.target&&(c=!0)});return c},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b])):c.helper=="clone"?this.element.clone().removeAttr("id"):this.element;d.parents("body").length||d.appendTo(c.appendTo=="parent"?this.element[0].parentNode:c.appendTo),d[0]!=this.element[0]&&!/(fixed|absolute)/.test(d.css("position"))&&d.css("position","absolute");return d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[b.containment=="document"?0:a(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,b.containment=="document"?0:a(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(b.containment=="document"?0:a(window).scrollLeft())+a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(b.containment=="document"?0:a(window).scrollTop())+(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)&&b.containment.constructor!=Array){var c=a(b.containment),d=c[0];if(!d)return;var e=c.offset(),f=a(d).css("overflow")!="hidden";this.containment=[(parseInt(a(d).css("borderLeftWidth"),10)||0)+(parseInt(a(d).css("paddingLeft"),10)||0),(parseInt(a(d).css("borderTopWidth"),10)||0)+(parseInt(a(d).css("paddingTop"),10)||0),(f?Math.max(d.scrollWidth,d.offsetWidth):d.offsetWidth)-(parseInt(a(d).css("borderLeftWidth"),10)||0)-(parseInt(a(d).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(f?Math.max(d.scrollHeight,d.offsetHeight):d.offsetHeight)-(parseInt(a(d).css("borderTopWidth"),10)||0)-(parseInt(a(d).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=c}else b.containment.constructor==Array&&(this.containment=b.containment)},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName),f=b.pageX,g=b.pageY;if(this.originalPosition){var h;if(this.containment){if(this.relative_container){var i=this.relative_container.offset();h=[this.containment[0]+i.left,this.containment[1]+i.top,this.containment[2]+i.left,this.containment[3]+i.top]}else h=this.containment;b.pageX-this.offset.click.left<h[0]&&(f=h[0]+this.offset.click.left),b.pageY-this.offset.click.top<h[1]&&(g=h[1]+this.offset.click.top),b.pageX-this.offset.click.left>h[2]&&(f=h[2]+this.offset.click.left),b.pageY-this.offset.click.top>h[3]&&(g=h[3]+this.offset.click.top)}if(c.grid){var j=c.grid[1]?this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1]:this.originalPageY;g=h?j-this.offset.click.top<h[1]||j-this.offset.click.top>h[3]?j-this.offset.click.top<h[1]?j+c.grid[1]:j-c.grid[1]:j:j;var k=c.grid[0]?this.originalPageX+Math.round((f-this.originalPageX)/c.grid[0])*c.grid[0]:this.originalPageX;f=h?k-this.offset.click.left<h[0]||k-this.offset.click.left>h[2]?k-this.offset.click.left<h[0]?k+c.grid[0]:k-c.grid[0]:k:k}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:d.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:d.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(b,c,d){d=d||this._uiHash(),a.ui.plugin.call(this,b,[c,d]),b=="drag"&&(this.positionAbs=this._convertPositionTo("absolute"));return a.Widget.prototype._trigger.call(this,b,c,d)},plugins:{},_uiHash:function(a){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),a.extend(a.ui.draggable,{version:"1.8.18"}),a.ui.plugin.add("draggable","connectToSortable",{start:function(b,c){var d=a(this).data("draggable"),e=d.options,f=a.extend({},c,{item:d.element});d.sortables=[],a(e.connectToSortable).each(function(){var c=a.data(this,"sortable");c&&!c.options.disabled&&(d.sortables.push({instance:c,shouldRevert:c.options.revert}),c.refreshPositions(),c._trigger("activate",b,f))})},stop:function(b,c){var d=a(this).data("draggable"),e=a.extend({},c,{item:d.element});a.each(d.sortables,function(){this.instance.isOver?(this.instance.isOver=0,d.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=!0),this.instance._mouseStop(b),this.instance.options.helper=this.instance.options._helper,d.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",b,e))})},drag:function(b,c){var d=a(this).data("draggable"),e=this,f=function(b){var c=this.offset.click.top,d=this.offset.click.left,e=this.positionAbs.top,f=this.positionAbs.left,g=b.height,h=b.width,i=b.top,j=b.left;return a.ui.isOver(e+c,f+d,i,j,g,h)};a.each(d.sortables,function(f){this.instance.positionAbs=d.positionAbs,this.instance.helperProportions=d.helperProportions,this.instance.offset.click=d.offset.click,this.instance._intersectsWith(this.instance.containerCache)?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=a(e).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return c.helper[0]},b.target=this.instance.currentItem[0],this.instance._mouseCapture(b,!0),this.instance._mouseStart(b,!0,!0),this.instance.offset.click.top=d.offset.click.top,this.instance.offset.click.left=d.offset.click.left,this.instance.offset.parent.left-=d.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=d.offset.parent.top-this.instance.offset.parent.top,d._trigger("toSortable",b),d.dropped=this.instance.element,d.currentItem=d.element,this.instance.fromOutside=d),this.instance.currentItem&&this.instance._mouseDrag(b)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",b,this.instance._uiHash(this.instance)),this.instance._mouseStop(b,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),d._trigger("fromSortable",b),d.dropped=!1)})}}),a.ui.plugin.add("draggable","cursor",{start:function(b,c){var d=a("body"),e=a(this).data("draggable").options;d.css("cursor")&&(e._cursor=d.css("cursor")),d.css("cursor",e.cursor)},stop:function(b,c){var d=a(this).data("draggable").options;d._cursor&&a("body").css("cursor",d._cursor)}}),a.ui.plugin.add("draggable","opacity",{start:function(b,c){var d=a(c.helper),e=a(this).data("draggable").options;d.css("opacity")&&(e._opacity=d.css("opacity")),d.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;d._opacity&&a(c.helper).css("opacity",d._opacity)}}),a.ui.plugin.add("draggable","scroll",{start:function(b,c){var d=a(this).data("draggable");d.scrollParent[0]!=document&&d.scrollParent[0].tagName!="HTML"&&(d.overflowOffset=d.scrollParent.offset())},drag:function(b,c){var d=a(this).data("draggable"),e=d.options,f=!1;if(d.scrollParent[0]!=document&&d.scrollParent[0].tagName!="HTML"){if(!e.axis||e.axis!="x")d.overflowOffset.top+d.scrollParent[0].offsetHeight-b.pageY<e.scrollSensitivity?d.scrollParent[0].scrollTop=f=d.scrollParent[0].scrollTop+e.scrollSpeed:b.pageY-d.overflowOffset.top<e.scrollSensitivity&&(d.scrollParent[0].scrollTop=f=d.scrollParent[0].scrollTop-e.scrollSpeed);if(!e.axis||e.axis!="y")d.overflowOffset.left+d.scrollParent[0].offsetWidth-b.pageX<e.scrollSensitivity?d.scrollParent[0].scrollLeft=f=d.scrollParent[0].scrollLeft+e.scrollSpeed:b.pageX-d.overflowOffset.left<e.scrollSensitivity&&(d.scrollParent[0].scrollLeft=f=d.scrollParent[0].scrollLeft-e.scrollSpeed)}else{if(!e.axis||e.axis!="x")b.pageY-a(document).scrollTop()<e.scrollSensitivity?f=a(document).scrollTop(a(document).scrollTop()-e.scrollSpeed):a(window).height()-(b.pageY-a(document).scrollTop())<e.scrollSensitivity&&(f=a(document).scrollTop(a(document).scrollTop()+e.scrollSpeed));if(!e.axis||e.axis!="y")b.pageX-a(document).scrollLeft()<e.scrollSensitivity?f=a(document).scrollLeft(a(document).scrollLeft()-e.scrollSpeed):a(window).width()-(b.pageX-a(document).scrollLeft())<e.scrollSensitivity&&(f=a(document).scrollLeft(a(document).scrollLeft()+e.scrollSpeed))}f!==!1&&a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(d,b)}}),a.ui.plugin.add("draggable","snap",{start:function(b,c){var d=a(this).data("draggable"),e=d.options;d.snapElements=[],a(e.snap.constructor!=String?e.snap.items||":data(draggable)":e.snap).each(function(){var b=a(this),c=b.offset();this!=d.element[0]&&d.snapElements.push({item:this,width:b.outerWidth(),height:b.outerHeight(),top:c.top,left:c.left})})},drag:function(b,c){var d=a(this).data("draggable"),e=d.options,f=e.snapTolerance,g=c.offset.left,h=g+d.helperProportions.width,i=c.offset.top,j=i+d.helperProportions.height;for(var k=d.snapElements.length-1;k>=0;k--){var l=d.snapElements[k].left,m=l+d.snapElements[k].width,n=d.snapElements[k].top,o=n+d.snapElements[k].height;if(!(l-f<g&&g<m+f&&n-f<i&&i<o+f||l-f<g&&g<m+f&&n-f<j&&j<o+f||l-f<h&&h<m+f&&n-f<i&&i<o+f||l-f<h&&h<m+f&&n-f<j&&j<o+f)){d.snapElements[k].snapping&&d.options.snap.release&&d.options.snap.release.call(d.element,b,a.extend(d._uiHash(),{snapItem:d.snapElements[k].item})),d.snapElements[k].snapping=!1;continue}if(e.snapMode!="inner"){var p=Math.abs(n-j)<=f,q=Math.abs(o-i)<=f,r=Math.abs(l-h)<=f,s=Math.abs(m-g)<=f;p&&(c.position.top=d._convertPositionTo("relative",{top:n-d.helperProportions.height,left:0}).top-d.margins.top),q&&(c.position.top=d._convertPositionTo("relative",{top:o,left:0}).top-d.margins.top),r&&(c.position.left=d._convertPositionTo("relative",{top:0,left:l-d.helperProportions.width}).left-d.margins.left),s&&(c.position.left=d._convertPositionTo("relative",{top:0,left:m}).left-d.margins.left)}var t=p||q||r||s;if(e.snapMode!="outer"){var p=Math.abs(n-i)<=f,q=Math.abs(o-j)<=f,r=Math.abs(l-g)<=f,s=Math.abs(m-h)<=f;p&&(c.position.top=d._convertPositionTo("relative",{top:n,left:0}).top-d.margins.top),q&&(c.position.top=d._convertPositionTo("relative",{top:o-d.helperProportions.height,left:0}).top-d.margins.top),r&&(c.position.left=d._convertPositionTo("relative",{top:0,left:l}).left-d.margins.left),s&&(c.position.left=d._convertPositionTo("relative",{top:0,left:m-d.helperProportions.width}).left-d.margins.left)}!d.snapElements[k].snapping&&(p||q||r||s||t)&&d.options.snap.snap&&d.options.snap.snap.call(d.element,b,a.extend(d._uiHash(),{snapItem:d.snapElements[k].item})),d.snapElements[k].snapping=p||q||r||s||t}}}),a.ui.plugin.add("draggable","stack",{start:function(b,c){var d=a(this).data("draggable").options,e=a.makeArray(a(d.stack)).sort(function(b,c){return(parseInt(a(b).css("zIndex"),10)||0)-(parseInt(a(c).css("zIndex"),10)||0)});if(!!e.length){var f=parseInt(e[0].style.zIndex)||0;a(e).each(function(a){this.style.zIndex=f+a}),this[0].style.zIndex=f+e.length}}}),a.ui.plugin.add("draggable","zIndex",{start:function(b,c){var d=a(c.helper),e=a(this).data("draggable").options;d.css("zIndex")&&(e._zIndex=d.css("zIndex")),d.css("zIndex",e.zIndex)},stop:function(b,c){var d=a(this).data("draggable").options;d._zIndex&&a(c.helper).css("zIndex",d._zIndex)}})})(jQuery);/*
+ * jQuery UI Droppable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Droppables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ * jquery.ui.mouse.js
+ * jquery.ui.draggable.js
+ */(function(a,b){a.widget("ui.droppable",{widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect"},_create:function(){var b=this.options,c=b.accept;this.isover=0,this.isout=1,this.accept=a.isFunction(c)?c:function(a){return a.is(c)},this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight},a.ui.ddmanager.droppables[b.scope]=a.ui.ddmanager.droppables[b.scope]||[],a.ui.ddmanager.droppables[b.scope].push(this),b.addClasses&&this.element.addClass("ui-droppable")},destroy:function(){var b=a.ui.ddmanager.droppables[this.options.scope];for(var c=0;c<b.length;c++)b[c]==this&&b.splice(c,1);this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable");return this},_setOption:function(b,c){b=="accept"&&(this.accept=a.isFunction(c)?c:function(a){return a.is(c)}),a.Widget.prototype._setOption.apply(this,arguments)},_activate:function(b){var c=a.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),c&&this._trigger("activate",b,this.ui(c))},_deactivate:function(b){var c=a.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),c&&this._trigger("deactivate",b,this.ui(c))},_over:function(b){var c=a.ui.ddmanager.current;!!c&&(c.currentItem||c.element)[0]!=this.element[0]&&this.accept.call(this.element[0],c.currentItem||c.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",b,this.ui(c)))},_out:function(b){var c=a.ui.ddmanager.current;!!c&&(c.currentItem||c.element)[0]!=this.element[0]&&this.accept.call(this.element[0],c.currentItem||c.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",b,this.ui(c)))},_drop:function(b,c){var d=c||a.ui.ddmanager.current;if(!d||(d.currentItem||d.element)[0]==this.element[0])return!1;var e=!1;this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var b=a.data(this,"droppable");if(b.options.greedy&&!b.options.disabled&&b.options.scope==d.options.scope&&b.accept.call(b.element[0],d.currentItem||d.element)&&a.ui.intersect(d,a.extend(b,{offset:b.element.offset()}),b.options.tolerance)){e=!0;return!1}});if(e)return!1;if(this.accept.call(this.element[0],d.currentItem||d.element)){this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",b,this.ui(d));return this.element}return!1},ui:function(a){return{draggable:a.currentItem||a.element,helper:a.helper,position:a.position,offset:a.positionAbs}}}),a.extend(a.ui.droppable,{version:"1.8.18"}),a.ui.intersect=function(b,c,d){if(!c.offset)return!1;var e=(b.positionAbs||b.position.absolute).left,f=e+b.helperProportions.width,g=(b.positionAbs||b.position.absolute).top,h=g+b.helperProportions.height,i=c.offset.left,j=i+c.proportions.width,k=c.offset.top,l=k+c.proportions.height;switch(d){case"fit":return i<=e&&f<=j&&k<=g&&h<=l;case"intersect":return i<e+b.helperProportions.width/2&&f-b.helperProportions.width/2<j&&k<g+b.helperProportions.height/2&&h-b.helperProportions.height/2<l;case"pointer":var m=(b.positionAbs||b.position.absolute).left+(b.clickOffset||b.offset.click).left,n=(b.positionAbs||b.position.absolute).top+(b.clickOffset||b.offset.click).top,o=a.ui.isOver(n,m,k,i,c.proportions.height,c.proportions.width);return o;case"touch":return(g>=k&&g<=l||h>=k&&h<=l||g<k&&h>l)&&(e>=i&&e<=j||f>=i&&f<=j||e<i&&f>j);default:return!1}},a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(b,c){var d=a.ui.ddmanager.droppables[b.options.scope]||[],e=c?c.type:null,f=(b.currentItem||b.element).find(":data(droppable)").andSelf();droppablesLoop:for(var g=0;g<d.length;g++){if(d[g].options.disabled||b&&!d[g].accept.call(d[g].element[0],b.currentItem||b.element))continue;for(var h=0;h<f.length;h++)if(f[h]==d[g].element[0]){d[g].proportions.height=0;continue droppablesLoop}d[g].visible=d[g].element.css("display")!="none";if(!d[g].visible)continue;e=="mousedown"&&d[g]._activate.call(d[g],c),d[g].offset=d[g].element.offset(),d[g].proportions={width:d[g].element[0].offsetWidth,height:d[g].element[0].offsetHeight}}},drop:function(b,c){var d=!1;a.each(a.ui.ddmanager.droppables[b.options.scope]||[],function(){!this.options||(!this.options.disabled&&this.visible&&a.ui.intersect(b,this,this.options.tolerance)&&(d=this._drop.call(this,c)||d),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],b.currentItem||b.element)&&(this.isout=1,this.isover=0,this._deactivate.call(this,c)))});return d},dragStart:function(b,c){b.element.parents(":not(body,html)").bind("scroll.droppable",function(){b.options.refreshPositions||a.ui.ddmanager.prepareOffsets(b,c)})},drag:function(b,c){b.options.refreshPositions&&a.ui.ddmanager.prepareOffsets(b,c),a.each(a.ui.ddmanager.droppables[b.options.scope]||[],function(){if(!(this.options.disabled||this.greedyChild||!this.visible)){var d=a.ui.intersect(b,this,this.options.tolerance),e=!d&&this.isover==1?"isout":d&&this.isover==0?"isover":null;if(!e)return;var f;if(this.options.greedy){var g=this.element.parents(":data(droppable):eq(0)");g.length&&(f=a.data(g[0],"droppable"),f.greedyChild=e=="isover"?1:0)}f&&e=="isover"&&(f.isover=0,f.isout=1,f._out.call(f,c)),this[e]=1,this[e=="isout"?"isover":"isout"]=0,this[e=="isover"?"_over":"_out"].call(this,c),f&&e=="isout"&&(f.isout=0,f.isover=1,f._over.call(f,c))}})},dragStop:function(b,c){b.element.parents(":not(body,html)").unbind("scroll.droppable"),b.options.refreshPositions||a.ui.ddmanager.prepareOffsets(b,c)}}})(jQuery);/*
+ * jQuery UI Resizable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */(function(a,b){a.widget("ui.resizable",a.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1e3},_create:function(){var b=this,c=this.options;this.element.addClass("ui-resizable"),a.extend(this,{_aspectRatio:!!c.aspectRatio,aspectRatio:c.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:c.helper||c.ghost||c.animate?c.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(a('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("resizable",this.element.data("resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=c.handles||(a(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se");if(this.handles.constructor==String){this.handles=="all"&&(this.handles="n,e,s,w,se,sw,ne,nw");var d=this.handles.split(",");this.handles={};for(var e=0;e<d.length;e++){var f=a.trim(d[e]),g="ui-resizable-"+f,h=a('<div class="ui-resizable-handle '+g+'"></div>');/sw|se|ne|nw/.test(f)&&h.css({zIndex:++c.zIndex}),"se"==f&&h.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[f]=".ui-resizable-"+f,this.element.append(h)}}this._renderAxis=function(b){b=b||this.element;for(var c in this.handles){this.handles[c].constructor==String&&(this.handles[c]=a(this.handles[c],this.element).show());if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var d=a(this.handles[c],this.element),e=0;e=/sw|ne|nw|se|n|s/.test(c)?d.outerHeight():d.outerWidth();var f=["padding",/ne|nw|n/.test(c)?"Top":/se|sw|s/.test(c)?"Bottom":/^e$/.test(c)?"Right":"Left"].join("");b.css(f,e),this._proportionallyResize()}if(!a(this.handles[c]).length)continue}},this._renderAxis(this.element),this._handles=a(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){if(!b.resizing){if(this.className)var a=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=a&&a[1]?a[1]:"se"}}),c.autoHide&&(this._handles.hide(),a(this.element).addClass("ui-resizable-autohide").hover(function(){c.disabled||(a(this).removeClass("ui-resizable-autohide"),b._handles.show())},function(){c.disabled||b.resizing||(a(this).addClass("ui-resizable-autohide"),b._handles.hide())})),this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(b){a(b).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var c=this.element;c.after(this.originalElement.css({position:c.css("position"),width:c.outerWidth(),height:c.outerHeight(),top:c.css("top"),left:c.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle),b(this.originalElement);return this},_mouseCapture:function(b){var c=!1;for(var d in this.handles)a(this.handles[d])[0]==b.target&&(c=!0);return!this.options.disabled&&c},_mouseStart:function(b){var d=this.options,e=this.element.position(),f=this.element;this.resizing=!0,this.documentScroll={top:a(document).scrollTop(),left:a(document).scrollLeft()},(f.is(".ui-draggable")||/absolute/.test(f.css("position")))&&f.css({position:"absolute",top:e.top,left:e.left}),this._renderProxy();var g=c(this.helper.css("left")),h=c(this.helper.css("top"));d.containment&&(g+=a(d.containment).scrollLeft()||0,h+=a(d.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:g,top:h},this.size=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalSize=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalPosition={left:g,top:h},this.sizeDiff={width:f.outerWidth()-f.width(),height:f.outerHeight()-f.height()},this.originalMousePosition={left:b.pageX,top:b.pageY},this.aspectRatio=typeof d.aspectRatio=="number"?d.aspectRatio:this.originalSize.width/this.originalSize.height||1;var i=a(".ui-resizable-"+this.axis).css("cursor");a("body").css("cursor",i=="auto"?this.axis+"-resize":i),f.addClass("ui-resizable-resizing"),this._propagate("start",b);return!0},_mouseDrag:function(b){var c=this.helper,d=this.options,e={},f=this,g=this.originalMousePosition,h=this.axis,i=b.pageX-g.left||0,j=b.pageY-g.top||0,k=this._change[h];if(!k)return!1;var l=k.apply(this,[b,i,j]),m=a.browser.msie&&a.browser.version<7,n=this.sizeDiff;this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)l=this._updateRatio(l,b);l=this._respectSize(l,b),this._propagate("resize",b),c.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"}),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),this._updateCache(l),this._trigger("resize",b,this.ui());return!1},_mouseStop:function(b){this.resizing=!1;var c=this.options,d=this;if(this._helper){var e=this._proportionallyResizeElements,f=e.length&&/textarea/i.test(e[0].nodeName),g=f&&a.ui.hasScroll(e[0],"left")?0:d.sizeDiff.height,h=f?0:d.sizeDiff.width,i={width:d.helper.width()-h,height:d.helper.height()-g},j=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,k=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;c.animate||this.element.css(a.extend(i,{top:k,left:j})),d.helper.height(d.size.height),d.helper.width(d.size.width),this._helper&&!c.animate&&this._proportionallyResize()}a("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",b),this._helper&&this.helper.remove();return!1},_updateVirtualBoundaries:function(a){var b=this.options,c,e,f,g,h;h={minWidth:d(b.minWidth)?b.minWidth:0,maxWidth:d(b.maxWidth)?b.maxWidth:Infinity,minHeight:d(b.minHeight)?b.minHeight:0,maxHeight:d(b.maxHeight)?b.maxHeight:Infinity};if(this._aspectRatio||a)c=h.minHeight*this.aspectRatio,f=h.minWidth/this.aspectRatio,e=h.maxHeight*this.aspectRatio,g=h.maxWidth/this.aspectRatio,c>h.minWidth&&(h.minWidth=c),f>h.minHeight&&(h.minHeight=f),e<h.maxWidth&&(h.maxWidth=e),g<h.maxHeight&&(h.maxHeight=g);this._vBoundaries=h},_updateCache:function(a){var b=this.options;this.offset=this.helper.offset(),d(a.left)&&(this.position.left=a.left),d(a.top)&&(this.position.top=a.top),d(a.height)&&(this.size.height=a.height),d(a.width)&&(this.size.width=a.width)},_updateRatio:function(a,b){var c=this.options,e=this.position,f=this.size,g=this.axis;d(a.height)?a.width=a.height*this.aspectRatio:d(a.width)&&(a.height=a.width/this.aspectRatio),g=="sw"&&(a.left=e.left+(f.width-a.width),a.top=null),g=="nw"&&(a.top=e.top+(f.height-a.height),a.left=e.left+(f.width-a.width));return a},_respectSize:function(a,b){var c=this.helper,e=this._vBoundaries,f=this._aspectRatio||b.shiftKey,g=this.axis,h=d(a.width)&&e.maxWidth&&e.maxWidth<a.width,i=d(a.height)&&e.maxHeight&&e.maxHeight<a.height,j=d(a.width)&&e.minWidth&&e.minWidth>a.width,k=d(a.height)&&e.minHeight&&e.minHeight>a.height;j&&(a.width=e.minWidth),k&&(a.height=e.minHeight),h&&(a.width=e.maxWidth),i&&(a.height=e.maxHeight);var l=this.originalPosition.left+this.originalSize.width,m=this.position.top+this.size.height,n=/sw|nw|w/.test(g),o=/nw|ne|n/.test(g);j&&n&&(a.left=l-e.minWidth),h&&n&&(a.left=l-e.maxWidth),k&&o&&(a.top=m-e.minHeight),i&&o&&(a.top=m-e.maxHeight);var p=!a.width&&!a.height;p&&!a.left&&a.top?a.top=null:p&&!a.top&&a.left&&(a.left=null);return a},_proportionallyResize:function(){var b=this.options;if(!!this._proportionallyResizeElements.length){var c=this.helper||this.element;for(var d=0;d<this._proportionallyResizeElements.length;d++){var e=this._proportionallyResizeElements[d];if(!this.borderDif){var f=[e.css("borderTopWidth"),e.css("borderRightWidth"),e.css("borderBottomWidth"),e.css("borderLeftWidth")],g=[e.css("paddingTop"),e.css("paddingRight"),e.css("paddingBottom"),e.css("paddingLeft")];this.borderDif=a.map(f,function(a,b){var c=parseInt(a,10)||0,d=parseInt(g[b],10)||0;return c+d})}if(a.browser.msie&&(!!a(c).is(":hidden")||!!a(c).parents(":hidden").length))continue;e.css({height:c.height()-this.borderDif[0]-this.borderDif[2]||0,width:c.width()-this.borderDif[1]-this.borderDif[3]||0})}}},_renderProxy:function(){var b=this.element,c=this.options;this.elementOffset=b.offset();if(this._helper){this.helper=this.helper||a('<div style="overflow:hidden;"></div>');var d=a.browser.msie&&a.browser.version<7,e=d?1:0,f=d?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+f,height:this.element.outerHeight()+f,position:"absolute",left:this.elementOffset.left-e+"px",top:this.elementOffset.top-e+"px",zIndex:++c.zIndex}),this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(a,b,c){return{width:this.originalSize.width+b}},w:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{left:f.left+b,width:e.width-b}},n:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{top:f.top+c,height:e.height-c}},s:function(a,b,c){return{height:this.originalSize.height+c}},se:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},sw:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,c,d]))},ne:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},nw:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,c,d]))}},_propagate:function(b,c){a.ui.plugin.call(this,b,[c,this.ui()]),b!="resize"&&this._trigger(b,c,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),a.extend(a.ui.resizable,{version:"1.8.18"}),a.ui.plugin.add("resizable","alsoResize",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=function(b){a(b).each(function(){var b=a(this);b.data("resizable-alsoresize",{width:parseInt(b.width(),10),height:parseInt(b.height(),10),left:parseInt(b.css("left"),10),top:parseInt(b.css("top"),10)})})};typeof e.alsoResize=="object"&&!e.alsoResize.parentNode?e.alsoResize.length?(e.alsoResize=e.alsoResize[0],f(e.alsoResize)):a.each(e.alsoResize,function(a){f(a)}):f(e.alsoResize)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.originalSize,g=d.originalPosition,h={height:d.size.height-f.height||0,width:d.size.width-f.width||0,top:d.position.top-g.top||0,left:d.position.left-g.left||0},i=function(b,d){a(b).each(function(){var b=a(this),e=a(this).data("resizable-alsoresize"),f={},g=d&&d.length?d:b.parents(c.originalElement[0]).length?["width","height"]:["width","height","top","left"];a.each(g,function(a,b){var c=(e[b]||0)+(h[b]||0);c&&c>=0&&(f[b]=c||null)}),b.css(f)})};typeof e.alsoResize=="object"&&!e.alsoResize.nodeType?a.each(e.alsoResize,function(a,b){i(a,b)}):i(e.alsoResize)},stop:function(b,c){a(this).removeData("resizable-alsoresize")}}),a.ui.plugin.add("resizable","animate",{stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d._proportionallyResizeElements,g=f.length&&/textarea/i.test(f[0].nodeName),h=g&&a.ui.hasScroll(f[0],"left")?0:d.sizeDiff.height,i=g?0:d.sizeDiff.width,j={width:d.size.width-i,height:d.size.height-h},k=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,l=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;d.element.animate(a.extend(j,l&&k?{top:l,left:k}:{}),{duration:e.animateDuration,easing:e.animateEasing,step:function(){var c={width:parseInt(d.element.css("width"),10),height:parseInt(d.element.css("height"),10),top:parseInt(d.element.css("top"),10),left:parseInt(d.element.css("left"),10)};f&&f.length&&a(f[0]).css({width:c.width,height:c.height}),d._updateCache(c),d._propagate("resize",b)}})}}),a.ui.plugin.add("resizable","containment",{start:function(b,d){var e=a(this).data("resizable"),f=e.options,g=e.element,h=f.containment,i=h instanceof a?h.get(0):/parent/.test(h)?g.parent().get(0):h;if(!!i){e.containerElement=a(i);if(/document/.test(h)||h==document)e.containerOffset={left:0,top:0},e.containerPosition={left:0,top:0},e.parentData={element:a(document),left:0,top:0,width:a(document).width(),height:a(document).height()||document.body.parentNode.scrollHeight};else{var j=a(i),k=[];a(["Top","Right","Left","Bottom"]).each(function(a,b){k[a]=c(j.css("padding"+b))}),e.containerOffset=j.offset(),e.containerPosition=j.position(),e.containerSize={height:j.innerHeight()-k[3],width:j.innerWidth()-k[1]};var l=e.containerOffset,m=e.containerSize.height,n=e.containerSize.width,o=a.ui.hasScroll(i,"left")?i.scrollWidth:n,p=a.ui.hasScroll(i)?i.scrollHeight:m;e.parentData={element:i,left:l.left,top:l.top,width:o,height:p}}}},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.containerSize,g=d.containerOffset,h=d.size,i=d.position,j=d._aspectRatio||b.shiftKey,k={top:0,left:0},l=d.containerElement;l[0]!=document&&/static/.test(l.css("position"))&&(k=g),i.left<(d._helper?g.left:0)&&(d.size.width=d.size.width+(d._helper?d.position.left-g.left:d.position.left-k.left),j&&(d.size.height=d.size.width/e.aspectRatio),d.position.left=e.helper?g.left:0),i.top<(d._helper?g.top:0)&&(d.size.height=d.size.height+(d._helper?d.position.top-g.top:d.position.top),j&&(d.size.width=d.size.height*e.aspectRatio),d.position.top=d._helper?g.top:0),d.offset.left=d.parentData.left+d.position.left,d.offset.top=d.parentData.top+d.position.top;var m=Math.abs((d._helper?d.offset.left-k.left:d.offset.left-k.left)+d.sizeDiff.width),n=Math.abs((d._helper?d.offset.top-k.top:d.offset.top-g.top)+d.sizeDiff.height),o=d.containerElement.get(0)==d.element.parent().get(0),p=/relative|absolute/.test(d.containerElement.css("position"));o&&p&&(m-=d.parentData.left),m+d.size.width>=d.parentData.width&&(d.size.width=d.parentData.width-m,j&&(d.size.height=d.size.width/d.aspectRatio)),n+d.size.height>=d.parentData.height&&(d.size.height=d.parentData.height-n,j&&(d.size.width=d.size.height*d.aspectRatio))},stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.position,g=d.containerOffset,h=d.containerPosition,i=d.containerElement,j=a(d.helper),k=j.offset(),l=j.outerWidth()-d.sizeDiff.width,m=j.outerHeight()-d.sizeDiff.height;d._helper&&!e.animate&&/relative/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m}),d._helper&&!e.animate&&/static/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m})}}),a.ui.plugin.add("resizable","ghost",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size;d.ghost=d.originalElement.clone(),d.ghost.css({opacity:.25,display:"block",position:"relative",height:f.height,width:f.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof e.ghost=="string"?e.ghost:""),d.ghost.appendTo(d.helper)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.ghost.css({position:"relative",height:d.size.height,width:d.size.width})},stop:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.helper&&d.helper.get(0).removeChild(d.ghost.get(0))}}),a.ui.plugin.add("resizable","grid",{resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size,g=d.originalSize,h=d.originalPosition,i=d.axis,j=e._aspectRatio||b.shiftKey;e.grid=typeof e.grid=="number"?[e.grid,e.grid]:e.grid;var k=Math.round((f.width-g.width)/(e.grid[0]||1))*(e.grid[0]||1),l=Math.round((f.height-g.height)/(e.grid[1]||1))*(e.grid[1]||1);/^(se|s|e)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l):/^(ne)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l):/^(sw)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.left=h.left-k):(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l,d.position.left=h.left-k)}});var c=function(a){return parseInt(a,10)||0},d=function(a){return!isNaN(parseInt(a,10))}})(jQuery);/*
+ * jQuery UI Selectable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */(function(a,b){a.widget("ui.selectable",a.ui.mouse,{options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch"},_create:function(){var b=this;this.element.addClass("ui-selectable"),this.dragged=!1;var c;this.refresh=function(){c=a(b.options.filter,b.element[0]),c.addClass("ui-selectee"),c.each(function(){var b=a(this),c=b.offset();a.data(this,"selectable-item",{element:this,$element:b,left:c.left,top:c.top,right:c.left+b.outerWidth(),bottom:c.top+b.outerHeight(),startselected:!1,selected:b.hasClass("ui-selected"),selecting:b.hasClass("ui-selecting"),unselecting:b.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=c.addClass("ui-selectee"),this._mouseInit(),this.helper=a("<div class='ui-selectable-helper'></div>")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable"),this._mouseDestroy();return this},_mouseStart:function(b){var c=this;this.opos=[b.pageX,b.pageY];if(!this.options.disabled){var d=this.options;this.selectees=a(d.filter,this.element[0]),this._trigger("start",b),a(d.appendTo).append(this.helper),this.helper.css({left:b.clientX,top:b.clientY,width:0,height:0}),d.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var d=a.data(this,"selectable-item");d.startselected=!0,!b.metaKey&&!b.ctrlKey&&(d.$element.removeClass("ui-selected"),d.selected=!1,d.$element.addClass("ui-unselecting"),d.unselecting=!0,c._trigger("unselecting",b,{unselecting:d.element}))}),a(b.target).parents().andSelf().each(function(){var d=a.data(this,"selectable-item");if(d){var e=!b.metaKey&&!b.ctrlKey||!d.$element.hasClass("ui-selected");d.$element.removeClass(e?"ui-unselecting":"ui-selected").addClass(e?"ui-selecting":"ui-unselecting"),d.unselecting=!e,d.selecting=e,d.selected=e,e?c._trigger("selecting",b,{selecting:d.element}):c._trigger("unselecting",b,{unselecting:d.element});return!1}})}},_mouseDrag:function(b){var c=this;this.dragged=!0;if(!this.options.disabled){var d=this.options,e=this.opos[0],f=this.opos[1],g=b.pageX,h=b.pageY;if(e>g){var i=g;g=e,e=i}if(f>h){var i=h;h=f,f=i}this.helper.css({left:e,top:f,width:g-e,height:h-f}),this.selectees.each(function(){var i=a.data(this,"selectable-item");if(!!i&&i.element!=c.element[0]){var j=!1;d.tolerance=="touch"?j=!(i.left>g||i.right<e||i.top>h||i.bottom<f):d.tolerance=="fit"&&(j=i.left>e&&i.right<g&&i.top>f&&i.bottom<h),j?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,c._trigger("selecting",b,{selecting:i.element}))):(i.selecting&&((b.metaKey||b.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),c._trigger("unselecting",b,{unselecting:i.element}))),i.selected&&!b.metaKey&&!b.ctrlKey&&!i.startselected&&(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,c._trigger("unselecting",b,{unselecting:i.element})))}});return!1}},_mouseStop:function(b){var c=this;this.dragged=!1;var d=this.options;a(".ui-unselecting",this.element[0]).each(function(){var d=a.data(this,"selectable-item");d.$element.removeClass("ui-unselecting"),d.unselecting=!1,d.startselected=!1,c._trigger("unselected",b,{unselected:d.element})}),a(".ui-selecting",this.element[0]).each(function(){var d=a.data(this,"selectable-item");d.$element.removeClass("ui-selecting").addClass("ui-selected"),d.selecting=!1,d.selected=!0,d.startselected=!0,c._trigger("selected",b,{selected:d.element})}),this._trigger("stop",b),this.helper.remove();return!1}}),a.extend(a.ui.selectable,{version:"1.8.18"})})(jQuery);/*
+ * jQuery UI Sortable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Sortables
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */(function(a,b){a.widget("ui.sortable",a.ui.mouse,{widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3},_create:function(){var a=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},destroy:function(){a.Widget.prototype.destroy.call(this),this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var b=this.items.length-1;b>=0;b--)this.items[b].item.removeData(this.widgetName+"-item");return this},_setOption:function(b,c){b==="disabled"?(this.options[b]=c,this.widget()[c?"addClass":"removeClass"]("ui-sortable-disabled")):a.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(b,c){var d=this;if(this.reverting)return!1;if(this.options.disabled||this.options.type=="static")return!1;this._refreshItems(b);var e=null,f=this,g=a(b.target).parents().each(function(){if(a.data(this,d.widgetName+"-item")==f){e=a(this);return!1}});a.data(b.target,d.widgetName+"-item")==f&&(e=a(b.target));if(!e)return!1;if(this.options.handle&&!c){var h=!1;a(this.options.handle,e).find("*").andSelf().each(function(){this==b.target&&(h=!0)});if(!h)return!1}this.currentItem=e,this._removeCurrentsFromItems();return!0},_mouseStart:function(b,c,d){var e=this.options,f=this;this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(b),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,e.cursorAt&&this._adjustOffsetFromHelper(e.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!=this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),e.containment&&this._setContainment(),e.cursor&&(a("body").css("cursor")&&(this._storedCursor=a("body").css("cursor")),a("body").css("cursor",e.cursor)),e.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",e.opacity)),e.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",e.zIndex)),this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",b,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions();if(!d)for(var g=this.containers.length-1;g>=0;g--)this.containers[g]._trigger("activate",b,f._uiHash(this));a.ui.ddmanager&&(a.ui.ddmanager.current=this),a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(b);return!0},_mouseDrag:function(b){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs);if(this.options.scroll){var c=this.options,d=!1;this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-b.pageY<c.scrollSensitivity?this.scrollParent[0].scrollTop=d=this.scrollParent[0].scrollTop+c.scrollSpeed:b.pageY-this.overflowOffset.top<c.scrollSensitivity&&(this.scrollParent[0].scrollTop=d=this.scrollParent[0].scrollTop-c.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-b.pageX<c.scrollSensitivity?this.scrollParent[0].scrollLeft=d=this.scrollParent[0].scrollLeft+c.scrollSpeed:b.pageX-this.overflowOffset.left<c.scrollSensitivity&&(this.scrollParent[0].scrollLeft=d=this.scrollParent[0].scrollLeft-c.scrollSpeed)):(b.pageY-a(document).scrollTop()<c.scrollSensitivity?d=a(document).scrollTop(a(document).scrollTop()-c.scrollSpeed):a(window).height()-(b.pageY-a(document).scrollTop())<c.scrollSensitivity&&(d=a(document).scrollTop(a(document).scrollTop()+c.scrollSpeed)),b.pageX-a(document).scrollLeft()<c.scrollSensitivity?d=a(document).scrollLeft(a(document).scrollLeft()-c.scrollSpeed):a(window).width()-(b.pageX-a(document).scrollLeft())<c.scrollSensitivity&&(d=a(document).scrollLeft(a(document).scrollLeft()+c.scrollSpeed))),d!==!1&&a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b)}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";for(var e=this.items.length-1;e>=0;e--){var f=this.items[e],g=f.item[0],h=this._intersectsWithPointer(f);if(!h)continue;if(g!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=g&&!a.ui.contains(this.placeholder[0],g)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],g):!0)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(f))this._rearrange(b,f);else break;this._trigger("change",b,this._uiHash());break}}this._contactContainers(b),a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),this._trigger("sort",b,this._uiHash()),this.lastPositionAbs=this.positionAbs;return!1},_mouseStop:function(b,c){if(!!b){a.ui.ddmanager&&!this.options.dropBehaviour&&a.ui.ddmanager.drop(this,b);if(this.options.revert){var d=this,e=d.placeholder.offset();d.reverting=!0,a(this.helper).animate({left:e.left-this.offset.parent.left-d.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-d.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){d._clear(b)})}else this._clear(b,c);return!1}},cancel:function(){var b=this;if(this.dragging){this._mouseUp({target:null}),this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("deactivate",null,b._uiHash(this)),this.containers[c].containerCache.over&&(this.containers[c]._trigger("out",null,b._uiHash(this)),this.containers[c].containerCache.over=0)}this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),a.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?a(this.domPosition.prev).after(this.currentItem):a(this.domPosition.parent).prepend(this.currentItem));return this},serialize:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];b=b||{},a(c).each(function(){var c=(a(b.item||this).attr(b.attribute||"id")||"").match(b.expression||/(.+)[-=_](.+)/);c&&d.push((b.key||c[1]+"[]")+"="+(b.key&&b.expression?c[1]:c[2]))}),!d.length&&b.key&&d.push(b.key+"=");return d.join("&")},toArray:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];b=b||{},c.each(function(){d.push(a(b.item||this).attr(b.attribute||"id")||"")});return d},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,d=this.positionAbs.top,e=d+this.helperProportions.height,f=a.left,g=f+a.width,h=a.top,i=h+a.height,j=this.offset.click.top,k=this.offset.click.left,l=d+j>h&&d+j<i&&b+k>f&&b+k<g;return this.options.tolerance=="pointer"||this.options.forcePointerForContainers||this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>a[this.floating?"width":"height"]?l:f<b+this.helperProportions.width/2&&c-this.helperProportions.width/2<g&&h<d+this.helperProportions.height/2&&e-this.helperProportions.height/2<i},_intersectsWithPointer:function(b){var c=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,b.top,b.height),d=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,b.left,b.width),e=c&&d,f=this._getDragVerticalDirection(),g=this._getDragHorizontalDirection();if(!e)return!1;return this.floating?g&&g=="right"||f=="down"?2:1:f&&(f=="down"?2:1)},_intersectsWithSides:function(b){var c=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,b.top+b.height/2,b.height),d=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,b.left+b.width/2,b.width),e=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();return this.floating&&f?f=="right"&&d||f=="left"&&!d:e&&(e=="down"&&c||e=="up"&&!c)},_getDragVerticalDirection:function(){var a=this.positionAbs.top-this.lastPositionAbs.top;return a!=0&&(a>0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a),this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(b){var c=this,d=[],e=[],f=this._connectWith();if(f&&b)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&e.push([a.isFunction(j.options.items)?j.options.items.call(j.element):a(j.options.items,j.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),j])}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(var g=e.length-1;g>=0;g--)e[g][0].each(function(){d.push(this)});return a(d)},_removeCurrentsFromItems:function(){var a=this.currentItem.find(":data("+this.widgetName+"-item)");for(var b=0;b<this.items.length;b++)for(var c=0;c<a.length;c++)a[c]==this.items[b].item[0]&&this.items.splice(b,1)},_refreshItems:function(b){this.items=[],this.containers=[this];var c=this.items,d=this,e=[[a.isFunction(this.options.items)?this.options.items.call(this.element[0],b,{item:this.currentItem}):a(this.options.items,this.element),this]],f=this._connectWith();if(f&&this.ready)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&(e.push([a.isFunction(j.options.items)?j.options.items.call(j.element[0],b,{item:this.currentItem}):a(j.options.items,j.element),j]),this.containers.push(j))}}for(var g=e.length-1;g>=0;g--){var k=e[g][1],l=e[g][0];for(var i=0,m=l.length;i<m;i++){var n=a(l[i]);n.data(this.widgetName+"-item",k),c.push({item:n,instance:k,width:0,height:0,left:0,top:0})}}},refreshPositions:function(b){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());for(var c=this.items.length-1;c>=0;c--){var d=this.items[c];if(d.instance!=this.currentContainer&&this.currentContainer&&d.item[0]!=this.currentItem[0])continue;var e=this.options.toleranceElement?a(this.options.toleranceElement,d.item):d.item;b||(d.width=e.outerWidth(),d.height=e.outerHeight());var f=e.offset();d.left=f.left,d.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(var c=this.containers.length-1;c>=0;c--){var f=this.containers[c].element.offset();this.containers[c].containerCache.left=f.left,this.containers[c].containerCache.top=f.top,this.containers[c].containerCache.width=this.containers[c].element.outerWidth(),this.containers[c].containerCache.height=this.containers[c].element.outerHeight()}return this},_createPlaceholder:function(b){var c=b||this,d=c.options;if(!d.placeholder||d.placeholder.constructor==String){var e=d.placeholder;d.placeholder={element:function(){var b=a(document.createElement(c.currentItem[0].nodeName)).addClass(e||c.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];e||(b.style.visibility="hidden");return b},update:function(a,b){if(!e||!!d.forcePlaceholderSize)b.height()||b.height(c.currentItem.innerHeight()-parseInt(c.currentItem.css("paddingTop")||0,10)-parseInt(c.currentItem.css("paddingBottom")||0,10)),b.width()||b.width(c.currentItem.innerWidth()-parseInt(c.currentItem.css("paddingLeft")||0,10)-parseInt(c.currentItem.css("paddingRight")||0,10))}}}c.placeholder=a(d.placeholder.element.call(c.element,c.currentItem)),c.currentItem.after(c.placeholder),d.placeholder.update(c,c.placeholder)},_contactContainers:function(b){var c=null,d=null;for(var e=this.containers.length-1;e>=0;e--){if(a.ui.contains(this.currentItem[0],this.containers[e].element[0]))continue;if(this._intersectsWith(this.containers[e].containerCache)){if(c&&a.ui.contains(this.containers[e].element[0],c.element[0]))continue;c=this.containers[e],d=e}else this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",b,this._uiHash(this)),this.containers[e].containerCache.over=0)}if(!!c)if(this.containers.length===1)this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1;else if(this.currentContainer!=this.containers[d]){var f=1e4,g=null,h=this.positionAbs[this.containers[d].floating?"left":"top"];for(var i=this.items.length-1;i>=0;i--){if(!a.ui.contains(this.containers[d].element[0],this.items[i].item[0]))continue;var j=this.items[i][this.containers[d].floating?"left":"top"];Math.abs(j-h)<f&&(f=Math.abs(j-h),g=this.items[i])}if(!g&&!this.options.dropOnEmpty)return;this.currentContainer=this.containers[d],g?this._rearrange(b,g,null,!0):this._rearrange(b,null,this.containers[d].element,!0),this._trigger("change",b,this._uiHash()),this.containers[d]._trigger("change",b,this._uiHash(this)),this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1}},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b,this.currentItem])):c.helper=="clone"?this.currentItem.clone():this.currentItem;d.parents("body").length||a(c.appendTo!="parent"?c.appendTo:this.currentItem[0].parentNode)[0].appendChild(d[0]),d[0]==this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(d[0].style.width==""||c.forceHelperSize)&&d.width(this.currentItem.width()),(d[0].style.height==""||c.forceHelperSize)&&d.height(this.currentItem.height());return d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)){var c=a(b.containment)[0],d=a(b.containment).offset(),e=a(c).css("overflow")!="hidden";this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(e?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(e?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName);this.cssPosition=="relative"&&(this.scrollParent[0]==document||this.scrollParent[0]==this.offsetParent[0])&&(this.offset.relative=this._getRelativeOffset());var f=b.pageX,g=b.pageY;if(this.originalPosition){this.containment&&(b.pageX-this.offset.click.left<this.containment[0]&&(f=this.containment[0]+this.offset.click.left),b.pageY-this.offset.click.top<this.containment[1]&&(g=this.containment[1]+this.offset.click.top),b.pageX-this.offset.click.left>this.containment[2]&&(f=this.containment[2]+this.offset.click.left),b.pageY-this.offset.click.top>this.containment[3]&&(g=this.containment[3]+this.offset.click.top));if(c.grid){var h=this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1];g=this.containment?h-this.offset.click.top<this.containment[1]||h-this.offset.click.top>this.containment[3]?h-this.offset.click.top<this.containment[1]?h+c.grid[1]:h-c.grid[1]:h:h;var i=this.originalPageX+Math.round((f-this.originalPageX)/c.grid[0])*c.grid[0];f=this.containment?i-this.offset.click.left<this.containment[0]||i-this.offset.click.left>this.containment[2]?i-this.offset.click.left<this.containment[0]?i+c.grid[0]:i-c.grid[0]:i:i}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:d.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:d.scrollLeft())}},_rearrange:function(a,b,c,d){c?c[0].appendChild(this.placeholder[0]):b.item[0].parentNode.insertBefore(this.placeholder[0],this.direction=="down"?b.item[0]:b.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var e=this,f=this.counter;window.setTimeout(function(){f==e.counter&&e.refreshPositions(!d)},0)},_clear:function(b,c){this.reverting=!1;var d=[],e=this;!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var f in this._storedCSS)if(this._storedCSS[f]=="auto"||this._storedCSS[f]=="static")this._storedCSS[f]="";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!c&&d.push(function(a){this._trigger("receive",a,this._uiHash(this.fromOutside))}),(this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!c&&d.push(function(a){this._trigger("update",a,this._uiHash())});if(!a.ui.contains(this.element[0],this.currentItem[0])){c||d.push(function(a){this._trigger("remove",a,this._uiHash())});for(var f=this.containers.length-1;f>=0;f--)a.ui.contains(this.containers[f].element[0],this.currentItem[0])&&!c&&(d.push(function(a){return function(b){a._trigger("receive",b,this._uiHash(this))}}.call(this,this.containers[f])),d.push(function(a){return function(b){a._trigger("update",b,this._uiHash(this))}}.call(this,this.containers[f])))}for(var f=this.containers.length-1;f>=0;f--)c||d.push(function(a){return function(b){a._trigger("deactivate",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over&&(d.push(function(a){return function(b){a._trigger("out",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over=0);this._storedCursor&&a("body").css("cursor",this._storedCursor),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex),this.dragging=!1;if(this.cancelHelperRemoval){if(!c){this._trigger("beforeStop",b,this._uiHash());for(var f=0;f<d.length;f++)d[f].call(this,b);this._trigger("stop",b,this._uiHash())}return!1}c||this._trigger("beforeStop",b,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!=this.currentItem[0]&&this.helper.remove(),this.helper=null;if(!c){for(var f=0;f<d.length;f++)d[f].call(this,b);this._trigger("stop",b,this._uiHash())}this.fromOutside=!1;return!0},_trigger:function(){a.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(b){var c=b||this;return{helper:c.helper,placeholder:c.placeholder||a([]),position:c.position,originalPosition:c.originalPosition,offset:c.positionAbs,item:c.currentItem,sender:b?b.element:null}}}),a.extend(a.ui.sortable,{version:"1.8.18"})})(jQuery);/*
+ * jQuery UI Accordion 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */(function(a,b){a.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:!0,clearStyle:!1,collapsible:!1,event:"click",fillSpace:!1,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:!1,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var b=this,c=b.options;b.running=0,b.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"),b.headers=b.element.find(c.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){c.disabled||a(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){c.disabled||a(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){c.disabled||a(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){c.disabled||a(this).removeClass("ui-state-focus")}),b.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");if(c.navigation){var d=b.element.find("a").filter(c.navigationFilter).eq(0);if(d.length){var e=d.closest(".ui-accordion-header");e.length?b.active=e:b.active=d.closest(".ui-accordion-content").prev()}}b.active=b._findActive(b.active||c.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top"),b.active.next().addClass("ui-accordion-content-active"),b._createIcons(),b.resize(),b.element.attr("role","tablist"),b.headers.attr("role","tab").bind("keydown.accordion",function(a){return b._keydown(a)}).next().attr("role","tabpanel"),b.headers.not(b.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide(),b.active.length?b.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):b.headers.eq(0).attr("tabIndex",0),a.browser.safari||b.headers.find("a").attr("tabIndex",-1),c.event&&b.headers.bind(c.event.split(" ").join(".accordion ")+".accordion",function(a){b._clickHandler.call(b,a,this),a.preventDefault()})},_createIcons:function(){var b=this.options;b.icons&&(a("<span></span>").addClass("ui-icon "+b.icons.header).prependTo(this.headers),this.active.children(".ui-icon").toggleClass(b.icons.header).toggleClass(b.icons.headerSelected),this.element.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.children(".ui-icon").remove(),this.element.removeClass("ui-accordion-icons")},destroy:function(){var b=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"),this.headers.find("a").removeAttr("tabIndex"),this._destroyIcons();var c=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");(b.autoHeight||b.fillHeight)&&c.css("height","");return a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b=="active"&&this.activate(c),b=="icons"&&(this._destroyIcons(),c&&this._createIcons()),b=="disabled"&&this.headers.add(this.headers.next())[c?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(b){if(!(this.options.disabled||b.altKey||b.ctrlKey)){var c=a.ui.keyCode,d=this.headers.length,e=this.headers.index(b.target),f=!1;switch(b.keyCode){case c.RIGHT:case c.DOWN:f=this.headers[(e+1)%d];break;case c.LEFT:case c.UP:f=this.headers[(e-1+d)%d];break;case c.SPACE:case c.ENTER:this._clickHandler({target:b.target},b.target),b.preventDefault()}if(f){a(b.target).attr("tabIndex",-1),a(f).attr("tabIndex",0),f.focus();return!1}return!0}},resize:function(){var b=this.options,c;if(b.fillSpace){if(a.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}c=this.element.parent().height(),a.browser.msie&&this.element.parent().css("overflow",d),this.headers.each(function(){c-=a(this).outerHeight(!0)}),this.headers.next().each(function(){a(this).height(Math.max(0,c-a(this).innerHeight()+a(this).height()))}).css("overflow","auto")}else b.autoHeight&&(c=0,this.headers.next().each(function(){c=Math.max(c,a(this).height("").height())}).height(c));return this},activate:function(a){this.options.active=a;var b=this._findActive(a)[0];this._clickHandler({target:b},b);return this},_findActive:function(b){return b?typeof b=="number"?this.headers.filter(":eq("+b+")"):this.headers.not(this.headers.not(b)):b===!1?a([]):this.headers.filter(":eq(0)")},_clickHandler:function(b,c){var d=this.options;if(!d.disabled){if(!b.target){if(!d.collapsible)return;this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),this.active.next().addClass("ui-accordion-content-active");var e=this.active.next(),f={options:d,newHeader:a([]),oldHeader:d.active,newContent:a([]),oldContent:e},g=this.active=a([]);this._toggle(g,e,f);return}var h=a(b.currentTarget||c),i=h[0]===this.active[0];d.active=d.collapsible&&i?!1:this.headers.index(h);if(this.running||!d.collapsible&&i)return;var j=this.active,g=h.next(),e=this.active.next(),f={options:d,newHeader:i&&d.collapsible?a([]):h,oldHeader:this.active,newContent:i&&d.collapsible?a([]):g,oldContent:e},k=this.headers.index(this.active[0])>this.headers.index(h[0]);this.active=i?a([]):h,this._toggle(g,e,f,i,k),j.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),i||(h.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected),h.next().addClass("ui-accordion-content-active"));return}},_toggle:function(b,c,d,e,f){var g=this,h=g.options;g.toShow=b,g.toHide=c,g.data=d;var i=function(){if(!!g)return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data),g.running=c.size()===0?b.size():c.size();if(h.animated){var j={};h.collapsible&&e?j={toShow:a([]),toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace}:j={toShow:b,toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace},h.proxied||(h.proxied=h.animated),h.proxiedDuration||(h.proxiedDuration=h.duration),h.animated=a.isFunction(h.proxied)?h.proxied(j):h.proxied,h.duration=a.isFunction(h.proxiedDuration)?h.proxiedDuration(j):h.proxiedDuration;var k=a.ui.accordion.animations,l=h.duration,m=h.animated;m&&!k[m]&&!a.easing[m]&&(m="slide"),k[m]||(k[m]=function(a){this.slide(a,{easing:m,duration:l||700})}),k[m](j)}else h.collapsible&&e?b.toggle():(c.hide(),b.show()),i(!0);c.prev().attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).blur(),b.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;this.running||(this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""}),this.toHide.removeClass("ui-accordion-content-active"),this.toHide.length&&(this.toHide.parent()[0].className=this.toHide.parent()[0].className),this._trigger("change",null,this.data))}}),a.extend(a.ui.accordion,{version:"1.8.18",animations:{slide:function(b,c){b=a.extend({easing:"swing",duration:300},b,c);if(!b.toHide.size())b.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},b);else{if(!b.toShow.size()){b.toHide.animate({height:"hide",paddingTop:"hide",paddingBottom:"hide"},b);return}var d=b.toShow.css("overflow"),e=0,f={},g={},h=["height","paddingTop","paddingBottom"],i,j=b.toShow;i=j[0].style.width,j.width(j.parent().width()-parseFloat(j.css("paddingLeft"))-parseFloat(j.css("paddingRight"))-(parseFloat(j.css("borderLeftWidth"))||0)-(parseFloat(j.css("borderRightWidth"))||0)),a.each(h,function(c,d){g[d]="hide";var e=(""+a.css(b.toShow[0],d)).match(/^([\d+-.]+)(.*)$/);f[d]={value:e[1],unit:e[2]||"px"}}),b.toShow.css({height:0,overflow:"hidden"}).show(),b.toHide.filter(":hidden").each(b.complete).end().filter(":visible").animate(g,{step:function(a,c){c.prop=="height"&&(e=c.end-c.start===0?0:(c.now-c.start)/(c.end-c.start)),b.toShow[0].style[c.prop]=e*f[c.prop].value+f[c.prop].unit},duration:b.duration,easing:b.easing,complete:function(){b.autoHeight||b.toShow.css("height",""),b.toShow.css({width:i,overflow:d}),b.complete()}})}},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1e3:200})}}})})(jQuery);/*
+ * jQuery UI Autocomplete 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ * jquery.ui.position.js
+ */(function(a,b){var c=0;a.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var b=this,c=this.element[0].ownerDocument,d;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!b.options.disabled&&!b.element.propAttr("readOnly")){d=!1;var e=a.ui.keyCode;switch(c.keyCode){case e.PAGE_UP:b._move("previousPage",c);break;case e.PAGE_DOWN:b._move("nextPage",c);break;case e.UP:b._move("previous",c),c.preventDefault();break;case e.DOWN:b._move("next",c),c.preventDefault();break;case e.ENTER:case e.NUMPAD_ENTER:b.menu.active&&(d=!0,c.preventDefault());case e.TAB:if(!b.menu.active)return;b.menu.select(c);break;case e.ESCAPE:b.element.val(b.term),b.close(c);break;default:clearTimeout(b.searching),b.searching=setTimeout(function(){b.term!=b.element.val()&&(b.selectedItem=null,b.search(null,c))},b.options.delay)}}}).bind("keypress.autocomplete",function(a){d&&(d=!1,a.preventDefault())}).bind("focus.autocomplete",function(){b.options.disabled||(b.selectedItem=null,b.previous=b.element.val())}).bind("blur.autocomplete",function(a){b.options.disabled||(clearTimeout(b.searching),b.closing=setTimeout(function(){b.close(a),b._change(a)},150))}),this._initSource(),this.response=function(){return b._response.apply(b,arguments)},this.menu=a("<ul></ul>").addClass("ui-autocomplete").appendTo(a(this.options.appendTo||"body",c)[0]).mousedown(function(c){var d=b.menu.element[0];a(c.target).closest(".ui-menu-item").length||setTimeout(function(){a(document).one("mousedown",function(c){c.target!==b.element[0]&&c.target!==d&&!a.ui.contains(d,c.target)&&b.close()})},1),setTimeout(function(){clearTimeout(b.closing)},13)}).menu({focus:function(a,c){var d=c.item.data("item.autocomplete");!1!==b._trigger("focus",a,{item:d})&&/^key/.test(a.originalEvent.type)&&b.element.val(d.value)},selected:function(a,d){var e=d.item.data("item.autocomplete"),f=b.previous;b.element[0]!==c.activeElement&&(b.element.focus(),b.previous=f,setTimeout(function(){b.previous=f,b.selectedItem=e},1)),!1!==b._trigger("select",a,{item:e})&&b.element.val(e.value),b.term=b.element.val(),b.close(a),b.selectedItem=e},blur:function(a,c){b.menu.element.is(":visible")&&b.element.val()!==b.term&&b.element.val(b.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"),a.fn.bgiframe&&this.menu.element.bgiframe(),b.beforeunloadHandler=function(){b.element.removeAttr("autocomplete")},a(window).bind("beforeunload",b.beforeunloadHandler)},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"),this.menu.element.remove(),a(window).unbind("beforeunload",this.beforeunloadHandler),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b==="source"&&this._initSource(),b==="appendTo"&&this.menu.element.appendTo(a(c||"body",this.element[0].ownerDocument)[0]),b==="disabled"&&c&&this.xhr&&this.xhr.abort()},_initSource:function(){var b=this,d,e;a.isArray(this.options.source)?(d=this.options.source,this.source=function(b,c){c(a.ui.autocomplete.filter(d,b.term))}):typeof this.options.source=="string"?(e=this.options.source,this.source=function(d,f){b.xhr&&b.xhr.abort(),b.xhr=a.ajax({url:e,data:d,dataType:"json",context:{autocompleteRequest:++c},success:function(a,b){this.autocompleteRequest===c&&f(a)},error:function(){this.autocompleteRequest===c&&f([])}})}):this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val(),this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search",b)!==!1)return this._search(a)},_search:function(a){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.source({term:a},this.response)},_response:function(a){!this.options.disabled&&a&&a.length?(a=this._normalize(a),this._suggest(a),this._trigger("open")):this.close(),this.pending--,this.pending||this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing),this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.deactivate(),this._trigger("close",a))},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(b){if(b.length&&b[0].label&&b[0].value)return b;return a.map(b,function(b){if(typeof b=="string")return{label:b,value:b};return a.extend({label:b.label||b.value,value:b.value||b.label},b)})},_suggest:function(b){var c=this.menu.element.empty().zIndex(this.element.zIndex()+1);this._renderMenu(c,b),this.menu.deactivate(),this.menu.refresh(),c.show(),this._resizeMenu(),c.position(a.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next(new a.Event("mouseover"))},_resizeMenu:function(){var a=this.menu.element;a.outerWidth(Math.max(a.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(b,c){var d=this;a.each(c,function(a,c){d._renderItem(b,c)})},_renderItem:function(b,c){return a("<li></li>").data("item.autocomplete",c).append(a("<a></a>").text(c.label)).appendTo(b)},_move:function(a,b){if(!this.menu.element.is(":visible"))this.search(null,b);else{if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term),this.menu.deactivate();return}this.menu[a](b)}},widget:function(){return this.menu.element}}),a.extend(a.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},filter:function(b,c){var d=new RegExp(a.ui.autocomplete.escapeRegex(c),"i");return a.grep(b,function(a){return d.test(a.label||a.value||a)})}})})(jQuery),function(a){a.widget("ui.menu",{_create:function(){var b=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(c){!a(c.target).closest(".ui-menu-item a").length||(c.preventDefault(),b.select(c))}),this.refresh()},refresh:function(){var b=this,c=this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem");c.children("a").addClass("ui-corner-all").attr("tabindex",-1).mouseenter(function(c){b.activate(c,a(this).parent())}).mouseleave(function(){b.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.scrollTop(),e=this.element.height();c<0?this.element.scrollTop(d+c):c>=e&&this.element.scrollTop(d+c-e+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end(),this._trigger("focus",a,{item:b})},deactivate:function(){!this.active||(this.active.children("a").removeClass("ui-state-hover").removeAttr("id"),this._trigger("blur"),this.active=null)},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(!this.active)this.activate(c,this.element.children(b));else{var d=this.active[a+"All"](".ui-menu-item").eq(0);d.length?this.activate(c,d):this.activate(c,this.element.children(b))}},nextPage:function(b){if(this.hasScroll()){if(!this.active||this.last()){this.activate(b,this.element.children(".ui-menu-item:first"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c-d+a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:last")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.last()?":first":":last"))},previousPage:function(b){if(this.hasScroll()){if(!this.active||this.first()){this.activate(b,this.element.children(".ui-menu-item:last"));return}var c=this.active.offset().top,d=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c+d-a(this).height();return b<10&&b>-10}),result.length||(result=this.element.children(".ui-menu-item:first")),this.activate(b,result)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.first()?":last":":first"))},hasScroll:function(){return this.element.height()<this.element[a.fn.prop?"prop":"attr"]("scrollHeight")},select:function(a){this._trigger("selected",a,{item:this.active})}})}(jQuery);/*
+ * jQuery UI Button 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */(function(a,b){var c,d,e,f,g="ui-button ui-widget ui-state-default ui-corner-all",h="ui-state-hover ui-state-active ",i="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",j=function(){var b=a(this).find(":ui-button");setTimeout(function(){b.button("refresh")},1)},k=function(b){var c=b.name,d=b.form,e=a([]);c&&(d?e=a(d).find("[name='"+c+"']"):e=a("[name='"+c+"']",b.ownerDocument).filter(function(){return!this.form}));return e};a.widget("ui.button",{options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset.button").bind("reset.button",j),typeof this.options.disabled!="boolean"?this.options.disabled=!!this.element.propAttr("disabled"):this.element.propAttr("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var b=this,h=this.options,i=this.type==="checkbox"||this.type==="radio",l="ui-state-hover"+(i?"":" ui-state-active"),m="ui-state-focus";h.label===null&&(h.label=this.buttonElement.html()),this.buttonElement.addClass(g).attr("role","button").bind("mouseenter.button",function(){h.disabled||(a(this).addClass("ui-state-hover"),this===c&&a(this).addClass("ui-state-active"))}).bind("mouseleave.button",function(){h.disabled||a(this).removeClass(l)}).bind("click.button",function(a){h.disabled&&(a.preventDefault(),a.stopImmediatePropagation())}),this.element.bind("focus.button",function(){b.buttonElement.addClass(m)}).bind("blur.button",function(){b.buttonElement.removeClass(m)}),i&&(this.element.bind("change.button",function(){f||b.refresh()}),this.buttonElement.bind("mousedown.button",function(a){h.disabled||(f=!1,d=a.pageX,e=a.pageY)}).bind("mouseup.button",function(a){!h.disabled&&(d!==a.pageX||e!==a.pageY)&&(f=!0)})),this.type==="checkbox"?this.buttonElement.bind("click.button",function(){if(h.disabled||f)return!1;a(this).toggleClass("ui-state-active"),b.buttonElement.attr("aria-pressed",b.element[0].checked)}):this.type==="radio"?this.buttonElement.bind("click.button",function(){if(h.disabled||f)return!1;a(this).addClass("ui-state-active"),b.buttonElement.attr("aria-pressed","true");var c=b.element[0];k(c).not(c).map(function(){return a(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown.button",function(){if(h.disabled)return!1;a(this).addClass("ui-state-active"),c=this,a(document).one("mouseup",function(){c=null})}).bind("mouseup.button",function(){if(h.disabled)return!1;a(this).removeClass("ui-state-active")}).bind("keydown.button",function(b){if(h.disabled)return!1;(b.keyCode==a.ui.keyCode.SPACE||b.keyCode==a.ui.keyCode.ENTER)&&a(this).addClass("ui-state-active")}).bind("keyup.button",function(){a(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(b){b.keyCode===a.ui.keyCode.SPACE&&a(this).click()})),this._setOption("disabled",h.disabled),this._resetButton()},_determineButtonType:function(){this.element.is(":checkbox")?this.type="checkbox":this.element.is(":radio")?this.type="radio":this.element.is("input")?this.type="input":this.type="button";if(this.type==="checkbox"||this.type==="radio"){var a=this.element.parents().filter(":last"),b="label[for='"+this.element.attr("id")+"']";this.buttonElement=a.find(b),this.buttonElement.length||(a=a.length?a.siblings():this.element.siblings(),this.buttonElement=a.filter(b),this.buttonElement.length||(this.buttonElement=a.find(b))),this.element.addClass("ui-helper-hidden-accessible");var c=this.element.is(":checked");c&&this.buttonElement.addClass("ui-state-active"),this.buttonElement.attr("aria-pressed",c)}else this.buttonElement=this.element},widget:function(){return this.buttonElement},destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(g+" "+h+" "+i).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title"),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments);b==="disabled"?c?this.element.propAttr("disabled",!0):this.element.propAttr("disabled",!1):this._resetButton()},refresh:function(){var b=this.element.is(":disabled");b!==this.options.disabled&&this._setOption("disabled",b),this.type==="radio"?k(this.element[0]).each(function(){a(this).is(":checked")?a(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):a(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","false")}):this.type==="checkbox"&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if(this.type==="input")this.options.label&&this.element.val(this.options.label);else{var b=this.buttonElement.removeClass(i),c=a("<span></span>",this.element[0].ownerDocument).addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary,f=[];d.primary||d.secondary?(this.options.text&&f.push("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary")),d.primary&&b.prepend("<span class='ui-button-icon-primary ui-icon "+d.primary+"'></span>"),d.secondary&&b.append("<span class='ui-button-icon-secondary ui-icon "+d.secondary+"'></span>"),this.options.text||(f.push(e?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||b.attr("title",c))):f.push("ui-button-text-only"),b.addClass(f.join(" "))}}}),a.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c),a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var b=this.element.css("direction")==="rtl";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(b?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(b?"ui-corner-left":"ui-corner-right").end().end()},destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"),a.Widget.prototype.destroy.call(this)}})})(jQuery);/*
+ * jQuery UI Dialog 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ * jquery.ui.button.js
+ * jquery.ui.draggable.js
+ * jquery.ui.mouse.js
+ * jquery.ui.position.js
+ * jquery.ui.resizable.js
+ */(function(a,b){var c="ui-dialog ui-widget ui-widget-content ui-corner-all ",d={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},e={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},f=a.attrFn||{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0,click:!0};a.widget("ui.dialog",{options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",collision:"fit",using:function(b){var c=a(this).css(b).offset().top;c<0&&a(this).css("top",b.top-c)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.options.title=this.options.title||this.originalTitle;var b=this,d=b.options,e=d.title||"&#160;",f=a.ui.dialog.getTitleId(b.element),g=(b.uiDialog=a("<div></div>")).appendTo(document.body).hide().addClass(c+d.dialogClass).css({zIndex:d.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(c){d.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}).attr({role:"dialog","aria-labelledby":f}).mousedown(function(a){b.moveToTop(!1,a)}),h=b.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g),i=(b.uiDialogTitlebar=a("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),j=a('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){j.addClass("ui-state-hover")},function(){j.removeClass("ui-state-hover")}).focus(function(){j.addClass("ui-state-focus")}).blur(function(){j.removeClass("ui-state-focus")}).click(function(a){b.close(a);return!1}).appendTo(i),k=(b.uiDialogTitlebarCloseText=a("<span></span>")).addClass("ui-icon ui-icon-closethick").text(d.closeText).appendTo(j),l=a("<span></span>").addClass("ui-dialog-title").attr("id",f).html(e).prependTo(i);a.isFunction(d.beforeclose)&&!a.isFunction(d.beforeClose)&&(d.beforeClose=d.beforeclose),i.find("*").add(i).disableSelection(),d.draggable&&a.fn.draggable&&b._makeDraggable(),d.resizable&&a.fn.resizable&&b._makeResizable(),b._createButtons(d.buttons),b._isOpen=!1,a.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy(),a.uiDialog.hide(),a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),a.uiDialog.remove(),a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(b){var c=this,d,e;if(!1!==c._trigger("beforeClose",b)){c.overlay&&c.overlay.destroy(),c.uiDialog.unbind("keypress.ui-dialog"),c._isOpen=!1,c.options.hide?c.uiDialog.hide(c.options.hide,function(){c._trigger("close",b)}):(c.uiDialog.hide(),c._trigger("close",b)),a.ui.dialog.overlay.resize(),c.options.modal&&(d=0,a(".ui-dialog").each(function(){this!==c.uiDialog[0]&&(e=a(this).css("z-index"),isNaN(e)||(d=Math.max(d,e)))}),a.ui.dialog.maxZ=d);return c}},isOpen:function(){return this._isOpen},moveToTop:function(b,c){var d=this,e=d.options,f;if(e.modal&&!b||!e.stack&&!e.modal)return d._trigger("focus",c);e.zIndex>a.ui.dialog.maxZ&&(a.ui.dialog.maxZ=e.zIndex),d.overlay&&(a.ui.dialog.maxZ+=1,d.overlay.$el.css("z-index",a.ui.dialog.overlay.maxZ=a.ui.dialog.maxZ)),f={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()},a.ui.dialog.maxZ+=1,d.uiDialog.css("z-index",a.ui.dialog.maxZ),d.element.attr(f),d._trigger("focus",c);return d},open:function(){if(!this._isOpen){var b=this,c=b.options,d=b.uiDialog;b.overlay=c.modal?new a.ui.dialog.overlay(b):null,b._size(),b._position(c.position),d.show(c.show),b.moveToTop(!0),c.modal&&d.bind("keydown.ui-dialog",function(b){if(b.keyCode===a.ui.keyCode.TAB){var c=a(":tabbable",this),d=c.filter(":first"),e=c.filter(":last");if(b.target===e[0]&&!b.shiftKey){d.focus(1);return!1}if(b.target===d[0]&&b.shiftKey){e.focus(1);return!1}}}),a(b.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus(),b._isOpen=!0,b._trigger("open");return b}},_createButtons:function(b){var c=this,d=!1,e=a("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=a("<div></div>").addClass("ui-dialog-buttonset").appendTo(e);c.uiDialog.find(".ui-dialog-buttonpane").remove(),typeof b=="object"&&b!==null&&a.each(b,function(){return!(d=!0)}),d&&(a.each(b,function(b,d){d=a.isFunction(d)?{click:d,text:b}:d;var e=a('<button type="button"></button>').click(function(){d.click.apply(c.element[0],arguments)}).appendTo(g);a.each(d,function(a,b){a!=="click"&&(a in f?e[a](b):e.attr(a,b))}),a.fn.button&&e.button()}),e.appendTo(c.uiDialog))},_makeDraggable:function(){function f(a){return{position:a.position,offset:a.offset}}var b=this,c=b.options,d=a(document),e;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(d,g){e=c.height==="auto"?"auto":a(this).height(),a(this).height(a(this).height()).addClass("ui-dialog-dragging"),b._trigger("dragStart",d,f(g))},drag:function(a,c){b._trigger("drag",a,f(c))},stop:function(g,h){c.position=[h.position.left-d.scrollLeft(),h.position.top-d.scrollTop()],a(this).removeClass("ui-dialog-dragging").height(e),b._trigger("dragStop",g,f(h)),a.ui.dialog.overlay.resize()}})},_makeResizable:function(c){function h(a){return{originalPosition:a.originalPosition,originalSize:a.originalSize,position:a.position,size:a.size}}c=c===b?this.options.resizable:c;var d=this,e=d.options,f=d.uiDialog.css("position"),g=typeof c=="string"?c:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:g,start:function(b,c){a(this).addClass("ui-dialog-resizing"),d._trigger("resizeStart",b,h(c))},resize:function(a,b){d._trigger("resize",a,h(b))},stop:function(b,c){a(this).removeClass("ui-dialog-resizing"),e.height=a(this).height(),e.width=a(this).width(),d._trigger("resizeStop",b,h(c)),a.ui.dialog.overlay.resize()}}).css("position",f).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(b){var c=[],d=[0,0],e;if(b){if(typeof b=="string"||typeof b=="object"&&"0"in b)c=b.split?b.split(" "):[b[0],b[1]],c.length===1&&(c[1]=c[0]),a.each(["left","top"],function(a,b){+c[a]===c[a]&&(d[a]=c[a],c[a]=b)}),b={my:c.join(" "),at:c.join(" "),offset:d.join(" ")};b=a.extend({},a.ui.dialog.prototype.options.position,b)}else b=a.ui.dialog.prototype.options.position;e=this.uiDialog.is(":visible"),e||this.uiDialog.show(),this.uiDialog.css({top:0,left:0}).position(a.extend({of:window},b)),e||this.uiDialog.hide()},_setOptions:function(b){var c=this,f={},g=!1;a.each(b,function(a,b){c._setOption(a,b),a in d&&(g=!0),a in e&&(f[a]=b)}),g&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",f)},_setOption:function(b,d){var e=this,f=e.uiDialog;switch(b){case"beforeclose":b="beforeClose";break;case"buttons":e._createButtons(d);break;case"closeText":e.uiDialogTitlebarCloseText.text(""+d);break;case"dialogClass":f.removeClass(e.options.dialogClass).addClass(c+d);break;case"disabled":d?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case"draggable":var g=f.is(":data(draggable)");g&&!d&&f.draggable("destroy"),!g&&d&&e._makeDraggable();break;case"position":e._position(d);break;case"resizable":var h=f.is(":data(resizable)");h&&!d&&f.resizable("destroy"),h&&typeof d=="string"&&f.resizable("option","handles",d),!h&&d!==!1&&e._makeResizable(d);break;case"title":a(".ui-dialog-title",e.uiDialogTitlebar).html(""+(d||"&#160;"))}a.Widget.prototype._setOption.apply(e,arguments)},_size:function(){var b=this.options,c,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),b.minWidth>b.width&&(b.width=b.minWidth),c=this.uiDialog.css({height:"auto",width:b.width}).height(),d=Math.max(0,b.minHeight-c);if(b.height==="auto")if(a.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();var f=this.element.css("height","auto").height();e||this.uiDialog.hide(),this.element.height(Math.max(f,d))}else this.element.height(Math.max(b.height-c,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),a.extend(a.ui.dialog,{version:"1.8.18",uuid:0,maxZ:0,getTitleId:function(a){var b=a.attr("id");b||(this.uuid+=1,b=this.uuid);return"ui-dialog-title-"+b},overlay:function(b){this.$el=a.ui.dialog.overlay.create(b)}}),a.extend(a.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:a.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "),create:function(b){this.instances.length===0&&(setTimeout(function(){a.ui.dialog.overlay.instances.length&&a(document).bind(a.ui.dialog.overlay.events,function(b){if(a(b.target).zIndex()<a.ui.dialog.overlay.maxZ)return!1})},1),a(document).bind("keydown.dialog-overlay",function(c){b.options.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}),a(window).bind("resize.dialog-overlay",a.ui.dialog.overlay.resize));var c=(this.oldInstances.pop()||a("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});a.fn.bgiframe&&c.bgiframe(),this.instances.push(c);return c},destroy:function(b){var c=a.inArray(b,this.instances);c!=-1&&this.oldInstances.push(this.instances.splice(c,1)[0]),this.instances.length===0&&a([document,window]).unbind(".dialog-overlay"),b.remove();var d=0;a.each(this.instances,function(){d=Math.max(d,this.css("z-index"))}),this.maxZ=d},height:function(){var b,c;if(a.browser.msie&&a.browser.version<7){b=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),c=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return b<c?a(window).height()+"px":b+"px"}return a(document).height()+"px"},width:function(){var b,c;if(a.browser.msie){b=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth),c=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth);return b<c?a(window).width()+"px":b+"px"}return a(document).width()+"px"},resize:function(){var b=a([]);a.each(a.ui.dialog.overlay.instances,function(){b=b.add(this)}),b.css({width:0,height:0}).css({width:a.ui.dialog.overlay.width(),height:a.ui.dialog.overlay.height()})}}),a.extend(a.ui.dialog.overlay.prototype,{destroy:function(){a.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);/*
+ * jQuery UI Slider 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.mouse.js
+ * jquery.ui.widget.js
+ */(function(a,b){var c=5;a.widget("ui.slider",a.ui.mouse,{widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var b=this,d=this.options,e=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),f="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",g=d.values&&d.values.length||1,h=[];this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(d.disabled?" ui-slider-disabled ui-disabled":"")),this.range=a([]),d.range&&(d.range===!0&&(d.values||(d.values=[this._valueMin(),this._valueMin()]),d.values.length&&d.values.length!==2&&(d.values=[d.values[0],d.values[0]])),this.range=a("<div></div>").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(d.range==="min"||d.range==="max"?" ui-slider-range-"+d.range:"")));for(var i=e.length;i<g;i+=1)h.push(f);this.handles=e.add(a(h.join("")).appendTo(b.element)),this.handle=this.handles.eq(0),this.handles.add(this.range).filter("a").click(function(a){a.preventDefault()}).hover(function(){d.disabled||a(this).addClass("ui-state-hover")},function(){a(this).removeClass("ui-state-hover")}).focus(function(){d.disabled?a(this).blur():(a(".ui-slider .ui-state-focus").removeClass("ui-state-focus"),a(this).addClass("ui-state-focus"))}).blur(function(){a(this).removeClass("ui-state-focus")}),this.handles.each(function(b){a(this).data("index.ui-slider-handle",b)}),this.handles.keydown(function(d){var e=a(this).data("index.ui-slider-handle"),f,g,h,i;if(!b.options.disabled){switch(d.keyCode){case a.ui.keyCode.HOME:case a.ui.keyCode.END:case a.ui.keyCode.PAGE_UP:case a.ui.keyCode.PAGE_DOWN:case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:d.preventDefault();if(!b._keySliding){b._keySliding=!0,a(this).addClass("ui-state-active"),f=b._start(d,e);if(f===!1)return}}i=b.options.step,b.options.values&&b.options.values.length?g=h=b.values(e):g=h=b.value();switch(d.keyCode){case a.ui.keyCode.HOME:h=b._valueMin();break;case a.ui.keyCode.END:h=b._valueMax();break;case a.ui.keyCode.PAGE_UP:h=b._trimAlignValue(g+(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.PAGE_DOWN:h=b._trimAlignValue(g-(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:if(g===b._valueMax())return;h=b._trimAlignValue(g+i);break;case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:if(g===b._valueMin())return;h=b._trimAlignValue(g-i)}b._slide(d,e,h)}}).keyup(function(c){var d=a(this).data("index.ui-slider-handle");b._keySliding&&(b._keySliding=!1,b._stop(c,d),b._change(c,d),a(this).removeClass("ui-state-active"))}),this._refreshValue(),this._animateOff=!1},destroy:function(){this.handles.remove(),this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"),this._mouseDestroy();return this},_mouseCapture:function(b){var c=this.options,d,e,f,g,h,i,j,k,l;if(c.disabled)return!1;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),d={x:b.pageX,y:b.pageY},e=this._normValueFromMouse(d),f=this._valueMax()-this._valueMin()+1,h=this,this.handles.each(function(b){var c=Math.abs(e-h.values(b));f>c&&(f=c,g=a(this),i=b)}),c.range===!0&&this.values(1)===c.min&&(i+=1,g=a(this.handles[i])),j=this._start(b,i);if(j===!1)return!1;this._mouseSliding=!0,h._handleIndex=i,g.addClass("ui-state-active").focus(),k=g.offset(),l=!a(b.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:b.pageX-k.left-g.width()/2,top:b.pageY-k.top-g.height()/2-(parseInt(g.css("borderTopWidth"),10)||0)-(parseInt(g.css("borderBottomWidth"),10)||0)+(parseInt(g.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(b,i,e),this._animateOff=!0;return!0},_mouseStart:function(a){return!0},_mouseDrag:function(a){var b={x:a.pageX,y:a.pageY},c=this._normValueFromMouse(b);this._slide(a,this._handleIndex,c);return!1},_mouseStop:function(a){this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(a,this._handleIndex),this._change(a,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1;return!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b,c,d,e,f;this.orientation==="horizontal"?(b=this.elementSize.width,c=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(b=this.elementSize.height,c=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),d=c/b,d>1&&(d=1),d<0&&(d=0),this.orientation==="vertical"&&(d=1-d),e=this._valueMax()-this._valueMin(),f=this._valueMin()+d*e;return this._trimAlignValue(f)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values());return this._trigger("start",a,c)},_slide:function(a,b,c){var d,e,f;this.options.values&&this.options.values.length?(d=this.values(b?0:1),this.options.values.length===2&&this.options.range===!0&&(b===0&&c>d||b===1&&c<d)&&(c=d),c!==this.values(b)&&(e=this.values(),e[b]=c,f=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e}),d=this.values(b?0:1),f!==!1&&this.values(b,c,!0))):c!==this.value()&&(f=this._trigger("slide",a,{handle:this.handles[b],value:c}),f!==!1&&this.value(c))},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("change",a,c)}},value:function(a){if(arguments.length)this.options.value=this._trimAlignValue(a),this._refreshValue(),this._change(null,0);else return this._value()},values:function(b,c){var d,e,f;if(arguments.length>1)this.options.values[b]=this._trimAlignValue(c),this._refreshValue(),this._change(null,b);else{if(!arguments.length)return this._values();if(!a.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(b):this.value();d=this.options.values,e=arguments[0];for(f=0;f<d.length;f+=1)d[f]=this._trimAlignValue(e[f]),this._change(null,f);this._refreshValue()}},_setOption:function(b,c){var d,e=0;a.isArray(this.options.values)&&(e=this.options.values.length),a.Widget.prototype._setOption.apply(this,arguments);switch(b){case"disabled":c?(this.handles.filter(".ui-state-focus").blur(),this.handles.removeClass("ui-state-hover"),this.handles.propAttr("disabled",!0),this.element.addClass("ui-disabled")):(this.handles.propAttr("disabled",!1),this.element.removeClass("ui-disabled"));break;case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":this._animateOff=!0,this._refreshValue();for(d=0;d<e;d+=1)this._change(null,d);this._animateOff=!1}},_value:function(){var a=this.options.value;a=this._trimAlignValue(a);return a},_values:function(a){var b,c,d;if(arguments.length){b=this.options.values[a],b=this._trimAlignValue(b);return b}c=this.options.values.slice();for(d=0;d<c.length;d+=1)c[d]=this._trimAlignValue(c[d]);return c},_trimAlignValue:function(a){if(a<=this._valueMin())return this._valueMin();if(a>=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b,d=a-c;Math.abs(c)*2>=b&&(d+=c>0?b:-b);return parseFloat(d.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var b=this.options.range,c=this.options,d=this,e=this._animateOff?!1:c.animate,f,g={},h,i,j,k;this.options.values&&this.options.values.length?this.handles.each(function(b,i){f=(d.values(b)-d._valueMin())/(d._valueMax()-d._valueMin())*100,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",a(this).stop(1,1)[e?"animate":"css"](g,c.animate),d.options.range===!0&&(d.orientation==="horizontal"?(b===0&&d.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({width:f-h+"%"},{queue:!1,duration:c.animate})):(b===0&&d.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({height:f-h+"%"},{queue:!1,duration:c.animate}))),h=f}):(i=this.value(),j=this._valueMin(),k=this._valueMax(),f=k!==j?(i-j)/(k-j)*100:0,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",this.handle.stop(1,1)[e?"animate":"css"](g,c.animate),b==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},c.animate),b==="max"&&this.orientation==="horizontal"&&this.range[e?"animate":"css"]({width:100-f+"%"},{queue:!1,duration:c.animate}),b==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},c.animate),b==="max"&&this.orientation==="vertical"&&this.range[e?"animate":"css"]({height:100-f+"%"},{queue:!1,duration:c.animate}))}}),a.extend(a.ui.slider,{version:"1.8.18"})})(jQuery);/*
+ * jQuery UI Tabs 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */(function(a,b){function f(){return++d}function e(){return++c}var c=0,d=0;a.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:!1,cookie:null,collapsible:!1,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"<div></div>",remove:null,select:null,show:null,spinner:"<em>Loading&#8230;</em>",tabTemplate:"<li><a href='#{href}'><span>#{label}</span></a></li>"},_create:function(){this._tabify(!0)},_setOption:function(a,b){if(a=="selected"){if(this.options.collapsible&&b==this.options.selected)return;this.select(b)}else this.options[a]=b,this._tabify()},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+e()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+f());return a.cookie.apply(null,[b].concat(a.makeArray(arguments)))},_ui:function(a,b){return{tab:a,panel:b,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=a(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(c){function m(b,c){b.css("display",""),!a.support.opacity&&c.opacity&&b[0].style.removeAttribute("filter")}var d=this,e=this.options,f=/^#.+/;this.list=this.element.find("ol,ul").eq(0),this.lis=a(" > li:has(a[href])",this.list),this.anchors=this.lis.map(function(){return a("a",this)[0]}),this.panels=a([]),this.anchors.each(function(b,c){var g=a(c).attr("href"),h=g.split("#")[0],i;h&&(h===location.toString().split("#")[0]||(i=a("base")[0])&&h===i.href)&&(g=c.hash,c.href=g);if(f.test(g))d.panels=d.panels.add(d.element.find(d._sanitizeSelector(g)));else if(g&&g!=="#"){a.data(c,"href.tabs",g),a.data(c,"load.tabs",g.replace(/#.*$/,""));var j=d._tabId(c);c.href="#"+j;var k=d.element.find("#"+j);k.length||(k=a(e.panelTemplate).attr("id",j).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(d.panels[b-1]||d.list),k.data("destroy.tabs",!0)),d.panels=d.panels.add(k)}else e.disabled.push(b)}),c?(this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"),this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.lis.addClass("ui-state-default ui-corner-top"),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom"),e.selected===b?(location.hash&&this.anchors.each(function(a,b){if(b.hash==location.hash){e.selected=a;return!1}}),typeof e.selected!="number"&&e.cookie&&(e.selected=parseInt(d._cookie(),10)),typeof e.selected!="number"&&this.lis.filter(".ui-tabs-selected").length&&(e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))),e.selected=e.selected||(this.lis.length?0:-1)):e.selected===null&&(e.selected=-1),e.selected=e.selected>=0&&this.anchors[e.selected]||e.selected<0?e.selected:0,e.disabled=a.unique(e.disabled.concat(a.map(this.lis.filter(".ui-state-disabled"),function(a,b){return d.lis.index(a)}))).sort(),a.inArray(e.selected,e.disabled)!=-1&&e.disabled.splice(a.inArray(e.selected,e.disabled),1),this.panels.addClass("ui-tabs-hide"),this.lis.removeClass("ui-tabs-selected ui-state-active"),e.selected>=0&&this.anchors.length&&(d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash)).removeClass("ui-tabs-hide"),this.lis.eq(e.selected).addClass("ui-tabs-selected ui-state-active"),d.element.queue("tabs",function(){d._trigger("show",null,d._ui(d.anchors[e.selected],d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash))[0]))}),this.load(e.selected)),a(window).bind("unload",function(){d.lis.add(d.anchors).unbind(".tabs"),d.lis=d.anchors=d.panels=null})):e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")),this.element[e.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible"),e.cookie&&this._cookie(e.selected,e.cookie);for(var g=0,h;h=this.lis[g];g++)a(h)[a.inArray(g,e.disabled)!=-1&&!a(h).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");e.cache===!1&&this.anchors.removeData("cache.tabs"),this.lis.add(this.anchors).unbind(".tabs");if(e.event!=="mouseover"){var i=function(a,b){b.is(":not(.ui-state-disabled)")&&b.addClass("ui-state-"+a)},j=function(a,b){b.removeClass("ui-state-"+a)};this.lis.bind("mouseover.tabs",function(){i("hover",a(this))}),this.lis.bind("mouseout.tabs",function(){j("hover",a(this))}),this.anchors.bind("focus.tabs",function(){i("focus",a(this).closest("li"))}),this.anchors.bind("blur.tabs",function(){j("focus",a(this).closest("li"))})}var k,l;e.fx&&(a.isArray(e.fx)?(k=e.fx[0],l=e.fx[1]):k=l=e.fx);var n=l?function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.hide().removeClass("ui-tabs-hide").animate(l,l.duration||"normal",function(){m(c,l),d._trigger("show",null,d._ui(b,c[0]))})}:function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.removeClass("ui-tabs-hide"),d._trigger("show",null,d._ui(b,c[0]))},o=k?function(a,b){b.animate(k,k.duration||"normal",function(){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),m(b,k),d.element.dequeue("tabs")})}:function(a,b,c){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),d.element.dequeue("tabs")};this.anchors.bind(e.event+".tabs",function(){var b=this,c=a(b).closest("li"),f=d.panels.filter(":not(.ui-tabs-hide)"),g=d.element.find(d._sanitizeSelector(b.hash));if(c.hasClass("ui-tabs-selected")&&!e.collapsible||c.hasClass("ui-state-disabled")||c.hasClass("ui-state-processing")||d.panels.filter(":animated").length||d._trigger("select",null,d._ui(this,g[0]))===!1){this.blur();return!1}e.selected=d.anchors.index(this),d.abort();if(e.collapsible){if(c.hasClass("ui-tabs-selected")){e.selected=-1,e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){o(b,f)}).dequeue("tabs"),this.blur();return!1}if(!f.length){e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this)),this.blur();return!1}}e.cookie&&d._cookie(e.selected,e.cookie);if(g.length)f.length&&d.element.queue("tabs",function(){o(b,f)}),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this));else throw"jQuery UI Tabs: Mismatching fragment identifier.";a.browser.msie&&this.blur()}),this.anchors.bind("click.tabs",function(){return!1})},_getIndex:function(a){typeof a=="string"&&(a=this.anchors.index(this.anchors.filter("[href$="+a+"]")));return a},destroy:function(){var b=this.options;this.abort(),this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs"),this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.anchors.each(function(){var b=a.data(this,"href.tabs");b&&(this.href=b);var c=a(this).unbind(".tabs");a.each(["href","load","cache"],function(a,b){c.removeData(b+".tabs")})}),this.lis.unbind(".tabs").add(this.panels).each(function(){a.data(this,"destroy.tabs")?a(this).remove():a(this).removeClass(["ui-state-default","ui-corner-top","ui-tabs-selected","ui-state-active","ui-state-hover","ui-state-focus","ui-state-disabled","ui-tabs-panel","ui-widget-content","ui-corner-bottom","ui-tabs-hide"].join(" "))}),b.cookie&&this._cookie(null,b.cookie);return this},add:function(c,d,e){e===b&&(e=this.anchors.length);var f=this,g=this.options,h=a(g.tabTemplate.replace(/#\{href\}/g,c).replace(/#\{label\}/g,d)),i=c.indexOf("#")?this._tabId(a("a",h)[0]):c.replace("#","");h.addClass("ui-state-default ui-corner-top").data("destroy.tabs",!0);var j=f.element.find("#"+i);j.length||(j=a(g.panelTemplate).attr("id",i).data("destroy.tabs",!0)),j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide"),e>=this.lis.length?(h.appendTo(this.list),j.appendTo(this.list[0].parentNode)):(h.insertBefore(this.lis[e]),j.insertBefore(this.panels[e])),g.disabled=a.map(g.disabled,function(a,b){return a>=e?++a:a}),this._tabify(),this.anchors.length==1&&(g.selected=0,h.addClass("ui-tabs-selected ui-state-active"),j.removeClass("ui-tabs-hide"),this.element.queue("tabs",function(){f._trigger("show",null,f._ui(f.anchors[0],f.panels[0]))}),this.load(0)),this._trigger("add",null,this._ui(this.anchors[e],this.panels[e]));return this},remove:function(b){b=this._getIndex(b);var c=this.options,d=this.lis.eq(b).remove(),e=this.panels.eq(b).remove();d.hasClass("ui-tabs-selected")&&this.anchors.length>1&&this.select(b+(b+1<this.anchors.length?1:-1)),c.disabled=a.map(a.grep(c.disabled,function(a,c){return a!=b}),function(a,c){return a>=b?--a:a}),this._tabify(),this._trigger("remove",null,this._ui(d.find("a")[0],e[0]));return this},enable:function(b){b=this._getIndex(b);var c=this.options;if(a.inArray(b,c.disabled)!=-1){this.lis.eq(b).removeClass("ui-state-disabled"),c.disabled=a.grep(c.disabled,function(a,c){return a!=b}),this._trigger("enable",null,this._ui(this.anchors[b],this.panels[b]));return this}},disable:function(a){a=this._getIndex(a);var b=this,c=this.options;a!=c.selected&&(this.lis.eq(a).addClass("ui-state-disabled"),c.disabled.push(a),c.disabled.sort(),this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a])));return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this},load:function(b){b=this._getIndex(b);var c=this,d=this.options,e=this.anchors.eq(b)[0],f=a.data(e,"load.tabs");this.abort();if(!f||this.element.queue("tabs").length!==0&&a.data(e,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(b).addClass("ui-state-processing");if(d.spinner){var g=a("span",e);g.data("label.tabs",g.html()).html(d.spinner)}this.xhr=a.ajax(a.extend({},d.ajaxOptions,{url:f,success:function(f,g){c.element.find(c._sanitizeSelector(e.hash)).html(f),c._cleanup(),d.cache&&a.data(e,"cache.tabs",!0),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.success(f,g)}catch(h){}},error:function(a,f,g){c._cleanup(),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.error(a,f,b,e)}catch(g){}}})),c.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]),this.panels.stop(!1,!0),this.element.queue("tabs",this.element.queue("tabs").splice(-2,2)),this.xhr&&(this.xhr.abort(),delete this.xhr),this._cleanup();return this},url:function(a,b){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",b);return this},length:function(){return this.anchors.length}}),a.extend(a.ui.tabs,{version:"1.8.18"}),a.extend(a.ui.tabs.prototype,{rotation:null,rotate:function(a,b){var c=this,d=this.options,e=c._rotate||(c._rotate=function(b){clearTimeout(c.rotation),c.rotation=setTimeout(function(){var a=d.selected;c.select(++a<c.anchors.length?a:0)},a),b&&b.stopPropagation()}),f=c._unrotate||(c._unrotate=b?function(a){t=d.selected,e()}:function(a){a.clientX&&c.rotate(null)});a?(this.element.bind("tabsshow",e),this.anchors.bind(d.event+".tabs",f),e()):(clearTimeout(c.rotation),this.element.unbind("tabsshow",e),this.anchors.unbind(d.event+".tabs",f),delete this._rotate,delete this._unrotate);return this}})})(jQuery);/*
+ * jQuery UI Datepicker 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker
+ *
+ * Depends:
+ * jquery.ui.core.js
+ */(function($,undefined){function isArray(a){return a&&($.browser.safari&&typeof a=="object"&&a.length||a.constructor&&a.constructor.toString().match(/\Array\(\)/))}function extendRemove(a,b){$.extend(a,b);for(var c in b)if(b[c]==null||b[c]==undefined)a[c]=b[c];return a}function bindHover(a){var b="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return a.bind("mouseout",function(a){var c=$(a.target).closest(b);!c.length||c.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(c){var d=$(c.target).closest(b);!$.datepicker._isDisabledDatepicker(instActive.inline?a.parent()[0]:instActive.input[0])&&!!d.length&&(d.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),d.addClass("ui-state-hover"),d.hasClass("ui-datepicker-prev")&&d.addClass("ui-datepicker-prev-hover"),d.hasClass("ui-datepicker-next")&&d.addClass("ui-datepicker-next-hover"))})}function Datepicker(){this.debug=!1,this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},$.extend(this._defaults,this.regional[""]),this.dpDiv=bindHover($('<div id="'+this._mainDivId+'" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}$.extend($.ui,{datepicker:{version:"1.8.18"}});var PROP_NAME="datepicker",dpuuid=(new Date).getTime(),instActive;$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){extendRemove(this._defaults,a||{});return this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase(),inline=nodeName=="div"||nodeName=="span";target.id||(this.uuid+=1,target.id="dp"+this.uuid);var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{}),nodeName=="input"?this._connectDatepicker(target,inst):inline&&this._inlineDatepicker(target,inst)},_newInst:function(a,b){var c=a[0].id.replace(/([^A-Za-z0-9_-])/g,"\\\\$1");return{id:c,input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:b?bindHover($('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>')):this.dpDiv}},_connectDatepicker:function(a,b){var c=$(a);b.append=$([]),b.trigger=$([]);c.hasClass(this.markerClassName)||(this._attachments(c,b),c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),this._autoSize(b),$.data(a,PROP_NAME,b),b.settings.disabled&&this._disableDatepicker(a))},_attachments:function(a,b){var c=this._get(b,"appendText"),d=this._get(b,"isRTL");b.append&&b.append.remove(),c&&(b.append=$('<span class="'+this._appendClass+'">'+c+"</span>"),a[d?"before":"after"](b.append)),a.unbind("focus",this._showDatepicker),b.trigger&&b.trigger.remove();var e=this._get(b,"showOn");(e=="focus"||e=="both")&&a.focus(this._showDatepicker);if(e=="button"||e=="both"){var f=this._get(b,"buttonText"),g=this._get(b,"buttonImage");b.trigger=$(this._get(b,"buttonImageOnly")?$("<img/>").addClass(this._triggerClass).attr({src:g,alt:f,title:f}):$('<button type="button"></button>').addClass(this._triggerClass).html(g==""?f:$("<img/>").attr({src:g,alt:f,title:f}))),a[d?"before":"after"](b.trigger),b.trigger.click(function(){$.datepicker._datepickerShowing&&$.datepicker._lastInput==a[0]?$.datepicker._hideDatepicker():$.datepicker._datepickerShowing&&$.datepicker._lastInput!=a[0]?($.datepicker._hideDatepicker(),$.datepicker._showDatepicker(a[0])):$.datepicker._showDatepicker(a[0]);return!1})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var d=function(a){var b=0,c=0;for(var d=0;d<a.length;d++)a[d].length>b&&(b=a[d].length,c=d);return c};b.setMonth(d(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort"))),b.setDate(d(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=$(a);c.hasClass(this.markerClassName)||(c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),$.data(a,PROP_NAME,b),this._setDate(b,this._getDefaultDate(b),!0),this._updateDatepicker(b),this._updateAlternate(b),b.settings.disabled&&this._disableDatepicker(a),b.dpDiv.css("display","block"))},_dialogDatepicker:function(a,b,c,d,e){var f=this._dialogInst;if(!f){this.uuid+=1;var g="dp"+this.uuid;this._dialogInput=$('<input type="text" id="'+g+'" style="position: absolute; top: -100px; width: 0px; z-index: -10;"/>'),this._dialogInput.keydown(this._doKeyDown),$("body").append(this._dialogInput),f=this._dialogInst=this._newInst(this._dialogInput,!1),f.settings={},$.data(this._dialogInput[0],PROP_NAME,f)}extendRemove(f.settings,d||{}),b=b&&b.constructor==Date?this._formatDate(f,b):b,this._dialogInput.val(b),this._pos=e?e.length?e:[e.pageX,e.pageY]:null;if(!this._pos){var h=document.documentElement.clientWidth,i=document.documentElement.clientHeight,j=document.documentElement.scrollLeft||document.body.scrollLeft,k=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[h/2-100+j,i/2-150+k]}this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),f.settings.onSelect=c,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),$.blockUI&&$.blockUI(this.dpDiv),$.data(this._dialogInput[0],PROP_NAME,f);return this},_destroyDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!!b.hasClass(this.markerClassName)){var d=a.nodeName.toLowerCase();$.removeData(a,PROP_NAME),d=="input"?(c.append.remove(),c.trigger.remove(),b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):(d=="div"||d=="span")&&b.removeClass(this.markerClassName).empty()}},_enableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!!b.hasClass(this.markerClassName)){var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!1,c.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().removeClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b})}},_disableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!!b.hasClass(this.markerClassName)){var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!0,c.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().addClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b}),this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return!1;for(var b=0;b<this._disabledInputs.length;b++)if(this._disabledInputs[b]==a)return!0;return!1},_getInst:function(a){try{return $.data(a,PROP_NAME)}catch(b){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(a,b,c){var d=this._getInst(a);if(arguments.length==2&&typeof b=="string")return b=="defaults"?$.extend({},$.datepicker._defaults):d?b=="all"?$.extend({},d.settings):this._get(d,b):null;var e=b||{};typeof b=="string"&&(e={},e[b]=c);if(d){this._curInst==d&&this._hideDatepicker();var f=this._getDateDatepicker(a,!0),g=this._getMinMaxDate(d,"min"),h=this._getMinMaxDate(d,"max");extendRemove(d.settings,e),g!==null&&e.dateFormat!==undefined&&e.minDate===undefined&&(d.settings.minDate=this._formatDate(d,g)),h!==null&&e.dateFormat!==undefined&&e.maxDate===undefined&&(d.settings.maxDate=this._formatDate(d,h)),this._attachments($(a),d),this._autoSize(d),this._setDate(d,f),this._updateAlternate(d),this._updateDatepicker(d)}},_changeDatepicker:function(a,b,c){this._optionDatepicker(a,b,c)},_refreshDatepicker:function(a){var b=this._getInst(a);b&&this._updateDatepicker(b)},_setDateDatepicker:function(a,b){var c=this._getInst(a);c&&(this._setDate(c,b),this._updateDatepicker(c),this._updateAlternate(c))},_getDateDatepicker:function(a,b){var c=this._getInst(a);c&&!c.inline&&this._setDateFromField(c,b);return c?this._getDate(c):null},_doKeyDown:function(a){var b=$.datepicker._getInst(a.target),c=!0,d=b.dpDiv.is(".ui-datepicker-rtl");b._keyEvent=!0;if($.datepicker._datepickerShowing)switch(a.keyCode){case 9:$.datepicker._hideDatepicker(),c=!1;break;case 13:var e=$("td."+$.datepicker._dayOverClass+":not(."+$.datepicker._currentClass+")",b.dpDiv);e[0]&&$.datepicker._selectDay(a.target,b.selectedMonth,b.selectedYear,e[0]);var f=$.datepicker._get(b,"onSelect");if(f){var g=$.datepicker._formatDate(b);f.apply(b.input?b.input[0]:null,[g,b])}else $.datepicker._hideDatepicker();return!1;case 27:$.datepicker._hideDatepicker();break;case 33:$.datepicker._adjustDate(a.target,a.ctrlKey?-$.datepicker._get(b,"stepBigMonths"):-$.datepicker._get(b,"stepMonths"),"M");break;case 34:$.datepicker._adjustDate(a.target,a.ctrlKey?+$.datepicker._get(b,"stepBigMonths"):+$.datepicker._get(b,"stepMonths"),"M");break;case 35:(a.ctrlKey||a.metaKey)&&$.datepicker._clearDate(a.target),c=a.ctrlKey||a.metaKey;break;case 36:(a.ctrlKey||a.metaKey)&&$.datepicker._gotoToday(a.target),c=a.ctrlKey||a.metaKey;break;case 37:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,d?1:-1,"D"),c=a.ctrlKey||a.metaKey,a.originalEvent.altKey&&$.datepicker._adjustDate(a.target,a.ctrlKey?-$.datepicker._get(b,"stepBigMonths"):-$.datepicker._get(b,"stepMonths"),"M");break;case 38:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,-7,"D"),c=a.ctrlKey||a.metaKey;break;case 39:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,d?-1:1,"D"),c=a.ctrlKey||a.metaKey,a.originalEvent.altKey&&$.datepicker._adjustDate(a.target,a.ctrlKey?+$.datepicker._get(b,"stepBigMonths"):+$.datepicker._get(b,"stepMonths"),"M");break;case 40:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,7,"D"),c=a.ctrlKey||a.metaKey;break;default:c=!1}else a.keyCode==36&&a.ctrlKey?$.datepicker._showDatepicker(this):c=!1;c&&(a.preventDefault(),a.stopPropagation())},_doKeyPress:function(a){var b=$.datepicker._getInst(a.target);if($.datepicker._get(b,"constrainInput")){var c=$.datepicker._possibleChars($.datepicker._get(b,"dateFormat")),d=String.fromCharCode(a.charCode==undefined?a.keyCode:a.charCode);return a.ctrlKey||a.metaKey||d<" "||!c||c.indexOf(d)>-1}},_doKeyUp:function(a){var b=$.datepicker._getInst(a.target);if(b.input.val()!=b.lastVal)try{var c=$.datepicker.parseDate($.datepicker._get(b,"dateFormat"),b.input?b.input.val():null,$.datepicker._getFormatConfig(b));c&&($.datepicker._setDateFromField(b),$.datepicker._updateAlternate(b),$.datepicker._updateDatepicker(b))}catch(a){$.datepicker.log(a)}return!0},_showDatepicker:function(a){a=a.target||a,a.nodeName.toLowerCase()!="input"&&(a=$("input",a.parentNode)[0]);if(!$.datepicker._isDisabledDatepicker(a)&&$.datepicker._lastInput!=a){var b=$.datepicker._getInst(a);$.datepicker._curInst&&$.datepicker._curInst!=b&&($.datepicker._curInst.dpDiv.stop(!0,!0),b&&$.datepicker._datepickerShowing&&$.datepicker._hideDatepicker($.datepicker._curInst.input[0]));var c=$.datepicker._get(b,"beforeShow"),d=c?c.apply(a,[a,b]):{};if(d===!1)return;extendRemove(b.settings,d),b.lastVal=null,$.datepicker._lastInput=a,$.datepicker._setDateFromField(b),$.datepicker._inDialog&&(a.value=""),$.datepicker._pos||($.datepicker._pos=$.datepicker._findPos(a),$.datepicker._pos[1]+=a.offsetHeight);var e=!1;$(a).parents().each(function(){e|=$(this).css("position")=="fixed";return!e}),e&&$.browser.opera&&($.datepicker._pos[0]-=document.documentElement.scrollLeft,$.datepicker._pos[1]-=document.documentElement.scrollTop);var f={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null,b.dpDiv.empty(),b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),$.datepicker._updateDatepicker(b),f=$.datepicker._checkOffset(b,f,e),b.dpDiv.css({position:$.datepicker._inDialog&&$.blockUI?"static":e?"fixed":"absolute",display:"none",left:f.left+"px",top:f.top+"px"});if(!b.inline){var g=$.datepicker._get(b,"showAnim"),h=$.datepicker._get(b,"duration"),i=function(){var a=b.dpDiv.find("iframe.ui-datepicker-cover");if(!!a.length){var c=$.datepicker._getBorders(b.dpDiv);a.css({left:-c[0],top:-c[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex($(a).zIndex()+1),$.datepicker._datepickerShowing=!0,$.effects&&$.effects[g]?b.dpDiv.show(g,$.datepicker._get(b,"showOptions"),h,i):b.dpDiv[g||"show"](g?h:null,i),(!g||!h)&&i(),b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus(),$.datepicker._curInst=b}}},_updateDatepicker:function(a){var b=this;b.maxRows=4;var c=$.datepicker._getBorders(a.dpDiv);instActive=a,a.dpDiv.empty().append(this._generateHTML(a));var d=a.dpDiv.find("iframe.ui-datepicker-cover");!d.length||d.css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}),a.dpDiv.find("."+this._dayOverClass+" a").mouseover();var e=this._getNumberOfMonths(a),f=e[1],g=17;a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),f>1&&a.dpDiv.addClass("ui-datepicker-multi-"+f).css("width",g*f+"em"),a.dpDiv[(e[0]!=1||e[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi"),a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),a==$.datepicker._curInst&&$.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var h=a.yearshtml;setTimeout(function(){h===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml),h=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(a){return{thin:1,medium:2,thick:3}[a]||a};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var d=a.dpDiv.outerWidth(),e=a.dpDiv.outerHeight(),f=a.input?a.input.outerWidth():0,g=a.input?a.input.outerHeight():0,h=document.documentElement.clientWidth+$(document).scrollLeft(),i=document.documentElement.clientHeight+$(document).scrollTop();b.left-=this._get(a,"isRTL")?d-f:0,b.left-=c&&b.left==a.input.offset().left?$(document).scrollLeft():0,b.top-=c&&b.top==a.input.offset().top+g?$(document).scrollTop():0,b.left-=Math.min(b.left,b.left+d>h&&h>d?Math.abs(b.left+d-h):0),b.top-=Math.min(b.top,b.top+e>i&&i>e?Math.abs(e+g):0);return b},_findPos:function(a){var b=this._getInst(a),c=this._get(b,"isRTL");while(a&&(a.type=="hidden"||a.nodeType!=1||$.expr.filters.hidden(a)))a=a[c?"previousSibling":"nextSibling"];var d=$(a).offset();return[d.left,d.top]},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=$.data(a,PROP_NAME))&&this._datepickerShowing){var c=this._get(b,"showAnim"),d=this._get(b,"duration"),e=this,f=function(){$.datepicker._tidyDialog(b),e._curInst=null};$.effects&&$.effects[c]?b.dpDiv.hide(c,$.datepicker._get(b,"showOptions"),d,f):b.dpDiv[c=="slideDown"?"slideUp":c=="fadeIn"?"fadeOut":"hide"](c?d:null,f),c||f(),this._datepickerShowing=!1;var g=this._get(b,"onClose");g&&g.apply(b.input?b.input[0]:null,[b.input?b.input.val():"",b]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),$.blockUI&&($.unblockUI(),$("body").append(this.dpDiv))),this._inDialog=!1}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(!!$.datepicker._curInst){var b=$(a.target),c=$.datepicker._getInst(b[0]);(b[0].id!=$.datepicker._mainDivId&&b.parents("#"+$.datepicker._mainDivId).length==0&&!b.hasClass($.datepicker.markerClassName)&&!b.closest("."+$.datepicker._triggerClass).length&&$.datepicker._datepickerShowing&&(!$.datepicker._inDialog||!$.blockUI)||b.hasClass($.datepicker.markerClassName)&&$.datepicker._curInst!=c)&&$.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){var d=$(a),e=this._getInst(d[0]);this._isDisabledDatepicker(d[0])||(this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c),this._updateDatepicker(e))},_gotoToday:function(a){var b=$(a),c=this._getInst(b[0]);if(this._get(c,"gotoCurrent")&&c.currentDay)c.selectedDay=c.currentDay,c.drawMonth=c.selectedMonth=c.currentMonth,c.drawYear=c.selectedYear=c.currentYear;else{var d=new Date;c.selectedDay=d.getDate(),c.drawMonth=c.selectedMonth=d.getMonth(),c.drawYear=c.selectedYear=d.getFullYear()}this._notifyChange(c),this._adjustDate(b)},_selectMonthYear:function(a,b,c){var d=$(a),e=this._getInst(d[0]);e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10),this._notifyChange(e),this._adjustDate(d)},_selectDay:function(a,b,c,d){var e=$(a);if(!$(d).hasClass(this._unselectableClass)&&!this._isDisabledDatepicker(e[0])){var f=this._getInst(e[0]);f.selectedDay=f.currentDay=$("a",d).html(),f.selectedMonth=f.currentMonth=b,f.selectedYear=f.currentYear=c,this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){var b=$(a),c=this._getInst(b[0]);this._selectDate(b,"")},_selectDate:function(a,b){var c=$(a),d=this._getInst(c[0]);b=b!=null?b:this._formatDate(d),d.input&&d.input.val(b),this._updateAlternate(d);var e=this._get(d,"onSelect");e?e.apply(d.input?d.input[0]:null,[b,d]):d.input&&d.input.trigger("change"),d.inline?this._updateDatepicker(d):(this._hideDatepicker(),this._lastInput=d.input[0],typeof d.input[0]!="object"&&d.input.focus(),this._lastInput=null)},_updateAlternate:function(a){var b=this._get(a,"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),d=this._getDate(a),e=this.formatDate(c,d,this._getFormatConfig(a));$(b).each(function(){$(this).val(e)})}},noWeekends:function(a){var b=a.getDay();return[b>0&&b<6,""]},iso8601Week:function(a){var b=new Date(a.getTime());b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();b.setMonth(0),b.setDate(1);return Math.floor(Math.round((c-b)/864e5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"?b.toString():b+"";if(b=="")return null;var d=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;d=typeof d!="string"?d:(new Date).getFullYear()%100+parseInt(d,10);var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,g=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,h=(c?c.monthNames:null)||this._defaults.monthNames,i=-1,j=-1,k=-1,l=-1,m=!1,n=function(b){var c=s+1<a.length&&a.charAt(s+1)==b;c&&s++;return c},o=function(a){var c=n(a),d=a=="@"?14:a=="!"?20:a=="y"&&c?4:a=="o"?3:2,e=new RegExp("^\\d{1,"+d+"}"),f=b.substring(r).match(e);if(!f)throw"Missing number at position "+r;r+=f[0].length;return parseInt(f[0],10)},p=function(a,c,d){var e=$.map(n(a)?d:c,function(a,b){return[[b,a]]}).sort(function(a,b){return-(a[1].length-b[1].length)}),f=-1;$.each(e,function(a,c){var d=c[1];if(b.substr(r,d.length).toLowerCase()==d.toLowerCase()){f=c[0],r+=d.length;return!1}});if(f!=-1)return f+1;throw"Unknown name at position "+r},q=function(){if(b.charAt(r)!=a.charAt(s))throw"Unexpected literal at position "+r;r++},r=0;for(var s=0;s<a.length;s++)if(m)a.charAt(s)=="'"&&!n("'")?m=!1:q();else switch(a.charAt(s)){case"d":k=o("d");break;case"D":p("D",e,f);break;case"o":l=o("o");break;case"m":j=o("m");break;case"M":j=p("M",g,h);break;case"y":i=o("y");break;case"@":var t=new Date(o("@"));i=t.getFullYear(),j=t.getMonth()+1,k=t.getDate();break;case"!":var t=new Date((o("!")-this._ticksTo1970)/1e4);i=t.getFullYear(),j=t.getMonth()+1,k=t.getDate();break;case"'":n("'")?q():m=!0;break;default:q()}if(r<b.length)throw"Extra/unparsed characters found in date: "+b.substring(r);i==-1?i=(new Date).getFullYear():i<100&&(i+=(new Date).getFullYear()-(new Date).getFullYear()%100+(i<=d?0:-100));if(l>-1){j=1,k=l;for(;;){var u=this._getDaysInMonth(i,j-1);if(k<=u)break;j++,k-=u}}var t=this._daylightSavingAdjust(new Date(i,j-1,k));if(t.getFullYear()!=i||t.getMonth()+1!=j||t.getDate()!=k)throw"Invalid date";return t},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1e7,formatDate:function(a,b,c){if(!b)return"";var d=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,e=(c?c.dayNames:null)||this._defaults.dayNames,f=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,h=function(b){var c=m+1<a.length&&a.charAt(m+1)==b;c&&m++;return c},i=function(a,b,c){var d=""+b;if(h(a))while(d.length<c)d="0"+d;return d},j=function(a,b,c,d){return h(a)?d[b]:c[b]},k="",l=!1;if(b)for(var m=0;m<a.length;m++)if(l)a.charAt(m)=="'"&&!h("'")?l=!1:k+=a.charAt(m);else switch(a.charAt(m)){case"d":k+=i("d",b.getDate(),2);break;case"D":k+=j("D",b.getDay(),d,e);break;case"o":k+=i("o",Math.round(((new Date(b.getFullYear(),b.getMonth(),b.getDate())).getTime()-(new Date(b.getFullYear(),0,0)).getTime())/864e5),3);break;case"m":k+=i("m",b.getMonth()+1,2);break;case"M":k+=j("M",b.getMonth(),f,g);break;case"y":k+=h("y")?b.getFullYear():(b.getYear()%100<10?"0":"")+b.getYear()%100;break;case"@":k+=b.getTime();break;case"!":k+=b.getTime()*1e4+this._ticksTo1970;break;case"'":h("'")?k+="'":l=!0;break;default:k+=a.charAt(m)}return k},_possibleChars:function(a){var b="",c=!1,d=function(b){var c=e+1<a.length&&a.charAt(e+1)==b;c&&e++;return c};for(var e=0;e<a.length;e++)if(c)a.charAt(e)=="'"&&!d("'")?c=!1:b+=a.charAt(e);else switch(a.charAt(e)){case"d":case"m":case"y":case"@":b+="0123456789";break;case"D":case"M":return null;case"'":d("'")?b+="'":c=!0;break;default:b+=a.charAt(e)}return b},_get:function(a,b){return a.settings[b]!==undefined?a.settings[b]:this._defaults[b]},_setDateFromField:function(a,b){if(a.input.val()!=a.lastVal){var c=this._get(a,"dateFormat"),d=a.lastVal=a.input?a.input.val():null,e,f;e=f=this._getDefaultDate(a);var g=this._getFormatConfig(a);try{e=this.parseDate(c,d,g)||f}catch(h){this.log(h),d=b?"":d}a.selectedDay=e.getDate(),a.drawMonth=a.selectedMonth=e.getMonth(),a.drawYear=a.selectedYear=e.getFullYear(),a.currentDay=d?e.getDate():0,a.currentMonth=d?e.getMonth():0,a.currentYear=d?e.getFullYear():0,this._adjustInstDate(a)}},_getDefaultDate:function(a){return this._restrictMinMax(a,this._determineDate(a,this._get(a,"defaultDate"),new Date))},_determineDate:function(a,b,c){var d=function(a){var b=new Date;b.setDate(b.getDate()+a);return b},e=function(b){try{return $.datepicker.parseDate($.datepicker._get(a,"dateFormat"),b,$.datepicker._getFormatConfig(a))}catch(c){}var d=(b.toLowerCase().match(/^c/)?$.datepicker._getDate(a):null)||new Date,e=d.getFullYear(),f=d.getMonth(),g=d.getDate(),h=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,i=h.exec(b);while(i){switch(i[2]||"d"){case"d":case"D":g+=parseInt(i[1],10);break;case"w":case"W":g+=parseInt(i[1],10)*7;break;case"m":case"M":f+=parseInt(i[1],10),g=Math.min(g,$.datepicker._getDaysInMonth(e,f));break;case"y":case"Y":e+=parseInt(i[1],10),g=Math.min(g,$.datepicker._getDaysInMonth(e,f))}i=h.exec(b)}return new Date(e,f,g)},f=b==null||b===""?c:typeof b=="string"?e(b):typeof b=="number"?isNaN(b)?c:d(b):new Date(b.getTime());f=f&&f.toString()=="Invalid Date"?c:f,f&&(f.setHours(0),f.setMinutes(0),f.setSeconds(0),f.setMilliseconds(0));return this._daylightSavingAdjust(f)},_daylightSavingAdjust:function(a){if(!a)return null;a.setHours(a.getHours()>12?a.getHours()+2:0);return a},_setDate:function(a,b,c){var d=!b,e=a.selectedMonth,f=a.selectedYear,g=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=g.getDate(),a.drawMonth=a.selectedMonth=a.currentMonth=g.getMonth(),a.drawYear=a.selectedYear=a.currentYear=g.getFullYear(),(e!=a.selectedMonth||f!=a.selectedYear)&&!c&&this._notifyChange(a),this._adjustInstDate(a),a.input&&a.input.val(d?"":this._formatDate(a))},_getDate:function(a){var b=!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return b},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),d=this._get(a,"showButtonPanel"),e=this._get(a,"hideIfNoPrevNext"),f=this._get(a,"navigationAsDateFormat"),g=this._getNumberOfMonths(a),h=this._get(a,"showCurrentAtPos"),i=this._get(a,"stepMonths"),j=g[0]!=1||g[1]!=1,k=this._daylightSavingAdjust(a.currentDay?new Date(a.currentYear,a.currentMonth,a.currentDay):new Date(9999,9,9)),l=this._getMinMaxDate(a,"min"),m=this._getMinMaxDate(a,"max"),n=a.drawMonth-h,o=a.drawYear;n<0&&(n+=12,o--);if(m){var p=this._daylightSavingAdjust(new Date(m.getFullYear(),m.getMonth()-g[0]*g[1]+1,m.getDate()));p=l&&p<l?l:p;while(this._daylightSavingAdjust(new Date(o,n,1))>p)n--,n<0&&(n=11,o--)}a.drawMonth=n,a.drawYear=o;var q=this._get(a,"prevText");q=f?this.formatDate(q,this._daylightSavingAdjust(new Date(o,n-i,1)),this._getFormatConfig(a)):q;var r=this._canAdjustMonth(a,-1,o,n)?'<a class="ui-datepicker-prev ui-corner-all" onclick="DP_jQuery_'+dpuuid+".datepicker._adjustDate('#"+a.id+"', -"+i+", 'M');\""+' title="'+q+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+q+"</span></a>":e?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+q+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+q+"</span></a>",s=this._get(a,"nextText");s=f?this.formatDate(s,this._daylightSavingAdjust(new Date(o,n+i,1)),this._getFormatConfig(a)):s;var t=this._canAdjustMonth(a,1,o,n)?'<a class="ui-datepicker-next ui-corner-all" onclick="DP_jQuery_'+dpuuid+".datepicker._adjustDate('#"+a.id+"', +"+i+", 'M');\""+' title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>":e?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>",u=this._get(a,"currentText"),v=this._get(a,"gotoCurrent")&&a.currentDay?k:b;u=f?this.formatDate(u,v,this._getFormatConfig(a)):u;var w=a.inline?"":'<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" onclick="DP_jQuery_'+dpuuid+'.datepicker._hideDatepicker();">'+this._get(a,"closeText")+"</button>",x=d?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(c?w:"")+(this._isInRange(a,v)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" onclick="DP_jQuery_'+dpuuid+".datepicker._gotoToday('#"+a.id+"');\""+">"+u+"</button>":"")+(c?"":w)+"</div>":"",y=parseInt(this._get(a,"firstDay"),10);y=isNaN(y)?0:y;var z=this._get(a,"showWeek"),A=this._get(a,"dayNames"),B=this._get(a,"dayNamesShort"),C=this._get(a,"dayNamesMin"),D=this._get(a,"monthNames"),E=this._get(a,"monthNamesShort"),F=this._get(a,"beforeShowDay"),G=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths"),I=this._get(a,"calculateWeek")||this.iso8601Week,J=this._getDefaultDate(a),K="";for(var L=0;L<g[0];L++){var M="";this.maxRows=4;for(var N=0;N<g[1];N++){var O=this._daylightSavingAdjust(new Date(o,n,a.selectedDay)),P=" ui-corner-all",Q="";if(j){Q+='<div class="ui-datepicker-group';if(g[1]>1)switch(N){case 0:Q+=" ui-datepicker-group-first",P=" ui-corner-"+(c?"right":"left");break;case g[1]-1:Q+=" ui-datepicker-group-last",P=" ui-corner-"+(c?"left":"right");break;default:Q+=" ui-datepicker-group-middle",P=""}Q+='">'}Q+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+P+'">'+(/all|left/.test(P)&&L==0?c?t:r:"")+(/all|right/.test(P)&&L==0?c?r:t:"")+this._generateMonthYearHeader(a,n,o,l,m,L>0||N>0,D,E)+'</div><table class="ui-datepicker-calendar"><thead>'+"<tr>";var R=z?'<th class="ui-datepicker-week-col">'+this._get(a,"weekHeader")+"</th>":"";for(var S=0;S<7;S++){var T=(S+y)%7;R+="<th"+((S+y+6)%7>=5?' class="ui-datepicker-week-end"':"")+">"+'<span title="'+A[T]+'">'+C[T]+"</span></th>"}Q+=R+"</tr></thead><tbody>";var U=this._getDaysInMonth(o,n);o==a.selectedYear&&n==a.selectedMonth&&(a.selectedDay=Math.min(a.selectedDay,U));var V=(this._getFirstDayOfMonth(o,n)-y+7)%7,W=Math.ceil((V+U)/7),X=j?this.maxRows>W?this.maxRows:W:W;this.maxRows=X;var Y=this._daylightSavingAdjust(new Date(o,n,1-V));for(var Z=0;Z<X;Z++){Q+="<tr>";var _=z?'<td class="ui-datepicker-week-col">'+this._get(a,"calculateWeek")(Y)+"</td>":"";for(var S=0;S<7;S++){var ba=F?F.apply(a.input?a.input[0]:null,[Y]):[!0,""],bb=Y.getMonth()!=n,bc=bb&&!H||!ba[0]||l&&Y<l||m&&Y>m;_+='<td class="'+((S+y+6)%7>=5?" ui-datepicker-week-end":"")+(bb?" ui-datepicker-other-month":"")+(Y.getTime()==O.getTime()&&n==a.selectedMonth&&a._keyEvent||J.getTime()==Y.getTime()&&J.getTime()==O.getTime()?" "+this._dayOverClass:"")+(bc?" "+this._unselectableClass+" ui-state-disabled":"")+(bb&&!G?"":" "+ba[1]+(Y.getTime()==k.getTime()?" "+this._currentClass:"")+(Y.getTime()==b.getTime()?" ui-datepicker-today":""))+'"'+((!bb||G)&&ba[2]?' title="'+ba[2]+'"':"")+(bc?"":' onclick="DP_jQuery_'+dpuuid+".datepicker._selectDay('#"+a.id+"',"+Y.getMonth()+","+Y.getFullYear()+', this);return false;"')+">"+(bb&&!G?"&#xa0;":bc?'<span class="ui-state-default">'+Y.getDate()+"</span>":'<a class="ui-state-default'+(Y.getTime()==b.getTime()?" ui-state-highlight":"")+(Y.getTime()==k.getTime()?" ui-state-active":"")+(bb?" ui-priority-secondary":"")+'" href="#">'+Y.getDate()+"</a>")+"</td>",Y.setDate(Y.getDate()+1),Y=this._daylightSavingAdjust(Y)}Q+=_+"</tr>"}n++,n>11&&(n=0,o++),Q+="</tbody></table>"+(j?"</div>"+(g[0]>0&&N==g[1]-1?'<div class="ui-datepicker-row-break"></div>':""):""),M+=Q}K+=M}K+=x+($.browser.msie&&parseInt($.browser.version,10)<7&&!a.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':""),
+a._keyEvent=!1;return K},_generateMonthYearHeader:function(a,b,c,d,e,f,g,h){var i=this._get(a,"changeMonth"),j=this._get(a,"changeYear"),k=this._get(a,"showMonthAfterYear"),l='<div class="ui-datepicker-title">',m="";if(f||!i)m+='<span class="ui-datepicker-month">'+g[b]+"</span>";else{var n=d&&d.getFullYear()==c,o=e&&e.getFullYear()==c;m+='<select class="ui-datepicker-month" onchange="DP_jQuery_'+dpuuid+".datepicker._selectMonthYear('#"+a.id+"', this, 'M');\" "+">";for(var p=0;p<12;p++)(!n||p>=d.getMonth())&&(!o||p<=e.getMonth())&&(m+='<option value="'+p+'"'+(p==b?' selected="selected"':"")+">"+h[p]+"</option>");m+="</select>"}k||(l+=m+(f||!i||!j?"&#xa0;":""));if(!a.yearshtml){a.yearshtml="";if(f||!j)l+='<span class="ui-datepicker-year">'+c+"</span>";else{var q=this._get(a,"yearRange").split(":"),r=(new Date).getFullYear(),s=function(a){var b=a.match(/c[+-].*/)?c+parseInt(a.substring(1),10):a.match(/[+-].*/)?r+parseInt(a,10):parseInt(a,10);return isNaN(b)?r:b},t=s(q[0]),u=Math.max(t,s(q[1]||""));t=d?Math.max(t,d.getFullYear()):t,u=e?Math.min(u,e.getFullYear()):u,a.yearshtml+='<select class="ui-datepicker-year" onchange="DP_jQuery_'+dpuuid+".datepicker._selectMonthYear('#"+a.id+"', this, 'Y');\" "+">";for(;t<=u;t++)a.yearshtml+='<option value="'+t+'"'+(t==c?' selected="selected"':"")+">"+t+"</option>";a.yearshtml+="</select>",l+=a.yearshtml,a.yearshtml=null}}l+=this._get(a,"yearSuffix"),k&&(l+=(f||!i||!j?"&#xa0;":"")+m),l+="</div>";return l},_adjustInstDate:function(a,b,c){var d=a.drawYear+(c=="Y"?b:0),e=a.drawMonth+(c=="M"?b:0),f=Math.min(a.selectedDay,this._getDaysInMonth(d,e))+(c=="D"?b:0),g=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(d,e,f)));a.selectedDay=g.getDate(),a.drawMonth=a.selectedMonth=g.getMonth(),a.drawYear=a.selectedYear=g.getFullYear(),(c=="M"||c=="Y")&&this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max"),e=c&&b<c?c:b;e=d&&e>d?d:e;return e},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");b&&b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){var b=this._get(a,"numberOfMonths");return b==null?[1,1]:typeof b=="number"?[1,b]:b},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,d){var e=this._getNumberOfMonths(a),f=this._daylightSavingAdjust(new Date(c,d+(b<0?b:e[0]*e[1]),1));b<0&&f.setDate(this._getDaysInMonth(f.getFullYear(),f.getMonth()));return this._isInRange(a,f)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!d||b.getTime()<=d.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,d){b||(a.currentDay=a.selectedDay,a.currentMonth=a.selectedMonth,a.currentYear=a.selectedYear);var e=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(d,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),e,this._getFormatConfig(a))}}),$.fn.datepicker=function(a){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv),$.datepicker.initialized=!0);var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return $.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return $.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b));return this.each(function(){typeof a=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this].concat(b)):$.datepicker._attachDatepicker(this,a)})},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.8.18",window["DP_jQuery_"+dpuuid]=$})(jQuery);/*
+ * jQuery UI Progressbar 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */(function(a,b){a.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()}),this.valueDiv=a("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove(),a.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===b)return this._value();this._setOption("value",a);return this},_setOption:function(b,c){b==="value"&&(this.options.value=c,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),a.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;typeof a!="number"&&(a=0);return Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var a=this.value(),b=this._percentage();this.oldValue!==a&&(this.oldValue=a,this._trigger("change")),this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(b.toFixed(0)+"%"),this.element.attr("aria-valuenow",a)}}),a.extend(a.ui.progressbar,{version:"1.8.18"})})(jQuery);/*
+ * jQuery UI Effects 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/
+ */jQuery.effects||function(a,b){function l(b){if(!b||typeof b=="number"||a.fx.speeds[b])return!0;if(typeof b=="string"&&!a.effects[b])return!0;return!1}function k(b,c,d,e){typeof b=="object"&&(e=c,d=null,c=b,b=c.effect),a.isFunction(c)&&(e=c,d=null,c={});if(typeof c=="number"||a.fx.speeds[c])e=d,d=c,c={};a.isFunction(d)&&(e=d,d=null),c=c||{},d=d||c.duration,d=a.fx.off?0:typeof d=="number"?d:d in a.fx.speeds?a.fx.speeds[d]:a.fx.speeds._default,e=e||c.complete;return[b,c,d,e]}function j(a,b){var c={_:0},d;for(d in b)a[d]!=b[d]&&(c[d]=b[d]);return c}function i(b){var c,d;for(c in b)d=b[c],(d==null||a.isFunction(d)||c in g||/scrollbar/.test(c)||!/color/i.test(c)&&isNaN(parseFloat(d)))&&delete b[c];return b}function h(){var a=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,b={},c,d;if(a&&a.length&&a[0]&&a[a[0]]){var e=a.length;while(e--)c=a[e],typeof a[c]=="string"&&(d=c.replace(/\-(\w)/g,function(a,b){return b.toUpperCase()}),b[d]=a[c])}else for(c in a)typeof a[c]=="string"&&(b[c]=a[c]);return b}function d(b,d){var e;do{e=a.curCSS(b,d);if(e!=""&&e!="transparent"||a.nodeName(b,"body"))break;d="backgroundColor"}while(b=b.parentNode);return c(e)}function c(b){var c;if(b&&b.constructor==Array&&b.length==3)return b;if(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))return[parseInt(c[1],10),parseInt(c[2],10),parseInt(c[3],10)];if(c=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))return[parseFloat(c[1])*2.55,parseFloat(c[2])*2.55,parseFloat(c[3])*2.55];if(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))return[parseInt(c[1],16),parseInt(c[2],16),parseInt(c[3],16)];if(c=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))return[parseInt(c[1]+c[1],16),parseInt(c[2]+c[2],16),parseInt(c[3]+c[3],16)];if(c=/rgba\(0, 0, 0, 0\)/.exec(b))return e.transparent;return e[a.trim(b).toLowerCase()]}a.effects={},a.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(b,e){a.fx.step[e]=function(a){a.colorInit||(a.start=d(a.elem,e),a.end=c(a.end),a.colorInit=!0),a.elem.style[e]="rgb("+Math.max(Math.min(parseInt(a.pos*(a.end[0]-a.start[0])+a.start[0],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[1]-a.start[1])+a.start[1],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[2]-a.start[2])+a.start[2],10),255),0)+")"}});var e={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},f=["add","remove","toggle"],g={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};a.effects.animateClass=function(b,c,d,e){a.isFunction(d)&&(e=d,d=null);return this.queue(function(){var g=a(this),k=g.attr("style")||" ",l=i(h.call(this)),m,n=g.attr("class");a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),m=i(h.call(this)),g.attr("class",n),g.animate(j(l,m),{queue:!1,duration:c,easing:d,complete:function(){a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),typeof g.attr("style")=="object"?(g.attr("style").cssText="",g.attr("style").cssText=k):g.attr("style",k),e&&e.apply(this,arguments),a.dequeue(this)}})})},a.fn.extend({_addClass:a.fn.addClass,addClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{add:b},c,d,e]):this._addClass(b)},_removeClass:a.fn.removeClass,removeClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{remove:b},c,d,e]):this._removeClass(b)},_toggleClass:a.fn.toggleClass,toggleClass:function(c,d,e,f,g){return typeof d=="boolean"||d===b?e?a.effects.animateClass.apply(this,[d?{add:c}:{remove:c},e,f,g]):this._toggleClass(c,d):a.effects.animateClass.apply(this,[{toggle:c},d,e,f])},switchClass:function(b,c,d,e,f){return a.effects.animateClass.apply(this,[{add:c,remove:b},d,e,f])}}),a.extend(a.effects,{version:"1.8.18",save:function(a,b){for(var c=0;c<b.length;c++)b[c]!==null&&a.data("ec.storage."+b[c],a[0].style[b[c]])},restore:function(a,b){for(var c=0;c<b.length;c++)b[c]!==null&&a.css(b[c],a.data("ec.storage."+b[c]))},setMode:function(a,b){b=="toggle"&&(b=a.is(":hidden")?"show":"hide");return b},getBaseline:function(a,b){var c,d;switch(a[0]){case"top":c=0;break;case"middle":c=.5;break;case"bottom":c=1;break;default:c=a[0]/b.height}switch(a[1]){case"left":d=0;break;case"center":d=.5;break;case"right":d=1;break;default:d=a[1]/b.width}return{x:d,y:c}},createWrapper:function(b){if(b.parent().is(".ui-effects-wrapper"))return b.parent();var c={width:b.outerWidth(!0),height:b.outerHeight(!0),"float":b.css("float")},d=a("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"}));return d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;if(b.parent().is(".ui-effects-wrapper")){c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus();return c}return b},setTransition:function(b,c,d,e){e=e||{},a.each(c,function(a,c){unit=b.cssUnit(c),unit[0]>0&&(e[c]=unit[0]*d+unit[1])});return e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];if(a.fx.off||!i)return h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)});return i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);b[1].mode="show";return this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);b[1].mode="hide";return this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);c[1].mode="toggle";return this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])});return d}}),a.easing.jswing=a.easing.swing,a.extend(a.easing,{def:"easeOutQuad",swing:function(b,c,d,e,f){return a.easing[a.easing.def](b,c,d,e,f)},easeInQuad:function(a,b,c,d,e){return d*(b/=e)*b+c},easeOutQuad:function(a,b,c,d,e){return-d*(b/=e)*(b-2)+c},easeInOutQuad:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b+c;return-d/2*(--b*(b-2)-1)+c},easeInCubic:function(a,b,c,d,e){return d*(b/=e)*b*b+c},easeOutCubic:function(a,b,c,d,e){return d*((b=b/e-1)*b*b+1)+c},easeInOutCubic:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b*b+c;return d/2*((b-=2)*b*b+2)+c},easeInQuart:function(a,b,c,d,e){return d*(b/=e)*b*b*b+c},easeOutQuart:function(a,b,c,d,e){return-d*((b=b/e-1)*b*b*b-1)+c},easeInOutQuart:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b*b*b+c;return-d/2*((b-=2)*b*b*b-2)+c},easeInQuint:function(a,b,c,d,e){return d*(b/=e)*b*b*b*b+c},easeOutQuint:function(a,b,c,d,e){return d*((b=b/e-1)*b*b*b*b+1)+c},easeInOutQuint:function(a,b,c,d,e){if((b/=e/2)<1)return d/2*b*b*b*b*b+c;return d/2*((b-=2)*b*b*b*b+2)+c},easeInSine:function(a,b,c,d,e){return-d*Math.cos(b/e*(Math.PI/2))+d+c},easeOutSine:function(a,b,c,d,e){return d*Math.sin(b/e*(Math.PI/2))+c},easeInOutSine:function(a,b,c,d,e){return-d/2*(Math.cos(Math.PI*b/e)-1)+c},easeInExpo:function(a,b,c,d,e){return b==0?c:d*Math.pow(2,10*(b/e-1))+c},easeOutExpo:function(a,b,c,d,e){return b==e?c+d:d*(-Math.pow(2,-10*b/e)+1)+c},easeInOutExpo:function(a,b,c,d,e){if(b==0)return c;if(b==e)return c+d;if((b/=e/2)<1)return d/2*Math.pow(2,10*(b-1))+c;return d/2*(-Math.pow(2,-10*--b)+2)+c},easeInCirc:function(a,b,c,d,e){return-d*(Math.sqrt(1-(b/=e)*b)-1)+c},easeOutCirc:function(a,b,c,d,e){return d*Math.sqrt(1-(b=b/e-1)*b)+c},easeInOutCirc:function(a,b,c,d,e){if((b/=e/2)<1)return-d/2*(Math.sqrt(1-b*b)-1)+c;return d/2*(Math.sqrt(1-(b-=2)*b)+1)+c},easeInElastic:function(a,b,c,d,e){var f=1.70158,g=0,h=d;if(b==0)return c;if((b/=e)==1)return c+d;g||(g=e*.3);if(h<Math.abs(d)){h=d;var f=g/4}else var f=g/(2*Math.PI)*Math.asin(d/h);return-(h*Math.pow(2,10*(b-=1))*Math.sin((b*e-f)*2*Math.PI/g))+c},easeOutElastic:function(a,b,c,d,e){var f=1.70158,g=0,h=d;if(b==0)return c;if((b/=e)==1)return c+d;g||(g=e*.3);if(h<Math.abs(d)){h=d;var f=g/4}else var f=g/(2*Math.PI)*Math.asin(d/h);return h*Math.pow(2,-10*b)*Math.sin((b*e-f)*2*Math.PI/g)+d+c},easeInOutElastic:function(a,b,c,d,e){var f=1.70158,g=0,h=d;if(b==0)return c;if((b/=e/2)==2)return c+d;g||(g=e*.3*1.5);if(h<Math.abs(d)){h=d;var f=g/4}else var f=g/(2*Math.PI)*Math.asin(d/h);if(b<1)return-0.5*h*Math.pow(2,10*(b-=1))*Math.sin((b*e-f)*2*Math.PI/g)+c;return h*Math.pow(2,-10*(b-=1))*Math.sin((b*e-f)*2*Math.PI/g)*.5+d+c},easeInBack:function(a,c,d,e,f,g){g==b&&(g=1.70158);return e*(c/=f)*c*((g+1)*c-g)+d},easeOutBack:function(a,c,d,e,f,g){g==b&&(g=1.70158);return e*((c=c/f-1)*c*((g+1)*c+g)+1)+d},easeInOutBack:function(a,c,d,e,f,g){g==b&&(g=1.70158);if((c/=f/2)<1)return e/2*c*c*(((g*=1.525)+1)*c-g)+d;return e/2*((c-=2)*c*(((g*=1.525)+1)*c+g)+2)+d},easeInBounce:function(b,c,d,e,f){return e-a.easing.easeOutBounce(b,f-c,0,e,f)+d},easeOutBounce:function(a,b,c,d,e){return(b/=e)<1/2.75?d*7.5625*b*b+c:b<2/2.75?d*(7.5625*(b-=1.5/2.75)*b+.75)+c:b<2.5/2.75?d*(7.5625*(b-=2.25/2.75)*b+.9375)+c:d*(7.5625*(b-=2.625/2.75)*b+.984375)+c},easeInOutBounce:function(b,c,d,e,f){if(c<f/2)return a.easing.easeInBounce(b,c*2,0,e,f)*.5+d;return a.easing.easeOutBounce(b,c*2-f,0,e,f)*.5+e*.5+d}})}(jQuery);/*
+ * jQuery UI Effects Blind 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Blind
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.blind=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"vertical";a.effects.save(c,d),c.show();var g=a.effects.createWrapper(c).css({overflow:"hidden"}),h=f=="vertical"?"height":"width",i=f=="vertical"?g.height():g.width();e=="show"&&g.css(h,0);var j={};j[h]=e=="show"?i:0,g.animate(j,b.duration,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);/*
+ * jQuery UI Effects Bounce 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Bounce
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.bounce=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"effect"),f=b.options.direction||"up",g=b.options.distance||20,h=b.options.times||5,i=b.duration||250;/show|hide/.test(e)&&d.push("opacity"),a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var j=f=="up"||f=="down"?"top":"left",k=f=="up"||f=="left"?"pos":"neg",g=b.options.distance||(j=="top"?c.outerHeight({margin:!0})/3:c.outerWidth({margin:!0})/3);e=="show"&&c.css("opacity",0).css(j,k=="pos"?-g:g),e=="hide"&&(g=g/(h*2)),e!="hide"&&h--;if(e=="show"){var l={opacity:1};l[j]=(k=="pos"?"+=":"-=")+g,c.animate(l,i/2,b.options.easing),g=g/2,h--}for(var m=0;m<h;m++){var n={},p={};n[j]=(k=="pos"?"-=":"+=")+g,p[j]=(k=="pos"?"+=":"-=")+g,c.animate(n,i/2,b.options.easing).animate(p,i/2,b.options.easing),g=e=="hide"?g*2:g/2}if(e=="hide"){var l={opacity:0};l[j]=(k=="pos"?"-=":"+=")+g,c.animate(l,i/2,b.options.easing,function(){c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)})}else{var n={},p={};n[j]=(k=="pos"?"-=":"+=")+g,p[j]=(k=="pos"?"+=":"-=")+g,c.animate(n,i/2,b.options.easing).animate(p,i/2,b.options.easing,function(){a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)})}c.queue("fx",function(){c.dequeue()}),c.dequeue()})}})(jQuery);/*
+ * jQuery UI Effects Clip 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Clip
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.clip=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","height","width"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"vertical";a.effects.save(c,d),c.show();var g=a.effects.createWrapper(c).css({overflow:"hidden"}),h=c[0].tagName=="IMG"?g:c,i={size:f=="vertical"?"height":"width",position:f=="vertical"?"top":"left"},j=f=="vertical"?h.height():h.width();e=="show"&&(h.css(i.size,0),h.css(i.position,j/2));var k={};k[i.size]=e=="show"?j:0,k[i.position]=e=="show"?0:j/2,h.animate(k,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()}})})}})(jQuery);/*
+ * jQuery UI Effects Drop 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Drop
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.drop=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","opacity"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"left";a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var g=f=="up"||f=="down"?"top":"left",h=f=="up"||f=="left"?"pos":"neg",i=b.options.distance||(g=="top"?c.outerHeight({margin:!0})/2:c.outerWidth({margin:!0})/2);e=="show"&&c.css("opacity",0).css(g,h=="pos"?-i:i);var j={opacity:e=="show"?1:0};j[g]=(e=="show"?h=="pos"?"+=":"-=":h=="pos"?"-=":"+=")+i,c.animate(j,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);/*
+ * jQuery UI Effects Explode 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Explode
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.explode=function(b){return this.queue(function(){var c=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3,d=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3;b.options.mode=b.options.mode=="toggle"?a(this).is(":visible")?"hide":"show":b.options.mode;var e=a(this).show().css("visibility","hidden"),f=e.offset();f.top-=parseInt(e.css("marginTop"),10)||0,f.left-=parseInt(e.css("marginLeft"),10)||0;var g=e.outerWidth(!0),h=e.outerHeight(!0);for(var i=0;i<c;i++)for(var j=0;j<d;j++)e.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-j*(g/d),top:-i*(h/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:g/d,height:h/c,left:f.left+j*(g/d)+(b.options.mode=="show"?(j-Math.floor(d/2))*(g/d):0),top:f.top+i*(h/c)+(b.options.mode=="show"?(i-Math.floor(c/2))*(h/c):0),opacity:b.options.mode=="show"?0:1}).animate({left:f.left+j*(g/d)+(b.options.mode=="show"?0:(j-Math.floor(d/2))*(g/d)),top:f.top+i*(h/c)+(b.options.mode=="show"?0:(i-Math.floor(c/2))*(h/c)),opacity:b.options.mode=="show"?1:0},b.duration||500);setTimeout(function(){b.options.mode=="show"?e.css({visibility:"visible"}):e.css({visibility:"visible"}).hide(),b.callback&&b.callback.apply(e[0]),e.dequeue(),a("div.ui-effects-explode").remove()},b.duration||500)})}})(jQuery);/*
+ * jQuery UI Effects Fade 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Fade
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.fade=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide");c.animate({opacity:d},{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);/*
+ * jQuery UI Effects Fold 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Fold
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.fold=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.size||15,g=!!b.options.horizFirst,h=b.duration?b.duration/2:a.fx.speeds._default/2;a.effects.save(c,d),c.show();var i=a.effects.createWrapper(c).css({overflow:"hidden"}),j=e=="show"!=g,k=j?["width","height"]:["height","width"],l=j?[i.width(),i.height()]:[i.height(),i.width()],m=/([0-9]+)%/.exec(f);m&&(f=parseInt(m[1],10)/100*l[e=="hide"?0:1]),e=="show"&&i.css(g?{height:0,width:f}:{height:f,width:0});var n={},p={};n[k[0]]=e=="show"?l[0]:f,p[k[1]]=e=="show"?l[1]:0,i.animate(n,h,b.options.easing).animate(p,h,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);/*
+ * jQuery UI Effects Highlight 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Highlight
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.highlight=function(b){return this.queue(function(){var c=a(this),d=["backgroundImage","backgroundColor","opacity"],e=a.effects.setMode(c,b.options.mode||"show"),f={backgroundColor:c.css("backgroundColor")};e=="hide"&&(f.opacity=0),a.effects.save(c,d),c.show().css({backgroundImage:"none",backgroundColor:b.options.color||"#ffff99"}).animate(f,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),e=="show"&&!a.support.opacity&&this.style.removeAttribute("filter"),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);/*
+ * jQuery UI Effects Pulsate 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Pulsate
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.pulsate=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"show");times=(b.options.times||5)*2-1,duration=b.duration?b.duration/2:a.fx.speeds._default/2,isVisible=c.is(":visible"),animateTo=0,isVisible||(c.css("opacity",0).show(),animateTo=1),(d=="hide"&&isVisible||d=="show"&&!isVisible)&&times--;for(var e=0;e<times;e++)c.animate({opacity:animateTo},duration,b.options.easing),animateTo=(animateTo+1)%2;c.animate({opacity:animateTo},duration,b.options.easing,function(){animateTo==0&&c.hide(),b.callback&&b.callback.apply(this,arguments)}),c.queue("fx",function(){c.dequeue()}).dequeue()})}})(jQuery);/*
+ * jQuery UI Effects Scale 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Scale
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.puff=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide"),e=parseInt(b.options.percent,10)||150,f=e/100,g={height:c.height(),width:c.width()};a.extend(b.options,{fade:!0,mode:d,percent:d=="hide"?e:100,from:d=="hide"?g:{height:g.height*f,width:g.width*f}}),c.effect("scale",b.options,b.duration,b.callback),c.dequeue()})},a.effects.scale=function(b){return this.queue(function(){var c=a(this),d=a.extend(!0,{},b.options),e=a.effects.setMode(c,b.options.mode||"effect"),f=parseInt(b.options.percent,10)||(parseInt(b.options.percent,10)==0?0:e=="hide"?0:100),g=b.options.direction||"both",h=b.options.origin;e!="effect"&&(d.origin=h||["middle","center"],d.restore=!0);var i={height:c.height(),width:c.width()};c.from=b.options.from||(e=="show"?{height:0,width:0}:i);var j={y:g!="horizontal"?f/100:1,x:g!="vertical"?f/100:1};c.to={height:i.height*j.y,width:i.width*j.x},b.options.fade&&(e=="show"&&(c.from.opacity=0,c.to.opacity=1),e=="hide"&&(c.from.opacity=1,c.to.opacity=0)),d.from=c.from,d.to=c.to,d.mode=e,c.effect("size",d,b.duration,b.callback),c.dequeue()})},a.effects.size=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","width","height","overflow","opacity"],e=["position","top","bottom","left","right","overflow","opacity"],f=["width","height","overflow"],g=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],i=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],j=a.effects.setMode(c,b.options.mode||"effect"),k=b.options.restore||!1,l=b.options.scale||"both",m=b.options.origin,n={height:c.height(),width:c.width()};c.from=b.options.from||n,c.to=b.options.to||n;if(m){var p=a.effects.getBaseline(m,n);c.from.top=(n.height-c.from.height)*p.y,c.from.left=(n.width-c.from.width)*p.x,c.to.top=(n.height-c.to.height)*p.y,c.to.left=(n.width-c.to.width)*p.x}var q={from:{y:c.from.height/n.height,x:c.from.width/n.width},to:{y:c.to.height/n.height,x:c.to.width/n.width}};if(l=="box"||l=="both")q.from.y!=q.to.y&&(d=d.concat(h),c.from=a.effects.setTransition(c,h,q.from.y,c.from),c.to=a.effects.setTransition(c,h,q.to.y,c.to)),q.from.x!=q.to.x&&(d=d.concat(i),c.from=a.effects.setTransition(c,i,q.from.x,c.from),c.to=a.effects.setTransition(c,i,q.to.x,c.to));(l=="content"||l=="both")&&q.from.y!=q.to.y&&(d=d.concat(g),c.from=a.effects.setTransition(c,g,q.from.y,c.from),c.to=a.effects.setTransition(c,g,q.to.y,c.to)),a.effects.save(c,k?d:e),c.show(),a.effects.createWrapper(c),c.css("overflow","hidden").css(c.from);if(l=="content"||l=="both")h=h.concat(["marginTop","marginBottom"]).concat(g),i=i.concat(["marginLeft","marginRight"]),f=d.concat(h).concat(i),c.find("*[width]").each(function(){child=a(this),k&&a.effects.save(child,f);var c={height:child.height(),width:child.width()};child.from={height:c.height*q.from.y,width:c.width*q.from.x},child.to={height:c.height*q.to.y,width:c.width*q.to.x},q.from.y!=q.to.y&&(child.from=a.effects.setTransition(child,h,q.from.y,child.from),child.to=a.effects.setTransition(child,h,q.to.y,child.to)),q.from.x!=q.to.x&&(child.from=a.effects.setTransition(child,i,q.from.x,child.from),child.to=a.effects.setTransition(child,i,q.to.x,child.to)),child.css(child.from),child.animate(child.to,b.duration,b.options.easing,function(){k&&a.effects.restore(child,f)})});c.animate(c.to,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){c.to.opacity===0&&c.css("opacity",c.from.opacity),j=="hide"&&c.hide(),a.effects.restore(c,k?d:e),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);/*
+ * jQuery UI Effects Shake 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Shake
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.shake=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"effect"),f=b.options.direction||"left",g=b.options.distance||20,h=b.options.times||3,i=b.duration||b.options.duration||140;a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var j=f=="up"||f=="down"?"top":"left",k=f=="up"||f=="left"?"pos":"neg",l={},m={},n={};l[j]=(k=="pos"?"-=":"+=")+g,m[j]=(k=="pos"?"+=":"-=")+g*2,n[j]=(k=="pos"?"-=":"+=")+g*2,c.animate(l,i,b.options.easing);for(var p=1;p<h;p++)c.animate(m,i,b.options.easing).animate(n,i,b.options.easing);c.animate(m,i,b.options.easing).animate(l,i/2,b.options.easing,function(){a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)}),c.queue("fx",function(){c.dequeue()}),c.dequeue()})}})(jQuery);/*
+ * jQuery UI Effects Slide 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Slide
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.slide=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"show"),f=b.options.direction||"left";a.effects.save(c,d),c.show(),a.effects.createWrapper(c).css({overflow:"hidden"});var g=f=="up"||f=="down"?"top":"left",h=f=="up"||f=="left"?"pos":"neg",i=b.options.distance||(g=="top"?c.outerHeight({margin:!0}):c.outerWidth({margin:!0}));e=="show"&&c.css(g,h=="pos"?isNaN(i)?"-"+i:-i:i);var j={};j[g]=(e=="show"?h=="pos"?"+=":"-=":h=="pos"?"-=":"+=")+i,c.animate(j,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);/*
+ * jQuery UI Effects Transfer 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Effects/Transfer
+ *
+ * Depends:
+ * jquery.effects.core.js
+ */(function(a,b){a.effects.transfer=function(b){return this.queue(function(){var c=a(this),d=a(b.options.to),e=d.offset(),f={top:e.top,left:e.left,height:d.innerHeight(),width:d.innerWidth()},g=c.offset(),h=a('<div class="ui-effects-transfer"></div>').appendTo(document.body).addClass(b.options.className).css({top:g.top,left:g.left,height:c.innerHeight(),width:c.innerWidth(),position:"absolute"}).animate(f,b.duration,b.options.easing,function(){h.remove(),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery); \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/pngfix.js b/subsonic-main/src/main/webapp/script/pngfix.js
new file mode 100644
index 00000000..87d4b1cd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/pngfix.js
@@ -0,0 +1,39 @@
+/*
+
+Correctly handle PNG transparency in Win IE 5.5 & 6.
+http://homepage.ntlworld.com/bobosola. Updated 18-Jan-2006.
+
+Use in <HEAD> with DEFER keyword wrapped in conditional comments:
+<!--[if lt IE 7]>
+<script defer type="text/javascript" src="pngfix.js"></script>
+<![endif]-->
+
+*/
+
+var arVersion = navigator.appVersion.split("MSIE")
+var version = parseFloat(arVersion[1])
+
+if ((version >= 5.5) && (document.body.filters))
+{
+ for(var i=0; i<document.images.length; i++)
+ {
+ var img = document.images[i]
+ var imgName = img.src.toUpperCase()
+ if (imgName.substring(imgName.length-3, imgName.length) == "PNG")
+ {
+ var imgID = (img.id) ? "id='" + img.id + "' " : ""
+ var imgClass = (img.className) ? "class='" + img.className + "' " : ""
+ var imgTitle = (img.title) ? "title='" + img.title + "' " : "title='" + img.alt + "' "
+ var imgStyle = "display:inline-block;" + img.style.cssText
+ if (img.align == "left") imgStyle = "float:left;" + imgStyle
+ if (img.align == "right") imgStyle = "float:right;" + imgStyle
+ if (img.parentElement.href) imgStyle = "cursor:hand;" + imgStyle
+ var strNewHTML = "<span " + imgID + imgClass + imgTitle
+ + " style=\"" + "width:" + img.width + "px; height:" + img.height + "px;" + imgStyle + ";"
+ + "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader"
+ + "(src=\'" + img.src + "\', sizingMethod='image');\"></span>"
+ img.outerHTML = strNewHTML
+ i = i-1
+ }
+ }
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/prototype.js b/subsonic-main/src/main/webapp/script/prototype.js
new file mode 100644
index 00000000..dfe8ab4e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/prototype.js
@@ -0,0 +1,4320 @@
+/* Prototype JavaScript framework, version 1.6.0.3
+ * (c) 2005-2008 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+ Version: '1.6.0.3',
+
+ Browser: {
+ IE: !!(window.attachEvent &&
+ navigator.userAgent.indexOf('Opera') === -1),
+ Opera: navigator.userAgent.indexOf('Opera') > -1,
+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 &&
+ navigator.userAgent.indexOf('KHTML') === -1,
+ MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+ },
+
+ BrowserFeatures: {
+ XPath: !!document.evaluate,
+ SelectorsAPI: !!document.querySelector,
+ ElementExtensions: !!window.HTMLElement,
+ SpecificElementExtensions:
+ document.createElement('div')['__proto__'] &&
+ document.createElement('div')['__proto__'] !==
+ document.createElement('form')['__proto__']
+ },
+
+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
+ K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+ Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+/* Based on Alex Arnell's inheritance implementation. */
+var Class = {
+ create: function() {
+ var parent = null, properties = $A(arguments);
+ if (Object.isFunction(properties[0]))
+ parent = properties.shift();
+
+ function klass() {
+ this.initialize.apply(this, arguments);
+ }
+
+ Object.extend(klass, Class.Methods);
+ klass.superclass = parent;
+ klass.subclasses = [];
+
+ if (parent) {
+ var subclass = function() { };
+ subclass.prototype = parent.prototype;
+ klass.prototype = new subclass;
+ parent.subclasses.push(klass);
+ }
+
+ for (var i = 0; i < properties.length; i++)
+ klass.addMethods(properties[i]);
+
+ if (!klass.prototype.initialize)
+ klass.prototype.initialize = Prototype.emptyFunction;
+
+ klass.prototype.constructor = klass;
+
+ return klass;
+ }
+};
+
+Class.Methods = {
+ addMethods: function(source) {
+ var ancestor = this.superclass && this.superclass.prototype;
+ var properties = Object.keys(source);
+
+ if (!Object.keys({ toString: true }).length)
+ properties.push("toString", "valueOf");
+
+ for (var i = 0, length = properties.length; i < length; i++) {
+ var property = properties[i], value = source[property];
+ if (ancestor && Object.isFunction(value) &&
+ value.argumentNames().first() == "$super") {
+ var method = value;
+ value = (function(m) {
+ return function() { return ancestor[m].apply(this, arguments) };
+ })(property).wrap(method);
+
+ value.valueOf = method.valueOf.bind(method);
+ value.toString = method.toString.bind(method);
+ }
+ this.prototype[property] = value;
+ }
+
+ return this;
+ }
+};
+
+var Abstract = { };
+
+Object.extend = function(destination, source) {
+ for (var property in source)
+ destination[property] = source[property];
+ return destination;
+};
+
+Object.extend(Object, {
+ inspect: function(object) {
+ try {
+ if (Object.isUndefined(object)) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : String(object);
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ },
+
+ toJSON: function(object) {
+ var type = typeof object;
+ switch (type) {
+ case 'undefined':
+ case 'function':
+ case 'unknown': return;
+ case 'boolean': return object.toString();
+ }
+
+ if (object === null) return 'null';
+ if (object.toJSON) return object.toJSON();
+ if (Object.isElement(object)) return;
+
+ var results = [];
+ for (var property in object) {
+ var value = Object.toJSON(object[property]);
+ if (!Object.isUndefined(value))
+ results.push(property.toJSON() + ': ' + value);
+ }
+
+ return '{' + results.join(', ') + '}';
+ },
+
+ toQueryString: function(object) {
+ return $H(object).toQueryString();
+ },
+
+ toHTML: function(object) {
+ return object && object.toHTML ? object.toHTML() : String.interpret(object);
+ },
+
+ keys: function(object) {
+ var keys = [];
+ for (var property in object)
+ keys.push(property);
+ return keys;
+ },
+
+ values: function(object) {
+ var values = [];
+ for (var property in object)
+ values.push(object[property]);
+ return values;
+ },
+
+ clone: function(object) {
+ return Object.extend({ }, object);
+ },
+
+ isElement: function(object) {
+ return !!(object && object.nodeType == 1);
+ },
+
+ isArray: function(object) {
+ return object != null && typeof object == "object" &&
+ 'splice' in object && 'join' in object;
+ },
+
+ isHash: function(object) {
+ return object instanceof Hash;
+ },
+
+ isFunction: function(object) {
+ return typeof object == "function";
+ },
+
+ isString: function(object) {
+ return typeof object == "string";
+ },
+
+ isNumber: function(object) {
+ return typeof object == "number";
+ },
+
+ isUndefined: function(object) {
+ return typeof object == "undefined";
+ }
+});
+
+Object.extend(Function.prototype, {
+ argumentNames: function() {
+ var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
+ .replace(/\s+/g, '').split(',');
+ return names.length == 1 && !names[0] ? [] : names;
+ },
+
+ bind: function() {
+ if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+ },
+
+ bindAsEventListener: function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [event || window.event].concat(args));
+ }
+ },
+
+ curry: function() {
+ if (!arguments.length) return this;
+ var __method = this, args = $A(arguments);
+ return function() {
+ return __method.apply(this, args.concat($A(arguments)));
+ }
+ },
+
+ delay: function() {
+ var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+ return window.setTimeout(function() {
+ return __method.apply(__method, args);
+ }, timeout);
+ },
+
+ defer: function() {
+ var args = [0.01].concat($A(arguments));
+ return this.delay.apply(this, args);
+ },
+
+ wrap: function(wrapper) {
+ var __method = this;
+ return function() {
+ return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+ }
+ },
+
+ methodize: function() {
+ if (this._methodized) return this._methodized;
+ var __method = this;
+ return this._methodized = function() {
+ return __method.apply(null, [this].concat($A(arguments)));
+ };
+ }
+});
+
+Date.prototype.toJSON = function() {
+ return '"' + this.getUTCFullYear() + '-' +
+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+ this.getUTCDate().toPaddedString(2) + 'T' +
+ this.getUTCHours().toPaddedString(2) + ':' +
+ this.getUTCMinutes().toPaddedString(2) + ':' +
+ this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) { }
+ }
+
+ return returnValue;
+ }
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+ return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create({
+ initialize: function(callback, frequency) {
+ this.callback = callback;
+ this.frequency = frequency;
+ this.currentlyExecuting = false;
+
+ this.registerCallback();
+ },
+
+ registerCallback: function() {
+ this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+ },
+
+ execute: function() {
+ this.callback(this);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.execute();
+ } finally {
+ this.currentlyExecuting = false;
+ }
+ }
+ }
+});
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
+
+Object.extend(String.prototype, {
+ gsub: function(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = arguments.callee.prepareReplacement(replacement);
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ },
+
+ sub: function(pattern, replacement, count) {
+ replacement = this.gsub.prepareReplacement(replacement);
+ count = Object.isUndefined(count) ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ },
+
+ scan: function(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return String(this);
+ },
+
+ truncate: function(length, truncation) {
+ length = length || 30;
+ truncation = Object.isUndefined(truncation) ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : String(this);
+ },
+
+ strip: function() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ stripTags: function() {
+ return this.replace(/<\/?[^>]+>/gi, '');
+ },
+
+ stripScripts: function() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ },
+
+ extractScripts: function() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+ var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ },
+
+ evalScripts: function() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ },
+
+ escapeHTML: function() {
+ var self = arguments.callee;
+ self.text.data = this;
+ return self.div.innerHTML;
+ },
+
+ unescapeHTML: function() {
+ var div = new Element('div');
+ div.innerHTML = this.stripTags();
+ return div.childNodes[0] ? (div.childNodes.length > 1 ?
+ $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+ div.childNodes[0].nodeValue) : '';
+ },
+
+ toQueryParams: function(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return { };
+
+ return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ return hash;
+ });
+ },
+
+ toArray: function() {
+ return this.split('');
+ },
+
+ succ: function() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ },
+
+ times: function(count) {
+ return count < 1 ? '' : new Array(count + 1).join(this);
+ },
+
+ camelize: function() {
+ var parts = this.split('-'), len = parts.length;
+ if (len == 1) return parts[0];
+
+ var camelized = this.charAt(0) == '-'
+ ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+ : parts[0];
+
+ for (var i = 1; i < len; i++)
+ camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+ return camelized;
+ },
+
+ capitalize: function() {
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ },
+
+ underscore: function() {
+ return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+ },
+
+ dasherize: function() {
+ return this.gsub(/_/,'-');
+ },
+
+ inspect: function(useDoubleQuotes) {
+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ },
+
+ toJSON: function() {
+ return this.inspect(true);
+ },
+
+ unfilterJSON: function(filter) {
+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
+ },
+
+ isJSON: function() {
+ var str = this;
+ if (str.blank()) return false;
+ str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ },
+
+ evalJSON: function(sanitize) {
+ var json = this.unfilterJSON();
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ },
+
+ include: function(pattern) {
+ return this.indexOf(pattern) > -1;
+ },
+
+ startsWith: function(pattern) {
+ return this.indexOf(pattern) === 0;
+ },
+
+ endsWith: function(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.lastIndexOf(pattern) === d;
+ },
+
+ empty: function() {
+ return this == '';
+ },
+
+ blank: function() {
+ return /^\s*$/.test(this);
+ },
+
+ interpolate: function(object, pattern) {
+ return new Template(this, pattern).evaluate(object);
+ }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+ escapeHTML: function() {
+ return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ unescapeHTML: function() {
+ return this.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+ }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+ if (Object.isFunction(replacement)) return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+};
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+ div: document.createElement('div'),
+ text: document.createTextNode('')
+});
+
+String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);
+
+var Template = Class.create({
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ if (Object.isFunction(object.toTemplateReplacements))
+ object = object.toTemplateReplacements();
+
+ return this.template.gsub(this.pattern, function(match) {
+ if (object == null) return '';
+
+ var before = match[1] || '';
+ if (before == '\\') return match[2];
+
+ var ctx = object, expr = match[3];
+ var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+ match = pattern.exec(expr);
+ if (match == null) return before;
+
+ while (match != null) {
+ var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+ ctx = ctx[comp];
+ if (null == ctx || '' == match[3]) break;
+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+ match = pattern.exec(expr);
+ }
+
+ return before + String.interpret(ctx);
+ });
+ }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = {
+ each: function(iterator, context) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ iterator.call(context, value, index++);
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ },
+
+ eachSlice: function(number, iterator, context) {
+ var index = -number, slices = [], array = this.toArray();
+ if (number < 1) return array;
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.collect(iterator, context);
+ },
+
+ all: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!iterator.call(context, value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ },
+
+ any: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!iterator.call(context, value, index))
+ throw $break;
+ });
+ return result;
+ },
+
+ collect: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var results = [];
+ this.each(function(value, index) {
+ results.push(iterator.call(context, value, index));
+ });
+ return results;
+ },
+
+ detect: function(iterator, context) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator.call(context, value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ },
+
+ findAll: function(iterator, context) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator.call(context, value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ grep: function(filter, iterator, context) {
+ iterator = iterator || Prototype.K;
+ var results = [];
+
+ if (Object.isString(filter))
+ filter = new RegExp(filter);
+
+ this.each(function(value, index) {
+ if (filter.match(value))
+ results.push(iterator.call(context, value, index));
+ });
+ return results;
+ },
+
+ include: function(object) {
+ if (Object.isFunction(this.indexOf))
+ if (this.indexOf(object) != -1) return true;
+
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ },
+
+ inGroupsOf: function(number, fillWith) {
+ fillWith = Object.isUndefined(fillWith) ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ },
+
+ inject: function(memo, iterator, context) {
+ this.each(function(value, index) {
+ memo = iterator.call(context, memo, value, index);
+ });
+ return memo;
+ },
+
+ invoke: function(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ },
+
+ max: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator.call(context, value, index);
+ if (result == null || value >= result)
+ result = value;
+ });
+ return result;
+ },
+
+ min: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator.call(context, value, index);
+ if (result == null || value < result)
+ result = value;
+ });
+ return result;
+ },
+
+ partition: function(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ (iterator.call(context, value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ },
+
+ pluck: function(property) {
+ var results = [];
+ this.each(function(value) {
+ results.push(value[property]);
+ });
+ return results;
+ },
+
+ reject: function(iterator, context) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator.call(context, value, index))
+ results.push(value);
+ });
+ return results;
+ },
+
+ sortBy: function(iterator, context) {
+ return this.map(function(value, index) {
+ return {
+ value: value,
+ criteria: iterator.call(context, value, index)
+ };
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ },
+
+ toArray: function() {
+ return this.map();
+ },
+
+ zip: function() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (Object.isFunction(args.last()))
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ },
+
+ size: function() {
+ return this.toArray().length;
+ },
+
+ inspect: function() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+};
+
+Object.extend(Enumerable, {
+ map: Enumerable.collect,
+ find: Enumerable.detect,
+ select: Enumerable.findAll,
+ filter: Enumerable.findAll,
+ member: Enumerable.include,
+ entries: Enumerable.toArray,
+ every: Enumerable.all,
+ some: Enumerable.any
+});
+function $A(iterable) {
+ if (!iterable) return [];
+ if (iterable.toArray) return iterable.toArray();
+ var length = iterable.length || 0, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+}
+
+if (Prototype.Browser.WebKit) {
+ $A = function(iterable) {
+ if (!iterable) return [];
+ // In Safari, only use the `toArray` method if it's not a NodeList.
+ // A NodeList is a function, has an function `item` property, and a numeric
+ // `length` property. Adapted from Google Doctype.
+ if (!(typeof iterable === 'function' && typeof iterable.length ===
+ 'number' && typeof iterable.item === 'function') && iterable.toArray)
+ return iterable.toArray();
+ var length = iterable.length || 0, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+ };
+}
+
+Array.from = $A;
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+ _each: function(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ },
+
+ clear: function() {
+ this.length = 0;
+ return this;
+ },
+
+ first: function() {
+ return this[0];
+ },
+
+ last: function() {
+ return this[this.length - 1];
+ },
+
+ compact: function() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ },
+
+ flatten: function() {
+ return this.inject([], function(array, value) {
+ return array.concat(Object.isArray(value) ?
+ value.flatten() : [value]);
+ });
+ },
+
+ without: function() {
+ var values = $A(arguments);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ },
+
+ reverse: function(inline) {
+ return (inline !== false ? this : this.toArray())._reverse();
+ },
+
+ reduce: function() {
+ return this.length > 1 ? this : this[0];
+ },
+
+ uniq: function(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ },
+
+ intersect: function(array) {
+ return this.uniq().findAll(function(item) {
+ return array.detect(function(value) { return item === value });
+ });
+ },
+
+ clone: function() {
+ return [].concat(this);
+ },
+
+ size: function() {
+ return this.length;
+ },
+
+ inspect: function() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ },
+
+ toJSON: function() {
+ var results = [];
+ this.each(function(object) {
+ var value = Object.toJSON(object);
+ if (!Object.isUndefined(value)) results.push(value);
+ });
+ return '[' + results.join(', ') + ']';
+ }
+});
+
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+ Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+ i || (i = 0);
+ var length = this.length;
+ if (i < 0) i = length + i;
+ for (; i < length; i++)
+ if (this[i] === item) return i;
+ return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+ i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+ var n = this.slice(0, i).reverse().indexOf(item);
+ return (n < 0) ? n : i - n - 1;
+};
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+ if (!Object.isString(string)) return [];
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+ Array.prototype.concat = function() {
+ var array = [];
+ for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ if (Object.isArray(arguments[i])) {
+ for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+ array.push(arguments[i][j]);
+ } else {
+ array.push(arguments[i]);
+ }
+ }
+ return array;
+ };
+}
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ return this.toPaddedString(2, 16);
+ },
+
+ succ: function() {
+ return this + 1;
+ },
+
+ times: function(iterator, context) {
+ $R(0, this, true).each(iterator, context);
+ return this;
+ },
+
+ toPaddedString: function(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
+ },
+
+ toJSON: function() {
+ return isFinite(this) ? this.toString() : 'null';
+ }
+});
+
+$w('abs round ceil floor').each(function(method){
+ Number.prototype[method] = Math[method].methodize();
+});
+function $H(object) {
+ return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+
+ function toQueryPair(key, value) {
+ if (Object.isUndefined(value)) return key;
+ return key + '=' + encodeURIComponent(String.interpret(value));
+ }
+
+ return {
+ initialize: function(object) {
+ this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+ },
+
+ _each: function(iterator) {
+ for (var key in this._object) {
+ var value = this._object[key], pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ set: function(key, value) {
+ return this._object[key] = value;
+ },
+
+ get: function(key) {
+ // simulating poorly supported hasOwnProperty
+ if (this._object[key] !== Object.prototype[key])
+ return this._object[key];
+ },
+
+ unset: function(key) {
+ var value = this._object[key];
+ delete this._object[key];
+ return value;
+ },
+
+ toObject: function() {
+ return Object.clone(this._object);
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ index: function(value) {
+ var match = this.detect(function(pair) {
+ return pair.value === value;
+ });
+ return match && match.key;
+ },
+
+ merge: function(object) {
+ return this.clone().update(object);
+ },
+
+ update: function(object) {
+ return new Hash(object).inject(this, function(result, pair) {
+ result.set(pair.key, pair.value);
+ return result;
+ });
+ },
+
+ toQueryString: function() {
+ return this.inject([], function(results, pair) {
+ var key = encodeURIComponent(pair.key), values = pair.value;
+
+ if (values && typeof values == 'object') {
+ if (Object.isArray(values))
+ return results.concat(values.map(toQueryPair.curry(key)));
+ } else results.push(toQueryPair(key, values));
+ return results;
+ }).join('&');
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ },
+
+ toJSON: function() {
+ return Object.toJSON(this.toObject());
+ },
+
+ clone: function() {
+ return new Hash(this);
+ }
+ }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
+ initialize: function(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ },
+
+ _each: function(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ },
+
+ include: function(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+});
+
+var $R = function(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+};
+
+var Ajax = {
+ getTransport: function() {
+ return Try.these(
+ function() {return new XMLHttpRequest()},
+ function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+ function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+ ) || false;
+ },
+
+ activeRequestCount: 0
+};
+
+Ajax.Responders = {
+ responders: [],
+
+ _each: function(iterator) {
+ this.responders._each(iterator);
+ },
+
+ register: function(responder) {
+ if (!this.include(responder))
+ this.responders.push(responder);
+ },
+
+ unregister: function(responder) {
+ this.responders = this.responders.without(responder);
+ },
+
+ dispatch: function(callback, request, transport, json) {
+ this.each(function(responder) {
+ if (Object.isFunction(responder[callback])) {
+ try {
+ responder[callback].apply(responder, [request, transport, json]);
+ } catch (e) { }
+ }
+ });
+ }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+ onCreate: function() { Ajax.activeRequestCount++ },
+ onComplete: function() { Ajax.activeRequestCount-- }
+});
+
+Ajax.Base = Class.create({
+ initialize: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: '',
+ evalJSON: true,
+ evalJS: true
+ };
+ Object.extend(this.options, options || { });
+
+ this.options.method = this.options.method.toLowerCase();
+
+ if (Object.isString(this.options.parameters))
+ this.options.parameters = this.options.parameters.toQueryParams();
+ else if (Object.isHash(this.options.parameters))
+ this.options.parameters = this.options.parameters.toObject();
+ }
+});
+
+Ajax.Request = Class.create(Ajax.Base, {
+ _complete: false,
+
+ initialize: function($super, url, options) {
+ $super(options);
+ this.transport = Ajax.getTransport();
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = Object.clone(this.options.parameters);
+
+ if (!['get', 'post'].include(this.method)) {
+ // simulate other verbs over post
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ this.parameters = params;
+
+ if (params = Object.toQueryString(params)) {
+ // when GET, append parameters to URL
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
+
+ try {
+ var response = new Ajax.Response(this);
+ if (this.options.onCreate) this.options.onCreate(response);
+ Ajax.Responders.dispatch('onCreate', this, response);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ // user-defined headers
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (Object.isFunction(extras.push))
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ var status = this.getStatus();
+ return !status || (status >= 200 && status < 300);
+ },
+
+ getStatus: function() {
+ try {
+ return this.transport.status || 0;
+ } catch (e) { return 0 }
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + response.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ var contentType = response.getHeader('Content-type');
+ if (this.options.evalJS == 'force'
+ || (this.options.evalJS && this.isSameOrigin() && contentType
+ && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+ Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ // avoid memory leak in MSIE: clean up
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ isSameOrigin: function() {
+ var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+ return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+ protocol: location.protocol,
+ domain: document.domain,
+ port: location.port ? ':' + location.port : ''
+ }));
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name) || null;
+ } catch (e) { return null }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval((this.transport.responseText || '').unfilterJSON());
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+ initialize: function(request){
+ this.request = request;
+ var transport = this.transport = request.transport,
+ readyState = this.readyState = transport.readyState;
+
+ if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+ this.status = this.getStatus();
+ this.statusText = this.getStatusText();
+ this.responseText = String.interpret(transport.responseText);
+ this.headerJSON = this._getHeaderJSON();
+ }
+
+ if(readyState == 4) {
+ var xml = transport.responseXML;
+ this.responseXML = Object.isUndefined(xml) ? null : xml;
+ this.responseJSON = this._getResponseJSON();
+ }
+ },
+
+ status: 0,
+ statusText: '',
+
+ getStatus: Ajax.Request.prototype.getStatus,
+
+ getStatusText: function() {
+ try {
+ return this.transport.statusText || '';
+ } catch (e) { return '' }
+ },
+
+ getHeader: Ajax.Request.prototype.getHeader,
+
+ getAllHeaders: function() {
+ try {
+ return this.getAllResponseHeaders();
+ } catch (e) { return null }
+ },
+
+ getResponseHeader: function(name) {
+ return this.transport.getResponseHeader(name);
+ },
+
+ getAllResponseHeaders: function() {
+ return this.transport.getAllResponseHeaders();
+ },
+
+ _getHeaderJSON: function() {
+ var json = this.getHeader('X-JSON');
+ if (!json) return null;
+ json = decodeURIComponent(escape(json));
+ try {
+ return json.evalJSON(this.request.options.sanitizeJSON ||
+ !this.request.isSameOrigin());
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ },
+
+ _getResponseJSON: function() {
+ var options = this.request.options;
+ if (!options.evalJSON || (options.evalJSON != 'force' &&
+ !(this.getHeader('Content-type') || '').include('application/json')) ||
+ this.responseText.blank())
+ return null;
+ try {
+ return this.responseText.evalJSON(options.sanitizeJSON ||
+ !this.request.isSameOrigin());
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+ initialize: function($super, container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ };
+
+ options = Object.clone(options);
+ var onComplete = options.onComplete;
+ options.onComplete = (function(response, json) {
+ this.updateContent(response.responseText);
+ if (Object.isFunction(onComplete)) onComplete(response, json);
+ }).bind(this);
+
+ $super(url, options);
+ },
+
+ updateContent: function(responseText) {
+ var receiver = this.container[this.success() ? 'success' : 'failure'],
+ options = this.options;
+
+ if (!options.evalScripts) responseText = responseText.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (options.insertion) {
+ if (Object.isString(options.insertion)) {
+ var insertion = { }; insertion[options.insertion] = responseText;
+ receiver.insert(insertion);
+ }
+ else options.insertion(receiver, responseText);
+ }
+ else receiver.update(responseText);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+ initialize: function($super, container, url, options) {
+ $super(options);
+ this.onComplete = this.options.onComplete;
+
+ this.frequency = (this.options.frequency || 2);
+ this.decay = (this.options.decay || 1);
+
+ this.updater = { };
+ this.container = container;
+ this.url = url;
+
+ this.start();
+ },
+
+ start: function() {
+ this.options.onComplete = this.updateComplete.bind(this);
+ this.onTimerEvent();
+ },
+
+ stop: function() {
+ this.updater.options.onComplete = undefined;
+ clearTimeout(this.timer);
+ (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+ },
+
+ updateComplete: function(response) {
+ if (this.options.decay) {
+ this.decay = (response.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = response.responseText;
+ }
+ this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (Object.isString(element))
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(Element.extend(query.snapshotItem(i)));
+ return results;
+ };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+ // DOM level 2 ECMAScript Language Binding
+ Object.extend(Node, {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ });
+}
+
+(function() {
+ var element = this.Element;
+ this.Element = function(tagName, attributes) {
+ attributes = attributes || { };
+ tagName = tagName.toLowerCase();
+ var cache = Element.cache;
+ if (Prototype.Browser.IE && attributes.name) {
+ tagName = '<' + tagName + ' name="' + attributes.name + '">';
+ delete attributes.name;
+ return Element.writeAttribute(document.createElement(tagName), attributes);
+ }
+ if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+ return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+ };
+ Object.extend(this.Element, element || { });
+ if (element) this.Element.prototype = element.prototype;
+}).call(window);
+
+Element.cache = { };
+
+Element.Methods = {
+ visible: function(element) {
+ return $(element).style.display != 'none';
+ },
+
+ toggle: function(element) {
+ element = $(element);
+ Element[Element.visible(element) ? 'hide' : 'show'](element);
+ return element;
+ },
+
+ hide: function(element) {
+ element = $(element);
+ element.style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ element = $(element);
+ element.style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+ content = Object.toHTML(content);
+ element.innerHTML = content.stripScripts();
+ content.evalScripts.bind(content).defer();
+ return element;
+ },
+
+ replace: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ else if (!Object.isElement(content)) {
+ content = Object.toHTML(content);
+ var range = element.ownerDocument.createRange();
+ range.selectNode(element);
+ content.evalScripts.bind(content).defer();
+ content = range.createContextualFragment(content.stripScripts());
+ }
+ element.parentNode.replaceChild(content, element);
+ return element;
+ },
+
+ insert: function(element, insertions) {
+ element = $(element);
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = {bottom:insertions};
+
+ var content, insert, tagName, childNodes;
+
+ for (var position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ insert = Element._insertionTranslations[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ insert(element, content);
+ continue;
+ }
+
+ content = Object.toHTML(content);
+
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+ if (position == 'top' || position == 'after') childNodes.reverse();
+ childNodes.each(insert.curry(element));
+
+ content.evalScripts.bind(content).defer();
+ }
+
+ return element;
+ },
+
+ wrap: function(element, wrapper, attributes) {
+ element = $(element);
+ if (Object.isElement(wrapper))
+ $(wrapper).writeAttribute(attributes || { });
+ else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+ else wrapper = new Element('div', wrapper);
+ if (element.parentNode)
+ element.parentNode.replaceChild(wrapper, element);
+ wrapper.appendChild(element);
+ return wrapper;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(), attribute = pair.last();
+ var value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property) {
+ element = $(element);
+ var elements = [];
+ while (element = element[property])
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return $(element).recursivelyCollect('parentNode');
+ },
+
+ descendants: function(element) {
+ return $(element).select("*");
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
+ },
+
+ immediateDescendants: function(element) {
+ if (!(element = $(element).firstChild)) return [];
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ if (element) return [element].concat($(element).nextSiblings());
+ return [];
+ },
+
+ previousSiblings: function(element) {
+ return $(element).recursivelyCollect('previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return $(element).recursivelyCollect('nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return element.previousSiblings().reverse().concat(element.nextSiblings());
+ },
+
+ match: function(element, selector) {
+ if (Object.isString(selector))
+ selector = new Selector(selector);
+ return selector.match($(element));
+ },
+
+ up: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = element.ancestors();
+ return Object.isNumber(expression) ? ancestors[expression] :
+ Selector.findElement(ancestors, expression, index);
+ },
+
+ down: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ return Object.isNumber(expression) ? element.descendants()[expression] :
+ Element.select(element, expression)[index || 0];
+ },
+
+ previous: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+ var previousSiblings = element.previousSiblings();
+ return Object.isNumber(expression) ? previousSiblings[expression] :
+ Selector.findElement(previousSiblings, expression, index);
+ },
+
+ next: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+ var nextSiblings = element.nextSiblings();
+ return Object.isNumber(expression) ? nextSiblings[expression] :
+ Selector.findElement(nextSiblings, expression, index);
+ },
+
+ select: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element, args);
+ },
+
+ adjacent: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element.parentNode, args).without(element);
+ },
+
+ identify: function(element) {
+ element = $(element);
+ var id = element.readAttribute('id'), self = arguments.callee;
+ if (id) return id;
+ do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+ element.writeAttribute('id', id);
+ return id;
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (Prototype.Browser.IE) {
+ var t = Element._attributeTranslations.read;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ if (name.include(':')) {
+ return (!element.attributes || !element.attributes[name]) ? null :
+ element.attributes[name].value;
+ }
+ }
+ return element.getAttribute(name);
+ },
+
+ writeAttribute: function(element, name, value) {
+ element = $(element);
+ var attributes = { }, t = Element._attributeTranslations.write;
+
+ if (typeof name == 'object') attributes = name;
+ else attributes[name] = Object.isUndefined(value) ? true : value;
+
+ for (var attr in attributes) {
+ name = t.names[attr] || attr;
+ value = attributes[attr];
+ if (t.values[attr]) name = t.values[attr](element, value);
+ if (value === false || value === null)
+ element.removeAttribute(name);
+ else if (value === true)
+ element.setAttribute(name, name);
+ else element.setAttribute(name, value);
+ }
+ return element;
+ },
+
+ getHeight: function(element) {
+ return $(element).getDimensions().height;
+ },
+
+ getWidth: function(element) {
+ return $(element).getDimensions().width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ return (elementClassName.length > 0 && (elementClassName == className ||
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ if (!element.hasClassName(className))
+ element.className += (element.className ? ' ' : '') + className;
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ element.className = element.className.replace(
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return element[element.hasClassName(className) ?
+ 'removeClassName' : 'addClassName'](className);
+ },
+
+ // removes whitespace-only text node children
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.blank();
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+
+ if (element.compareDocumentPosition)
+ return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+ if (ancestor.contains)
+ return ancestor.contains(element) && ancestor !== element;
+
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = element.cumulativeOffset();
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ style = style == 'float' ? 'cssFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value || value == 'auto') {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ }
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
+
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
+
+ setStyle: function(element, styles) {
+ element = $(element);
+ var elementStyle = element.style, match;
+ if (Object.isString(styles)) {
+ element.style.cssText += ';' + styles;
+ return styles.include('opacity') ?
+ element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+ }
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property]);
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+ property] = styles[property];
+
+ return element;
+ },
+
+ setOpacity: function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+ return element;
+ },
+
+ getDimensions: function(element) {
+ element = $(element);
+ var display = element.getStyle('display');
+ if (display != 'none' && display != null) // Safari bug
+ return {width: element.offsetWidth, height: element.offsetHeight};
+
+ // All *Width and *Height properties give 0 on elements with display none,
+ // so enable the element temporarily
+ var els = element.style;
+ var originalVisibility = els.visibility;
+ var originalPosition = els.position;
+ var originalDisplay = els.display;
+ els.visibility = 'hidden';
+ els.position = 'absolute';
+ els.display = 'block';
+ var originalWidth = element.clientWidth;
+ var originalHeight = element.clientHeight;
+ els.display = originalDisplay;
+ els.position = originalPosition;
+ els.visibility = originalVisibility;
+ return {width: originalWidth, height: originalHeight};
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ // Opera returns the offset relative to the positioning context, when an
+ // element is position relative but top and left have not been defined
+ if (Prototype.Browser.Opera) {
+ element.style.top = 0;
+ element.style.left = 0;
+ }
+ }
+ return element;
+ },
+
+ undoPositioned: function(element) {
+ element = $(element);
+ if (element._madePositioned) {
+ element._madePositioned = undefined;
+ element.style.position =
+ element.style.top =
+ element.style.left =
+ element.style.bottom =
+ element.style.right = '';
+ }
+ return element;
+ },
+
+ makeClipping: function(element) {
+ element = $(element);
+ if (element._overflow) return element;
+ element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+ if (element._overflow !== 'hidden')
+ element.style.overflow = 'hidden';
+ return element;
+ },
+
+ undoClipping: function(element) {
+ element = $(element);
+ if (!element._overflow) return element;
+ element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+ element._overflow = null;
+ return element;
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ positionedOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if (element.tagName.toUpperCase() == 'BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p !== 'static') break;
+ }
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'absolute') return element;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ var offsets = element.positionedOffset();
+ var top = offsets[1];
+ var left = offsets[0];
+ var width = element.clientWidth;
+ var height = element.clientHeight;
+
+ element._originalLeft = left - parseFloat(element.style.left || 0);
+ element._originalTop = top - parseFloat(element.style.top || 0);
+ element._originalWidth = element.style.width;
+ element._originalHeight = element.style.height;
+
+ element.style.position = 'absolute';
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.width = width + 'px';
+ element.style.height = height + 'px';
+ return element;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'relative') return element;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
+ var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ element.style.height = element._originalHeight;
+ element.style.width = element._originalWidth;
+ return element;
+ },
+
+ cumulativeScrollOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ getOffsetParent: function(element) {
+ if (element.offsetParent) return $(element.offsetParent);
+ if (element == document.body) return $(element);
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return $(element);
+
+ return $(document.body);
+ },
+
+ viewportOffset: function(forElement) {
+ var valueT = 0, valueL = 0;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ // Safari fix
+ if (element.offsetParent == document.body &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ clonePosition: function(element, source) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || { });
+
+ // find page position of source
+ source = $(source);
+ var p = source.viewportOffset();
+
+ // find coordinate system to use
+ element = $(element);
+ var delta = [0, 0];
+ var parent = null;
+ // delta [0,0] will do fine with position: fixed elements,
+ // position:absolute needs offsetParent deltas
+ if (Element.getStyle(element, 'position') == 'absolute') {
+ parent = element.getOffsetParent();
+ delta = parent.viewportOffset();
+ }
+
+ // correct by body offsets (fixes Safari)
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ // set position
+ if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if (options.setWidth) element.style.width = source.offsetWidth + 'px';
+ if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+ return element;
+ }
+};
+
+Element.Methods.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+ getElementsBySelector: Element.Methods.select,
+ childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+ write: {
+ names: {
+ className: 'class',
+ htmlFor: 'for'
+ },
+ values: { }
+ }
+};
+
+if (Prototype.Browser.Opera) {
+ Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+ function(proceed, element, style) {
+ switch (style) {
+ case 'left': case 'top': case 'right': case 'bottom':
+ if (proceed(element, 'position') === 'static') return null;
+ case 'height': case 'width':
+ // returns '0px' for hidden elements; we want it to return null
+ if (!Element.visible(element)) return null;
+
+ // returns the border-box dimensions rather than the content-box
+ // dimensions, so we subtract padding and borders from the value
+ var dim = parseInt(proceed(element, style), 10);
+
+ if (dim !== element['offset' + style.capitalize()])
+ return dim + 'px';
+
+ var properties;
+ if (style === 'height') {
+ properties = ['border-top-width', 'padding-top',
+ 'padding-bottom', 'border-bottom-width'];
+ }
+ else {
+ properties = ['border-left-width', 'padding-left',
+ 'padding-right', 'border-right-width'];
+ }
+ return properties.inject(dim, function(memo, property) {
+ var val = proceed(element, property);
+ return val === null ? memo : memo - parseInt(val, 10);
+ }) + 'px';
+ default: return proceed(element, style);
+ }
+ }
+ );
+
+ Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+ function(proceed, element, attribute) {
+ if (attribute === 'title') return element.title;
+ return proceed(element, attribute);
+ }
+ );
+}
+
+else if (Prototype.Browser.IE) {
+ // IE doesn't report offsets correctly for static elements, so we change them
+ // to "relative" to get the values, then change them back.
+ Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+ function(proceed, element) {
+ element = $(element);
+ // IE throws an error if element is not in document
+ try { element.offsetParent }
+ catch(e) { return $(document.body) }
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+
+ $w('positionedOffset viewportOffset').each(function(method) {
+ Element.Methods[method] = Element.Methods[method].wrap(
+ function(proceed, element) {
+ element = $(element);
+ try { element.offsetParent }
+ catch(e) { return Element._returnOffset(0,0) }
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ // Trigger hasLayout on the offset parent so that IE6 reports
+ // accurate offsetTop and offsetLeft values for position: fixed.
+ var offsetParent = element.getOffsetParent();
+ if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+ offsetParent.setStyle({ zoom: 1 });
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+ });
+
+ Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
+ function(proceed, element) {
+ try { element.offsetParent }
+ catch(e) { return Element._returnOffset(0,0) }
+ return proceed(element);
+ }
+ );
+
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
+
+ if (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset' + style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ function stripAlpha(filter){
+ return filter.replace(/alpha\([^\)]*\)/gi,'');
+ }
+ element = $(element);
+ var currentStyle = element.currentStyle;
+ if ((currentStyle && !currentStyle.hasLayout) ||
+ (!currentStyle && element.style.zoom == 'normal'))
+ element.style.zoom = 1;
+
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ (filter = stripAlpha(filter)) ?
+ style.filter = filter : style.removeAttribute('filter');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = stripAlpha(filter) +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ Element._attributeTranslations = {
+ read: {
+ names: {
+ 'class': 'className',
+ 'for': 'htmlFor'
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _getAttrNode: function(element, attribute) {
+ var node = element.getAttributeNode(attribute);
+ return node ? node.value : "";
+ },
+ _getEv: function(element, attribute) {
+ attribute = element.getAttribute(attribute);
+ return attribute ? attribute.toString().slice(23, -2) : null;
+ },
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ return element.title;
+ }
+ }
+ }
+ };
+
+ Element._attributeTranslations.write = {
+ names: Object.extend({
+ cellpadding: 'cellPadding',
+ cellspacing: 'cellSpacing'
+ }, Element._attributeTranslations.read.names),
+ values: {
+ checked: function(element, value) {
+ element.checked = !!value;
+ },
+
+ style: function(element, value) {
+ element.style.cssText = value ? value : '';
+ }
+ }
+ };
+
+ Element._attributeTranslations.has = {};
+
+ $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+ 'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
+ Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+ Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+ });
+
+ (function(v) {
+ Object.extend(v, {
+ href: v._getAttr,
+ src: v._getAttr,
+ type: v._getAttr,
+ action: v._getAttrNode,
+ disabled: v._flag,
+ checked: v._flag,
+ readonly: v._flag,
+ multiple: v._flag,
+ onload: v._getEv,
+ onunload: v._getEv,
+ onclick: v._getEv,
+ ondblclick: v._getEv,
+ onmousedown: v._getEv,
+ onmouseup: v._getEv,
+ onmouseover: v._getEv,
+ onmousemove: v._getEv,
+ onmouseout: v._getEv,
+ onfocus: v._getEv,
+ onblur: v._getEv,
+ onkeypress: v._getEv,
+ onkeydown: v._getEv,
+ onkeyup: v._getEv,
+ onsubmit: v._getEv,
+ onreset: v._getEv,
+ onselect: v._getEv,
+ onchange: v._getEv
+ });
+ })(Element._attributeTranslations.read.values);
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
+
+else if (Prototype.Browser.WebKit) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+
+ if (value == 1)
+ if(element.tagName.toUpperCase() == 'IMG' && element.width) {
+ element.width++; element.width--;
+ } else try {
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch (e) { }
+
+ return element;
+ };
+
+ // Safari returns margins on body which is incorrect if the child is absolutely
+ // positioned. For performance reasons, redefine Element#cumulativeOffset for
+ // KHTML/WebKit only.
+ Element.Methods.cumulativeOffset = function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == document.body)
+ if (Element.getStyle(element, 'position') == 'absolute') break;
+
+ element = element.offsetParent;
+ } while (element);
+
+ return Element._returnOffset(valueL, valueT);
+ };
+}
+
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+ // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+ Element.Methods.update = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+
+ content = Object.toHTML(content);
+ var tagName = element.tagName.toUpperCase();
+
+ if (tagName in Element._insertionTranslations.tags) {
+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
+ Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+ .each(function(node) { element.appendChild(node) });
+ }
+ else element.innerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+if ('outerHTML' in document.createElement('div')) {
+ Element.Methods.replace = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ element.parentNode.replaceChild(content, element);
+ return element;
+ }
+
+ content = Object.toHTML(content);
+ var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+ if (Element._insertionTranslations.tags[tagName]) {
+ var nextSibling = element.next();
+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ parent.removeChild(element);
+ if (nextSibling)
+ fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+ else
+ fragments.each(function(node) { parent.appendChild(node) });
+ }
+ else element.outerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+Element._returnOffset = function(l, t) {
+ var result = [l, t];
+ result.left = l;
+ result.top = t;
+ return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+ var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+ if (t) {
+ div.innerHTML = t[0] + html + t[1];
+ t[2].times(function() { div = div.firstChild });
+ } else div.innerHTML = html;
+ return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+ before: function(element, node) {
+ element.parentNode.insertBefore(node, element);
+ },
+ top: function(element, node) {
+ element.insertBefore(node, element.firstChild);
+ },
+ bottom: function(element, node) {
+ element.appendChild(node);
+ },
+ after: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
+ },
+ tags: {
+ TABLE: ['<table>', '</table>', 1],
+ TBODY: ['<table><tbody>', '</tbody></table>', 2],
+ TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
+ TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+ SELECT: ['<select>', '</select>', 1]
+ }
+};
+
+(function() {
+ Object.extend(this.tags, {
+ THEAD: this.tags.TBODY,
+ TFOOT: this.tags.TBODY,
+ TH: this.tags.TD
+ });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ attribute = Element._attributeTranslations.has[attribute] || attribute;
+ var node = $(element).getAttributeNode(attribute);
+ return !!(node && node.specified);
+ }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div')['__proto__']) {
+ window.HTMLElement = { };
+ window.HTMLElement.prototype = document.createElement('div')['__proto__'];
+ Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.extend = (function() {
+ if (Prototype.BrowserFeatures.SpecificElementExtensions)
+ return Prototype.K;
+
+ var Methods = { }, ByTag = Element.Methods.ByTag;
+
+ var extend = Object.extend(function(element) {
+ if (!element || element._extendedByPrototype ||
+ element.nodeType != 1 || element == window) return element;
+
+ var methods = Object.clone(Methods),
+ tagName = element.tagName.toUpperCase(), property, value;
+
+ // extend methods for specific tags
+ if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+ for (property in methods) {
+ value = methods[property];
+ if (Object.isFunction(value) && !(property in element))
+ element[property] = value.methodize();
+ }
+
+ element._extendedByPrototype = Prototype.emptyFunction;
+ return element;
+
+ }, {
+ refresh: function() {
+ // extend methods for all tags (Safari doesn't need this)
+ if (!Prototype.BrowserFeatures.ElementExtensions) {
+ Object.extend(Methods, Element.Methods);
+ Object.extend(Methods, Element.Methods.Simulated);
+ }
+ }
+ });
+
+ extend.refresh();
+ return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+ if (element.hasAttribute) return element.hasAttribute(attribute);
+ return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
+ }
+
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
+
+ if (!tagName) Object.extend(Element.Methods, methods || { });
+ else {
+ if (Object.isArray(tagName)) tagName.each(extend);
+ else extend(tagName);
+ }
+
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = { };
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!Object.isFunction(value)) continue;
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = value.methodize();
+ }
+ }
+
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ window[klass] = { };
+ window[klass].prototype = document.createElement(tagName)['__proto__'];
+ return window[klass];
+ }
+
+ if (F.ElementExtensions) {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+ }
+
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (Object.isUndefined(klass)) continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
+
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
+
+ if (Element.extend.refresh) Element.extend.refresh();
+ Element.cache = { };
+};
+
+document.viewport = {
+ getDimensions: function() {
+ var dimensions = { }, B = Prototype.Browser;
+ $w('width height').each(function(d) {
+ var D = d.capitalize();
+ if (B.WebKit && !document.evaluate) {
+ // Safari <3.0 needs self.innerWidth/Height
+ dimensions[d] = self['inner' + D];
+ } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
+ // Opera <9.5 needs document.body.clientWidth/Height
+ dimensions[d] = document.body['client' + D]
+ } else {
+ dimensions[d] = document.documentElement['client' + D];
+ }
+ });
+ return dimensions;
+ },
+
+ getWidth: function() {
+ return this.getDimensions().width;
+ },
+
+ getHeight: function() {
+ return this.getDimensions().height;
+ },
+
+ getScrollOffsets: function() {
+ return Element._returnOffset(
+ window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+ window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+ }
+};
+/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license. Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create({
+ initialize: function(expression) {
+ this.expression = expression.strip();
+
+ if (this.shouldUseSelectorsAPI()) {
+ this.mode = 'selectorsAPI';
+ } else if (this.shouldUseXPath()) {
+ this.mode = 'xpath';
+ this.compileXPathMatcher();
+ } else {
+ this.mode = "normal";
+ this.compileMatcher();
+ }
+
+ },
+
+ shouldUseXPath: function() {
+ if (!Prototype.BrowserFeatures.XPath) return false;
+
+ var e = this.expression;
+
+ // Safari 3 chokes on :*-of-type and :empty
+ if (Prototype.Browser.WebKit &&
+ (e.include("-of-type") || e.include(":empty")))
+ return false;
+
+ // XPath can't do namespaced attributes, nor can it read
+ // the "checked" property from DOM nodes
+ if ((/(\[[\w-]*?:|:checked)/).test(e))
+ return false;
+
+ return true;
+ },
+
+ shouldUseSelectorsAPI: function() {
+ if (!Prototype.BrowserFeatures.SelectorsAPI) return false;
+
+ if (!Selector._div) Selector._div = new Element('div');
+
+ // Make sure the browser treats the selector as valid. Test on an
+ // isolated element to minimize cost of this check.
+ try {
+ Selector._div.querySelector(this.expression);
+ } catch(e) {
+ return false;
+ }
+
+ return true;
+ },
+
+ compileMatcher: function() {
+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+ c = Selector.criteria, le, p, m;
+
+ if (Selector._cache[e]) {
+ this.matcher = Selector._cache[e];
+ return;
+ }
+
+ this.matcher = ["this.matcher = function(root) {",
+ "var r = root, h = Selector.handlers, c = false, n;"];
+
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+ new Template(c[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.matcher.push("return h.unique(n);\n}");
+ eval(this.matcher.join('\n'));
+ Selector._cache[this.expression] = this.matcher;
+ },
+
+ compileXPathMatcher: function() {
+ var e = this.expression, ps = Selector.patterns,
+ x = Selector.xpath, le, m;
+
+ if (Selector._cache[e]) {
+ this.xpath = Selector._cache[e]; return;
+ }
+
+ this.matcher = ['.//*'];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ if (m = e.match(ps[i])) {
+ this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+ new Template(x[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+
+ this.xpath = this.matcher.join('');
+ Selector._cache[this.expression] = this.xpath;
+ },
+
+ findElements: function(root) {
+ root = root || document;
+ var e = this.expression, results;
+
+ switch (this.mode) {
+ case 'selectorsAPI':
+ // querySelectorAll queries document-wide, then filters to descendants
+ // of the context element. That's not what we want.
+ // Add an explicit context to the selector if necessary.
+ if (root !== document) {
+ var oldId = root.id, id = $(root).identify();
+ e = "#" + id + " " + e;
+ }
+
+ results = $A(root.querySelectorAll(e)).map(Element.extend);
+ root.id = oldId;
+
+ return results;
+ case 'xpath':
+ return document._getElementsByXPath(this.xpath, root);
+ default:
+ return this.matcher(root);
+ }
+ },
+
+ match: function(element) {
+ this.tokens = [];
+
+ var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+ var le, p, m;
+
+ while (e && le !== e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ // use the Selector.assertions methods unless the selector
+ // is too complex.
+ if (as[i]) {
+ this.tokens.push([i, Object.clone(m)]);
+ e = e.replace(m[0], '');
+ } else {
+ // reluctantly do a document-wide search
+ // and look for a match in the array
+ return this.findElements(document).include(element);
+ }
+ }
+ }
+ }
+
+ var match = true, name, matches;
+ for (var i = 0, token; token = this.tokens[i]; i++) {
+ name = token[0], matches = token[1];
+ if (!Selector.assertions[name](element, matches)) {
+ match = false; break;
+ }
+ }
+
+ return match;
+ },
+
+ toString: function() {
+ return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector:" + this.expression.inspect() + ">";
+ }
+});
+
+Object.extend(Selector, {
+ _cache: { },
+
+ xpath: {
+ descendant: "//*",
+ child: "/*",
+ adjacent: "/following-sibling::*[1]",
+ laterSibling: '/following-sibling::*',
+ tagName: function(m) {
+ if (m[1] == '*') return '';
+ return "[local-name()='" + m[1].toLowerCase() +
+ "' or local-name()='" + m[1].toUpperCase() + "']";
+ },
+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+ id: "[@id='#{1}']",
+ attrPresence: function(m) {
+ m[1] = m[1].toLowerCase();
+ return new Template("[@#{1}]").evaluate(m);
+ },
+ attr: function(m) {
+ m[1] = m[1].toLowerCase();
+ m[3] = m[5] || m[6];
+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+ },
+ pseudo: function(m) {
+ var h = Selector.xpath.pseudos[m[1]];
+ if (!h) return '';
+ if (Object.isFunction(h)) return h(m);
+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+ },
+ operators: {
+ '=': "[@#{1}='#{3}']",
+ '!=': "[@#{1}!='#{3}']",
+ '^=': "[starts-with(@#{1}, '#{3}')]",
+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+ '*=': "[contains(@#{1}, '#{3}')]",
+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+ },
+ pseudos: {
+ 'first-child': '[not(preceding-sibling::*)]',
+ 'last-child': '[not(following-sibling::*)]',
+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
+ 'empty': "[count(*) = 0 and (count(text()) = 0)]",
+ 'checked': "[@checked]",
+ 'disabled': "[(@disabled) and (@type!='hidden')]",
+ 'enabled': "[not(@disabled) and (@type!='hidden')]",
+ 'not': function(m) {
+ var e = m[6], p = Selector.patterns,
+ x = Selector.xpath, le, v;
+
+ var exclusion = [];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in p) {
+ if (m = e.match(p[i])) {
+ v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+ return "[not(" + exclusion.join(" and ") + ")]";
+ },
+ 'nth-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+ },
+ 'nth-last-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+ },
+ 'nth-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("position() ", m);
+ },
+ 'nth-last-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+ },
+ 'first-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+ },
+ 'last-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+ },
+ 'only-of-type': function(m) {
+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+ },
+ nth: function(fragment, m) {
+ var mm, formula = m[6], predicate;
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ if (mm = formula.match(/^(\d+)$/)) // digit only
+ return '[' + fragment + "= " + mm[1] + ']';
+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (mm[1] == "-") mm[1] = -1;
+ var a = mm[1] ? Number(mm[1]) : 1;
+ var b = mm[2] ? Number(mm[2]) : 0;
+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+ "((#{fragment} - #{b}) div #{a} >= 0)]";
+ return new Template(predicate).evaluate({
+ fragment: fragment, a: a, b: b });
+ }
+ }
+ }
+ },
+
+ criteria: {
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
+ attr: function(m) {
+ m[3] = (m[5] || m[6]);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
+ },
+ pseudo: function(m) {
+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+ },
+ descendant: 'c = "descendant";',
+ child: 'c = "child";',
+ adjacent: 'c = "adjacent";',
+ laterSibling: 'c = "laterSibling";'
+ },
+
+ patterns: {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo:
+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
+ attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+ },
+
+ // for Selector.match and Element#match
+ assertions: {
+ tagName: function(element, matches) {
+ return matches[1].toUpperCase() == element.tagName.toUpperCase();
+ },
+
+ className: function(element, matches) {
+ return Element.hasClassName(element, matches[1]);
+ },
+
+ id: function(element, matches) {
+ return element.id === matches[1];
+ },
+
+ attrPresence: function(element, matches) {
+ return Element.hasAttribute(element, matches[1]);
+ },
+
+ attr: function(element, matches) {
+ var nodeValue = Element.readAttribute(element, matches[1]);
+ return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
+ }
+ },
+
+ handlers: {
+ // UTILITY FUNCTIONS
+ // joins two collections
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ a.push(node);
+ return a;
+ },
+
+ // marks an array of nodes for counting
+ mark: function(nodes) {
+ var _true = Prototype.emptyFunction;
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._countedByPrototype = _true;
+ return nodes;
+ },
+
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._countedByPrototype = undefined;
+ return nodes;
+ },
+
+ // mark each child node with its position (for nth calls)
+ // "ofType" flag indicates whether we're indexing for nth-of-type
+ // rather than nth-child
+ index: function(parentNode, reverse, ofType) {
+ parentNode._countedByPrototype = Prototype.emptyFunction;
+ if (reverse) {
+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+ var node = nodes[i];
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+ }
+ } else {
+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+ if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
+ }
+ },
+
+ // filters out duplicates and extends all nodes
+ unique: function(nodes) {
+ if (nodes.length == 0) return nodes;
+ var results = [], n;
+ for (var i = 0, l = nodes.length; i < l; i++)
+ if (!(n = nodes[i])._countedByPrototype) {
+ n._countedByPrototype = Prototype.emptyFunction;
+ results.push(Element.extend(n));
+ }
+ return Selector.handlers.unmark(results);
+ },
+
+ // COMBINATOR FUNCTIONS
+ descendant: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName('*'));
+ return results;
+ },
+
+ child: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ for (var j = 0, child; child = node.childNodes[j]; j++)
+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+ }
+ return results;
+ },
+
+ adjacent: function(nodes) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ var next = this.nextElementSibling(node);
+ if (next) results.push(next);
+ }
+ return results;
+ },
+
+ laterSibling: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, Element.nextSiblings(node));
+ return results;
+ },
+
+ nextElementSibling: function(node) {
+ while (node = node.nextSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ previousElementSibling: function(node) {
+ while (node = node.previousSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ // TOKEN FUNCTIONS
+ tagName: function(nodes, root, tagName, combinator) {
+ var uTagName = tagName.toUpperCase();
+ var results = [], h = Selector.handlers;
+ if (nodes) {
+ if (combinator) {
+ // fastlane for ordinary descendant combinators
+ if (combinator == "descendant") {
+ for (var i = 0, node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName(tagName));
+ return results;
+ } else nodes = this[combinator](nodes);
+ if (tagName == "*") return nodes;
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName.toUpperCase() === uTagName) results.push(node);
+ return results;
+ } else return root.getElementsByTagName(tagName);
+ },
+
+ id: function(nodes, root, id, combinator) {
+ var targetNode = $(id), h = Selector.handlers;
+ if (!targetNode) return [];
+ if (!nodes && root == document) return [targetNode];
+ if (nodes) {
+ if (combinator) {
+ if (combinator == 'child') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (targetNode.parentNode == node) return [targetNode];
+ } else if (combinator == 'descendant') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.descendantOf(targetNode, node)) return [targetNode];
+ } else if (combinator == 'adjacent') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Selector.handlers.previousElementSibling(targetNode) == node)
+ return [targetNode];
+ } else nodes = h[combinator](nodes);
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node == targetNode) return [targetNode];
+ return [];
+ }
+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+ },
+
+ className: function(nodes, root, className, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ return Selector.handlers.byClassName(nodes, root, className);
+ },
+
+ byClassName: function(nodes, root, className) {
+ if (!nodes) nodes = Selector.handlers.descendant([root]);
+ var needle = ' ' + className + ' ';
+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+ nodeClassName = node.className;
+ if (nodeClassName.length == 0) continue;
+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+ results.push(node);
+ }
+ return results;
+ },
+
+ attrPresence: function(nodes, root, attr, combinator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ var results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.hasAttribute(node, attr)) results.push(node);
+ return results;
+ },
+
+ attr: function(nodes, root, attr, value, operator, combinator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ var handler = Selector.operators[operator], results = [];
+ for (var i = 0, node; node = nodes[i]; i++) {
+ var nodeValue = Element.readAttribute(node, attr);
+ if (nodeValue === null) continue;
+ if (handler(nodeValue, value)) results.push(node);
+ }
+ return results;
+ },
+
+ pseudo: function(nodes, name, value, root, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ return Selector.pseudos[name](nodes, value, root);
+ }
+ },
+
+ pseudos: {
+ 'first-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.previousElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'last-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.nextElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'only-child': function(nodes, value, root) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+ results.push(node);
+ return results;
+ },
+ 'nth-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root);
+ },
+ 'nth-last-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true);
+ },
+ 'nth-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, false, true);
+ },
+ 'nth-last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true, true);
+ },
+ 'first-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, false, true);
+ },
+ 'last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, true, true);
+ },
+ 'only-of-type': function(nodes, formula, root) {
+ var p = Selector.pseudos;
+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+ },
+
+ // handles the an+b logic
+ getIndices: function(a, b, total) {
+ if (a == 0) return b > 0 ? [b] : [];
+ return $R(1, total).inject([], function(memo, i) {
+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+ return memo;
+ });
+ },
+
+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+ nth: function(nodes, formula, root, reverse, ofType) {
+ if (nodes.length == 0) return [];
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ var h = Selector.handlers, results = [], indexed = [], m;
+ h.mark(nodes);
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (!node.parentNode._countedByPrototype) {
+ h.index(node.parentNode, reverse, ofType);
+ indexed.push(node.parentNode);
+ }
+ }
+ if (formula.match(/^\d+$/)) { // just a number
+ formula = Number(formula);
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.nodeIndex == formula) results.push(node);
+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (m[1] == "-") m[1] = -1;
+ var a = m[1] ? Number(m[1]) : 1;
+ var b = m[2] ? Number(m[2]) : 0;
+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+ for (var j = 0; j < l; j++)
+ if (node.nodeIndex == indices[j]) results.push(node);
+ }
+ }
+ h.unmark(nodes);
+ h.unmark(indexed);
+ return results;
+ },
+
+ 'empty': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ // IE treats comments as element nodes
+ if (node.tagName == '!' || node.firstChild) continue;
+ results.push(node);
+ }
+ return results;
+ },
+
+ 'not': function(nodes, selector, root) {
+ var h = Selector.handlers, selectorType, m;
+ var exclusions = new Selector(selector).findElements(root);
+ h.mark(exclusions);
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node._countedByPrototype) results.push(node);
+ h.unmark(exclusions);
+ return results;
+ },
+
+ 'enabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node.disabled && (!node.type || node.type !== 'hidden'))
+ results.push(node);
+ return results;
+ },
+
+ 'disabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.disabled) results.push(node);
+ return results;
+ },
+
+ 'checked': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.checked) results.push(node);
+ return results;
+ }
+ },
+
+ operators: {
+ '=': function(nv, v) { return nv == v; },
+ '!=': function(nv, v) { return nv != v; },
+ '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
+ '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
+ '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
+ '$=': function(nv, v) { return nv.endsWith(v); },
+ '*=': function(nv, v) { return nv.include(v); },
+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+ '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
+ '-').include('-' + (v || "").toUpperCase() + '-'); }
+ },
+
+ split: function(expression) {
+ var expressions = [];
+ expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ return expressions;
+ },
+
+ matchElements: function(elements, expression) {
+ var matches = $$(expression), h = Selector.handlers;
+ h.mark(matches);
+ for (var i = 0, results = [], element; element = elements[i]; i++)
+ if (element._countedByPrototype) results.push(element);
+ h.unmark(matches);
+ return results;
+ },
+
+ findElement: function(elements, expression, index) {
+ if (Object.isNumber(expression)) {
+ index = expression; expression = false;
+ }
+ return Selector.matchElements(elements, expression || '*')[index || 0];
+ },
+
+ findChildElements: function(element, expressions) {
+ expressions = Selector.split(expressions.join(','));
+ var results = [], h = Selector.handlers;
+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
+ selector = new Selector(expressions[i].strip());
+ h.concat(results, selector.findElements(element));
+ }
+ return (l > 1) ? h.unique(results) : results;
+ }
+});
+
+if (Prototype.Browser.IE) {
+ Object.extend(Selector.handlers, {
+ // IE returns comment nodes on getElementsByTagName("*").
+ // Filter them out.
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ if (node.tagName !== "!") a.push(node);
+ return a;
+ },
+
+ // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node.removeAttribute('_countedByPrototype');
+ return nodes;
+ }
+ });
+}
+
+function $$() {
+ return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+ reset: function(form) {
+ $(form).reset();
+ return form;
+ },
+
+ serializeElements: function(elements, options) {
+ if (typeof options != 'object') options = { hash: !!options };
+ else if (Object.isUndefined(options.hash)) options.hash = true;
+ var key, value, submitted = false, submit = options.submit;
+
+ var data = elements.inject({ }, function(result, element) {
+ if (!element.disabled && element.name) {
+ key = element.name; value = $(element).getValue();
+ if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
+ submit !== false && (!submit || key == submit) && (submitted = true)))) {
+ if (key in result) {
+ // a key is already present; construct an array of values
+ if (!Object.isArray(result[key])) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return options.hash ? data : Object.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, options) {
+ return Form.serializeElements(Form.getElements(form), options);
+ },
+
+ getElements: function(form) {
+ return $A($(form).getElementsByTagName('*')).inject([],
+ function(elements, child) {
+ if (Form.Element.Serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ }
+ );
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('disable');
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('enable');
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ var elements = $(form).getElements().findAll(function(element) {
+ return 'hidden' != element.type && !element.disabled;
+ });
+ var firstByIndex = elements.findAll(function(element) {
+ return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+ }).sortBy(function(element) { return element.tabIndex }).first();
+
+ return firstByIndex ? firstByIndex : elements.find(function(element) {
+ return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || { });
+
+ var params = options.parameters, action = form.readAttribute('action') || '';
+ if (action.blank()) action = window.location.href;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (Object.isString(params)) params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(action, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+};
+
+Form.Element.Methods = {
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = { };
+ pair[element.name] = value;
+ return Object.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ setValue: function(element, value) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ Form.Element.Serializers[method](element, value);
+ return element;
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type)))
+ element.select();
+ } catch (e) { }
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.disabled = false;
+ return element;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element, value) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element, value);
+ default:
+ return Form.Element.Serializers.textarea(element, value);
+ }
+ },
+
+ inputSelector: function(element, value) {
+ if (Object.isUndefined(value)) return element.checked ? element.value : null;
+ else element.checked = !!value;
+ },
+
+ textarea: function(element, value) {
+ if (Object.isUndefined(value)) return element.value;
+ else element.value = value;
+ },
+
+ select: function(element, value) {
+ if (Object.isUndefined(value))
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ else {
+ var opt, currentValue, single = !Object.isArray(value);
+ for (var i = 0, length = element.length; i < length; i++) {
+ opt = element.options[i];
+ currentValue = this.optionValue(opt);
+ if (single) {
+ if (currentValue == value) {
+ opt.selected = true;
+ return;
+ }
+ }
+ else opt.selected = value.include(currentValue);
+ }
+ }
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ // extend element because hasAttribute may not be native
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+ initialize: function($super, element, frequency, callback) {
+ $super(callback, frequency);
+ this.element = $(element);
+ this.lastValue = this.getValue();
+ },
+
+ execute: function() {
+ var value = this.getValue();
+ if (Object.isString(this.lastValue) && Object.isString(value) ?
+ this.lastValue != value : String(this.lastValue) != String(value)) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+ initialize: function(element, callback) {
+ this.element = $(element);
+ this.callback = callback;
+
+ this.lastValue = this.getValue();
+ if (this.element.tagName.toLowerCase() == 'form')
+ this.registerFormCallbacks();
+ else
+ this.registerCallback(this.element);
+ },
+
+ onElementEvent: function() {
+ var value = this.getValue();
+ if (this.lastValue != value) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ },
+
+ registerFormCallbacks: function() {
+ Form.getElements(this.element).each(this.registerCallback, this);
+ },
+
+ registerCallback: function(element) {
+ if (element.type) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ Event.observe(element, 'click', this.onElementEvent.bind(this));
+ break;
+ default:
+ Event.observe(element, 'change', this.onElementEvent.bind(this));
+ break;
+ }
+ }
+ }
+});
+
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+if (!window.Event) var Event = { };
+
+Object.extend(Event, {
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+ KEY_INSERT: 45,
+
+ cache: { },
+
+ relatedTarget: function(event) {
+ var element;
+ switch(event.type) {
+ case 'mouseover': element = event.fromElement; break;
+ case 'mouseout': element = event.toElement; break;
+ default: return null;
+ }
+ return Element.extend(element);
+ }
+});
+
+Event.Methods = (function() {
+ var isButton;
+
+ if (Prototype.Browser.IE) {
+ var buttonMap = { 0: 1, 1: 4, 2: 2 };
+ isButton = function(event, code) {
+ return event.button == buttonMap[code];
+ };
+
+ } else if (Prototype.Browser.WebKit) {
+ isButton = function(event, code) {
+ switch (code) {
+ case 0: return event.which == 1 && !event.metaKey;
+ case 1: return event.which == 1 && event.metaKey;
+ default: return false;
+ }
+ };
+
+ } else {
+ isButton = function(event, code) {
+ return event.which ? (event.which === code + 1) : (event.button === code);
+ };
+ }
+
+ return {
+ isLeftClick: function(event) { return isButton(event, 0) },
+ isMiddleClick: function(event) { return isButton(event, 1) },
+ isRightClick: function(event) { return isButton(event, 2) },
+
+ element: function(event) {
+ event = Event.extend(event);
+
+ var node = event.target,
+ type = event.type,
+ currentTarget = event.currentTarget;
+
+ if (currentTarget && currentTarget.tagName) {
+ // Firefox screws up the "click" event when moving between radio buttons
+ // via arrow keys. It also screws up the "load" and "error" events on images,
+ // reporting the document as the target instead of the original image.
+ if (type === 'load' || type === 'error' ||
+ (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
+ && currentTarget.type === 'radio'))
+ node = currentTarget;
+ }
+ if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
+ return Element.extend(node);
+ },
+
+ findElement: function(event, expression) {
+ var element = Event.element(event);
+ if (!expression) return element;
+ var elements = [element].concat(element.ancestors());
+ return Selector.findElement(elements, expression, 0);
+ },
+
+ pointer: function(event) {
+ var docElement = document.documentElement,
+ body = document.body || { scrollLeft: 0, scrollTop: 0 };
+ return {
+ x: event.pageX || (event.clientX +
+ (docElement.scrollLeft || body.scrollLeft) -
+ (docElement.clientLeft || 0)),
+ y: event.pageY || (event.clientY +
+ (docElement.scrollTop || body.scrollTop) -
+ (docElement.clientTop || 0))
+ };
+ },
+
+ pointerX: function(event) { return Event.pointer(event).x },
+ pointerY: function(event) { return Event.pointer(event).y },
+
+ stop: function(event) {
+ Event.extend(event);
+ event.preventDefault();
+ event.stopPropagation();
+ event.stopped = true;
+ }
+ };
+})();
+
+Event.extend = (function() {
+ var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+ m[name] = Event.Methods[name].methodize();
+ return m;
+ });
+
+ if (Prototype.Browser.IE) {
+ Object.extend(methods, {
+ stopPropagation: function() { this.cancelBubble = true },
+ preventDefault: function() { this.returnValue = false },
+ inspect: function() { return "[object Event]" }
+ });
+
+ return function(event) {
+ if (!event) return false;
+ if (event._extendedByPrototype) return event;
+
+ event._extendedByPrototype = Prototype.emptyFunction;
+ var pointer = Event.pointer(event);
+ Object.extend(event, {
+ target: event.srcElement,
+ relatedTarget: Event.relatedTarget(event),
+ pageX: pointer.x,
+ pageY: pointer.y
+ });
+ return Object.extend(event, methods);
+ };
+
+ } else {
+ Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
+ Object.extend(Event.prototype, methods);
+ return Prototype.K;
+ }
+})();
+
+Object.extend(Event, (function() {
+ var cache = Event.cache;
+
+ function getEventID(element) {
+ if (element._prototypeEventID) return element._prototypeEventID[0];
+ arguments.callee.id = arguments.callee.id || 1;
+ return element._prototypeEventID = [++arguments.callee.id];
+ }
+
+ function getDOMEventName(eventName) {
+ if (eventName && eventName.include(':')) return "dataavailable";
+ return eventName;
+ }
+
+ function getCacheForID(id) {
+ return cache[id] = cache[id] || { };
+ }
+
+ function getWrappersForEventName(id, eventName) {
+ var c = getCacheForID(id);
+ return c[eventName] = c[eventName] || [];
+ }
+
+ function createWrapper(element, eventName, handler) {
+ var id = getEventID(element);
+ var c = getWrappersForEventName(id, eventName);
+ if (c.pluck("handler").include(handler)) return false;
+
+ var wrapper = function(event) {
+ if (!Event || !Event.extend ||
+ (event.eventName && event.eventName != eventName))
+ return false;
+
+ Event.extend(event);
+ handler.call(element, event);
+ };
+
+ wrapper.handler = handler;
+ c.push(wrapper);
+ return wrapper;
+ }
+
+ function findWrapper(id, eventName, handler) {
+ var c = getWrappersForEventName(id, eventName);
+ return c.find(function(wrapper) { return wrapper.handler == handler });
+ }
+
+ function destroyWrapper(id, eventName, handler) {
+ var c = getCacheForID(id);
+ if (!c[eventName]) return false;
+ c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+ }
+
+ function destroyCache() {
+ for (var id in cache)
+ for (var eventName in cache[id])
+ cache[id][eventName] = null;
+ }
+
+
+ // Internet Explorer needs to remove event handlers on page unload
+ // in order to avoid memory leaks.
+ if (window.attachEvent) {
+ window.attachEvent("onunload", destroyCache);
+ }
+
+ // Safari has a dummy event handler on page unload so that it won't
+ // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
+ // object when page is returned to via the back button using its bfcache.
+ if (Prototype.Browser.WebKit) {
+ window.addEventListener('unload', Prototype.emptyFunction, false);
+ }
+
+ return {
+ observe: function(element, eventName, handler) {
+ element = $(element);
+ var name = getDOMEventName(eventName);
+
+ var wrapper = createWrapper(element, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.addEventListener) {
+ element.addEventListener(name, wrapper, false);
+ } else {
+ element.attachEvent("on" + name, wrapper);
+ }
+
+ return element;
+ },
+
+ stopObserving: function(element, eventName, handler) {
+ element = $(element);
+ var id = getEventID(element), name = getDOMEventName(eventName);
+
+ if (!handler && eventName) {
+ getWrappersForEventName(id, eventName).each(function(wrapper) {
+ element.stopObserving(eventName, wrapper.handler);
+ });
+ return element;
+
+ } else if (!eventName) {
+ Object.keys(getCacheForID(id)).each(function(eventName) {
+ element.stopObserving(eventName);
+ });
+ return element;
+ }
+
+ var wrapper = findWrapper(id, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, wrapper, false);
+ } else {
+ element.detachEvent("on" + name, wrapper);
+ }
+
+ destroyWrapper(id, eventName, handler);
+
+ return element;
+ },
+
+ fire: function(element, eventName, memo) {
+ element = $(element);
+ if (element == document && document.createEvent && !element.dispatchEvent)
+ element = document.documentElement;
+
+ var event;
+ if (document.createEvent) {
+ event = document.createEvent("HTMLEvents");
+ event.initEvent("dataavailable", true, true);
+ } else {
+ event = document.createEventObject();
+ event.eventType = "ondataavailable";
+ }
+
+ event.eventName = eventName;
+ event.memo = memo || { };
+
+ if (document.createEvent) {
+ element.dispatchEvent(event);
+ } else {
+ element.fireEvent(event.eventType, event);
+ }
+
+ return Event.extend(event);
+ }
+ };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+ fire: Event.fire,
+ observe: Event.observe,
+ stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+ fire: Element.Methods.fire.methodize(),
+ observe: Element.Methods.observe.methodize(),
+ stopObserving: Element.Methods.stopObserving.methodize(),
+ loaded: false
+});
+
+(function() {
+ /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+ Matthias Miller, Dean Edwards and John Resig. */
+
+ var timer;
+
+ function fireContentLoadedEvent() {
+ if (document.loaded) return;
+ if (timer) window.clearInterval(timer);
+ document.fire("dom:loaded");
+ document.loaded = true;
+ }
+
+ if (document.addEventListener) {
+ if (Prototype.Browser.WebKit) {
+ timer = window.setInterval(function() {
+ if (/loaded|complete/.test(document.readyState))
+ fireContentLoadedEvent();
+ }, 0);
+
+ Event.observe(window, "load", fireContentLoadedEvent);
+
+ } else {
+ document.addEventListener("DOMContentLoaded",
+ fireContentLoadedEvent, false);
+ }
+
+ } else {
+ document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+ $("__onDOMContentLoaded").onreadystatechange = function() {
+ if (this.readyState == "complete") {
+ this.onreadystatechange = null;
+ fireContentLoadedEvent();
+ }
+ };
+ }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+ Before: function(element, content) {
+ return Element.insert(element, {before:content});
+ },
+
+ Top: function(element, content) {
+ return Element.insert(element, {top:content});
+ },
+
+ Bottom: function(element, content) {
+ return Element.insert(element, {bottom:content});
+ },
+
+ After: function(element, content) {
+ return Element.insert(element, {after:content});
+ }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
+var Position = {
+ // set to true if needed, warning: firefox performance problems
+ // NOT neeeded for page scrolling, only if draggable contained in
+ // scrollable elements
+ includeScrollOffsets: false,
+
+ // must be called before calling withinIncludingScrolloffset, every time the
+ // page is scrolled
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ // caches x/y coordinate pair to use with overlap
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = Element.cumulativeScrollOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ // within must be called directly before
+ overlap: function(mode, element) {
+ if (!mode) return 0;
+ if (mode == 'vertical')
+ return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+ element.offsetHeight;
+ if (mode == 'horizontal')
+ return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+ element.offsetWidth;
+ },
+
+ // Deprecation layer -- use newer Element methods now (1.5.2).
+
+ cumulativeOffset: Element.Methods.cumulativeOffset,
+
+ positionedOffset: Element.Methods.positionedOffset,
+
+ absolutize: function(element) {
+ Position.prepare();
+ return Element.absolutize(element);
+ },
+
+ relativize: function(element) {
+ Position.prepare();
+ return Element.relativize(element);
+ },
+
+ realOffset: Element.Methods.cumulativeScrollOffset,
+
+ offsetParent: Element.Methods.getOffsetParent,
+
+ page: Element.Methods.viewportOffset,
+
+ clone: function(source, target, options) {
+ options = options || { };
+ return Element.clonePosition(target, source, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+ function iter(name) {
+ return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+ }
+
+ instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+ function(element, className) {
+ className = className.toString().strip();
+ var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+ return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+ } : function(element, className) {
+ className = className.toString().strip();
+ var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+ if (!classNames && !className) return elements;
+
+ var nodes = $(element).getElementsByTagName('*');
+ className = ' ' + className + ' ';
+
+ for (var i = 0, child, cn; child = nodes[i]; i++) {
+ if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+ (classNames && classNames.all(function(name) {
+ return !name.toString().blank() && cn.include(' ' + name + ' ');
+ }))))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+ };
+
+ return function(className, parentElement) {
+ return $(parentElement || document.body).getElementsByClassName(className);
+ };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+Element.addMethods(); \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/scripts.js b/subsonic-main/src/main/webapp/script/scripts.js
new file mode 100644
index 00000000..959ed7a6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/scripts.js
@@ -0,0 +1,21 @@
+function noop() {
+}
+
+function popup(mylink, windowname) {
+ return popupSize(mylink, windowname, 400, 200);
+}
+
+function popupSize(mylink, windowname, width, height) {
+ var href;
+ if (typeof(mylink) == "string") {
+ href = mylink;
+ } else {
+ href = mylink.href;
+ }
+
+ var w = window.open(href, windowname, "width=" + width + ",height=" + height + ",scrollbars=yes,resizable=yes");
+ w.focus();
+ w.moveTo(300, 200);
+ return false;
+}
+
diff --git a/subsonic-main/src/main/webapp/script/smooth-scroll.js b/subsonic-main/src/main/webapp/script/smooth-scroll.js
new file mode 100644
index 00000000..9199732b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/smooth-scroll.js
@@ -0,0 +1,102 @@
+/*--------------------------------------------------------------------------
+ * Smooth Scroller Script, version 1.0.1
+ * (c) 2007 Dezinerfolio Inc. <midart@gmail.com>
+ *
+ * For details, please check the website : http://dezinerfolio.com/
+ *
+/*--------------------------------------------------------------------------*/
+
+Scroller = {
+ // control the speed of the scroller.
+ // dont change it here directly, please use Scroller.speed=50;
+ speed:10,
+
+ // returns the Y position of the div
+ gy: function (d) {
+ gy = d.offsetTop
+ if (d.offsetParent) while (d = d.offsetParent) gy += d.offsetTop
+ return gy
+ },
+
+ // returns the current scroll position
+ scrollTop: function (){
+ body=document.body
+ d=document.documentElement
+ if (body && body.scrollTop) return body.scrollTop
+ if (d && d.scrollTop) return d.scrollTop
+ if (window.pageYOffset) return window.pageYOffset
+ return 0
+ },
+
+ // attach an event for an element
+ // (element, type, function)
+ add: function(event, body, d) {
+ if (event.addEventListener) return event.addEventListener(body, d,false)
+ if (event.attachEvent) return event.attachEvent('on'+body, d)
+ },
+
+ // kill an event of an element
+ end: function(e){
+ if (window.event) {
+ window.event.cancelBubble = true
+ window.event.returnValue = false
+ return;
+ }
+ if (e.preventDefault && e.stopPropagation) {
+ e.preventDefault()
+ e.stopPropagation()
+ }
+ },
+
+ // move the scroll bar to the particular div.
+ scroll: function(d){
+ i = window.innerHeight || document.documentElement.clientHeight;
+ h=document.body.scrollHeight;
+ a = Scroller.scrollTop()
+ if(d>a)
+ if(h-d>i)
+ a+=Math.ceil((d-a)/Scroller.speed)
+ else
+ a+=Math.ceil((d-a-(h-d))/Scroller.speed)
+ else
+ a = a+(d-a)/Scroller.speed;
+ window.scrollTo(0,a)
+ if(a==d || Scroller.offsetTop==a)clearInterval(Scroller.interval)
+ Scroller.offsetTop=a
+ },
+ // initializer that adds the renderer to the onload function of the window
+ init: function(){
+ Scroller.add(window,'load', Scroller.render)
+ },
+
+ // this method extracts all the anchors and validates then as # and attaches the events.
+ render: function(){
+ a = document.getElementsByTagName('a');
+ Scroller.end(this);
+ window.onscroll
+ for (i=0;i<a.length;i++) {
+ l = a[i];
+ if(l.href && l.href.indexOf('#') != -1 && ((l.pathname==location.pathname) || ('/'+l.pathname==location.pathname)) ){
+ Scroller.add(l,'click',Scroller.end)
+ l.onclick = function(){
+ Scroller.end(this);
+ l=this.hash.substr(1);
+ a = document.getElementsByTagName('a');
+ for (i=0;i<a.length;i++) {
+ if(a[i].name == l){
+ clearInterval(Scroller.interval);
+ Scroller.interval=setInterval('Scroller.scroll('+Scroller.gy(a[i])+')',10);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+// invoke the initializer of the scroller
+Scroller.init();
+
+
+/*------------------------------------------------------------
+ * END OF CODE
+/*-----------------------------------------------------------*/ \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/swfobject.js b/subsonic-main/src/main/webapp/script/swfobject.js
new file mode 100644
index 00000000..8eafe9dd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/swfobject.js
@@ -0,0 +1,4 @@
+/* SWFObject v2.2 <http://code.google.com/p/swfobject/>
+ is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
+*/
+var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y<X;Y++){U[Y]()}}function K(X){if(J){X()}else{U[U.length]=X}}function s(Y){if(typeof O.addEventListener!=D){O.addEventListener("load",Y,false)}else{if(typeof j.addEventListener!=D){j.addEventListener("load",Y,false)}else{if(typeof O.attachEvent!=D){i(O,"onload",Y)}else{if(typeof O.onload=="function"){var X=O.onload;O.onload=function(){X();Y()}}else{O.onload=Y}}}}}function h(){if(T){V()}else{H()}}function V(){var X=j.getElementsByTagName("body")[0];var aa=C(r);aa.setAttribute("type",q);var Z=X.appendChild(aa);if(Z){var Y=0;(function(){if(typeof Z.GetVariable!=D){var ab=Z.GetVariable("$version");if(ab){ab=ab.split(" ")[1].split(",");M.pv=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}else{if(Y<10){Y++;setTimeout(arguments.callee,10);return}}X.removeChild(aa);Z=null;H()})()}else{H()}}function H(){var ag=o.length;if(ag>0){for(var af=0;af<ag;af++){var Y=o[af].id;var ab=o[af].callbackFn;var aa={success:false,id:Y};if(M.pv[0]>0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad<ac;ad++){if(X[ad].getAttribute("name").toLowerCase()!="movie"){ah[X[ad].getAttribute("name")]=X[ad].getAttribute("value")}}P(ai,ah,Y,ab)}else{p(ae);if(ab){ab(aa)}}}}}else{w(Y,true);if(ab){var Z=z(Y);if(Z&&typeof Z.SetVariable!=D){aa.success=true;aa.ref=Z}ab(aa)}}}}}function z(aa){var X=null;var Y=c(aa);if(Y&&Y.nodeName=="OBJECT"){if(typeof Y.SetVariable!=D){X=Y}else{var Z=Y.getElementsByTagName(r)[0];if(Z){X=Z}}}return X}function A(){return !a&&F("6.0.65")&&(M.win||M.mac)&&!(M.wk&&M.wk<312)}function P(aa,ab,X,Z){a=true;E=Z||null;B={success:false,id:X};var ae=c(X);if(ae){if(ae.nodeName=="OBJECT"){l=g(ae);Q=null}else{l=ae;Q=X}aa.id=R;if(typeof aa.width==D||(!/%$/.test(aa.width)&&parseInt(aa.width,10)<310)){aa.width="310"}if(typeof aa.height==D||(!/%$/.test(aa.height)&&parseInt(aa.height,10)<137)){aa.height="137"}j.title=j.title.slice(0,47)+" - Flash Player Installation";var ad=M.ie&&M.win?"ActiveX":"PlugIn",ac="MMredirectURL="+O.location.toString().replace(/&/g,"%26")+"&MMplayerType="+ad+"&MMdoctitle="+j.title;if(typeof ab.flashvars!=D){ab.flashvars+="&"+ac}else{ab.flashvars=ac}if(M.ie&&M.win&&ae.readyState!=4){var Y=C("div");X+="SWFObjectNew";Y.setAttribute("id",X);ae.parentNode.insertBefore(Y,ae);ae.style.display="none";(function(){if(ae.readyState==4){ae.parentNode.removeChild(ae)}else{setTimeout(arguments.callee,10)}})()}u(aa,ab,X)}}function p(Y){if(M.ie&&M.win&&Y.readyState!=4){var X=C("div");Y.parentNode.insertBefore(X,Y);X.parentNode.replaceChild(g(Y),X);Y.style.display="none";(function(){if(Y.readyState==4){Y.parentNode.removeChild(Y)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.replaceChild(g(Y),Y)}}function g(ab){var aa=C("div");if(M.win&&M.ie){aa.innerHTML=ab.innerHTML}else{var Y=ab.getElementsByTagName(r)[0];if(Y){var ad=Y.childNodes;if(ad){var X=ad.length;for(var Z=0;Z<X;Z++){if(!(ad[Z].nodeType==1&&ad[Z].nodeName=="PARAM")&&!(ad[Z].nodeType==8)){aa.appendChild(ad[Z].cloneNode(true))}}}}}return aa}function u(ai,ag,Y){var X,aa=c(Y);if(M.wk&&M.wk<312){return X}if(aa){if(typeof ai.id==D){ai.id=Y}if(M.ie&&M.win){var ah="";for(var ae in ai){if(ai[ae]!=Object.prototype[ae]){if(ae.toLowerCase()=="data"){ag.movie=ai[ae]}else{if(ae.toLowerCase()=="styleclass"){ah+=' class="'+ai[ae]+'"'}else{if(ae.toLowerCase()!="classid"){ah+=" "+ae+'="'+ai[ae]+'"'}}}}}var af="";for(var ad in ag){if(ag[ad]!=Object.prototype[ad]){af+='<param name="'+ad+'" value="'+ag[ad]+'" />'}}aa.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+ah+">"+af+"</object>";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab<ac;ab++){I[ab][0].detachEvent(I[ab][1],I[ab][2])}var Z=N.length;for(var aa=0;aa<Z;aa++){y(N[aa])}for(var Y in M){M[Y]=null}M=null;for(var X in swfobject){swfobject[X]=null}swfobject=null})}}();return{registerObject:function(ab,X,aa,Z){if(M.w3&&ab&&X){var Y={};Y.id=ab;Y.swfVersion=X;Y.expressInstall=aa;Y.callbackFn=Z;o[o.length]=Y;w(ab,false)}else{if(Z){Z({success:false,id:ab})}}},getObjectById:function(X){if(M.w3){return z(X)}},embedSWF:function(ab,ah,ae,ag,Y,aa,Z,ad,af,ac){var X={success:false,id:ah};if(M.w3&&!(M.wk&&M.wk<312)&&ab&&ah&&ae&&ag&&Y){w(ah,false);K(function(){ae+="";ag+="";var aj={};if(af&&typeof af===r){for(var al in af){aj[al]=af[al]}}aj.data=ab;aj.width=ae;aj.height=ag;var am={};if(ad&&typeof ad===r){for(var ak in ad){am[ak]=ad[ak]}}if(Z&&typeof Z===r){for(var ai in Z){if(typeof am.flashvars!=D){am.flashvars+="&"+ai+"="+Z[ai]}else{am.flashvars=ai+"="+Z[ai]}}}if(F(Y)){var an=u(aj,am,ah);if(aj.id==ah){w(ah,true)}X.success=true;X.ref=an}else{if(aa&&A()){aj.data=aa;P(aj,am,ah,ac);return}else{w(ah,true)}}if(ac){ac(X)}})}else{if(ac){ac(X)}}},switchOffAutoHideShow:function(){m=false},ua:M,getFlashPlayerVersion:function(){return{major:M.pv[0],minor:M.pv[1],release:M.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(Z,Y,X){if(M.w3){return u(Z,Y,X)}else{return undefined}},showExpressInstall:function(Z,aa,X,Y){if(M.w3&&A()){P(Z,aa,X,Y)}},removeSWF:function(X){if(M.w3){y(X)}},createCSS:function(aa,Z,Y,X){if(M.w3){v(aa,Z,Y,X)}},addDomLoadEvent:K,addLoadEvent:s,getQueryParamValue:function(aa){var Z=j.location.search||j.location.hash;if(Z){if(/\?/.test(Z)){Z=Z.split("?")[1]}if(aa==null){return L(Z)}var Y=Z.split("&");for(var X=0;X<Y.length;X++){if(Y[X].substring(0,Y[X].indexOf("="))==aa){return L(Y[X].substring((Y[X].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(a){var X=c(R);if(X&&l){X.parentNode.replaceChild(l,X);if(Q){w(Q,true);if(M.ie&&M.win){l.style.display="block"}}if(E){E(B)}}a=false}}}}(); \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon.js b/subsonic-main/src/main/webapp/script/tip_balloon.js
new file mode 100644
index 00000000..dc9d2e44
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon.js
@@ -0,0 +1,221 @@
+/*
+tip_balloon.js v. 1.81
+
+The latest version is available at
+http://www.walterzorn.com
+or http://www.devira.com
+or http://www.walterzorn.de
+
+Initial author: Walter Zorn
+Last modified: 2.2.2009
+
+Extension for the tooltip library wz_tooltip.js.
+Implements balloon tooltips.
+*/
+
+// Make sure that the core file wz_tooltip.js is included first
+if(typeof config == "undefined")
+ alert("Error:\nThe core tooltip script file 'wz_tooltip.js' must be included first, before the plugin files!");
+
+// Here we define new global configuration variable(s) (as members of the
+// predefined "config." class).
+// From each of these config variables, wz_tooltip.js will automatically derive
+// a command which can be passed to Tip() or TagToTip() in order to customize
+// tooltips individually. These command names are just the config variable
+// name(s) translated to uppercase,
+// e.g. from config. Balloon a command BALLOON will automatically be
+// created.
+
+//=================== GLOBAL TOOLTIP CONFIGURATION =========================//
+config. Balloon = false // true or false - set to true if you want this to be the default behaviour
+config. BalloonImgPath = "script/tip_balloon/" // Path to images (border, corners, stem), in quotes. Path must be relative to your HTML file.
+// Sizes of balloon images
+config. BalloonEdgeSize = 6 // Integer - sidelength of quadratic corner images
+config. BalloonStemWidth = 15 // Integer
+config. BalloonStemHeight = 19 // Integer
+config. BalloonStemOffset = -7 // Integer - horizontal offset of left stem edge from mouse (recommended: -stemwidth/2 to center the stem above the mouse)
+config. BalloonImgExt = "gif";// File name extension of default balloon images, e.g. "gif" or "png"
+//======= END OF TOOLTIP CONFIG, DO NOT CHANGE ANYTHING BELOW ==============//
+
+
+// Create a new tt_Extension object (make sure that the name of that object,
+// here balloon, is unique amongst the extensions available for wz_tooltips.js):
+var balloon = new tt_Extension();
+
+// Implement extension eventhandlers on which our extension should react
+
+balloon.OnLoadConfig = function()
+{
+ if(tt_aV[BALLOON])
+ {
+ // Turn off native style properties which are not appropriate
+ balloon.padding = Math.max(tt_aV[PADDING] - tt_aV[BALLOONEDGESIZE], 0);
+ balloon.width = tt_aV[WIDTH];
+ //if(tt_bBoxOld)
+ // balloon.width += (balloon.padding << 1);
+ tt_aV[BORDERWIDTH] = 0;
+ tt_aV[WIDTH] = 0;
+ tt_aV[PADDING] = 0;
+ tt_aV[BGCOLOR] = "";
+ tt_aV[BGIMG] = "";
+ tt_aV[SHADOW] = false;
+ // Append slash to img path if missing
+ if(tt_aV[BALLOONIMGPATH].charAt(tt_aV[BALLOONIMGPATH].length - 1) != '/')
+ tt_aV[BALLOONIMGPATH] += "/";
+ return true;
+ }
+ return false;
+};
+balloon.OnCreateContentString = function()
+{
+ if(!tt_aV[BALLOON])
+ return false;
+
+ var aImg, sImgZ, sCssCrn, sVaT, sVaB, sCss0;
+
+ // Cache balloon images in advance:
+ // Either use the pre-cached default images...
+ if(tt_aV[BALLOONIMGPATH] == config.BalloonImgPath)
+ aImg = balloon.aDefImg;
+ // ...or load images from different directory
+ else
+ aImg = Balloon_CacheImgs(tt_aV[BALLOONIMGPATH], tt_aV[BALLOONIMGEXT]);
+ sCss0 = 'padding:0;margin:0;border:0;line-height:0;overflow:hidden;';
+ sCssCrn = ' style="position:relative;width:' + tt_aV[BALLOONEDGESIZE] + 'px;' + sCss0 + 'overflow:hidden;';
+ sVaT = 'vertical-align:top;" valign="top"';
+ sVaB = 'vertical-align:bottom;" valign="bottom"';
+ sImgZ = '" style="' + sCss0 + '" />';
+
+ tt_sContent = '<table border="0" cellpadding="0" cellspacing="0" style="width:auto;padding:0;margin:0;left:0;top:0;"><tr>'
+ // Left-top corner
+ + '<td' + sCssCrn + sVaB + '>'
+ + '<img src="' + aImg[1].src + '" width="' + tt_aV[BALLOONEDGESIZE] + '" height="' + tt_aV[BALLOONEDGESIZE] + sImgZ
+ + '</td>'
+ // Top border
+ + '<td valign="bottom" style="position:relative;' + sCss0 + '">'
+ + '<img id="bALlOOnT" style="position:relative;top:1px;z-index:1;display:none;' + sCss0 + '" src="' + aImg[9].src + '" width="' + tt_aV[BALLOONSTEMWIDTH] + '" height="' + tt_aV[BALLOONSTEMHEIGHT] + '" />'
+ + '<div style="position:relative;z-index:0;top:0;' + sCss0 + 'width:auto;height:' + tt_aV[BALLOONEDGESIZE] + 'px;background-image:url(' + aImg[2].src + ');">'
+ + '</div>'
+ + '</td>'
+ // Right-top corner
+ + '<td' + sCssCrn + sVaB + '>'
+ + '<img src="' + aImg[3].src + '" width="' + tt_aV[BALLOONEDGESIZE] + '" height="' + tt_aV[BALLOONEDGESIZE] + sImgZ
+ + '</td>'
+ + '</tr><tr>'
+ // Left border (background-repeat fix courtesy Dirk Schnitzler)
+ + '<td style="position:relative;background-repeat:repeat;' + sCss0 + 'width:' + tt_aV[BALLOONEDGESIZE] + 'px;background-image:url(' + aImg[8].src + ');">'
+ // Redundant image for bugous old Geckos which won't auto-expand TD height to 100%
+ + '<img width="' + tt_aV[BALLOONEDGESIZE] + '" height="100%" src="' + aImg[8].src + sImgZ
+ + '</td>'
+ // Content
+ + '<td id="bALlO0nBdY" style="position:relative;line-height:normal;background-repeat:repeat;'
+ + ';background-image:url(' + aImg[0].src + ')'
+ + ';color:' + tt_aV[FONTCOLOR]
+ + ';font-family:' + tt_aV[FONTFACE]
+ + ';font-size:' + tt_aV[FONTSIZE]
+ + ';font-weight:' + tt_aV[FONTWEIGHT]
+ + ';text-align:' + tt_aV[TEXTALIGN]
+ + ';padding:' + balloon.padding + 'px'
+ + ';width:' + ((balloon.width > 0) ? (balloon.width + 'px') : 'auto')
+ + ';">' + tt_sContent + '</td>'
+ // Right border
+ + '<td style="position:relative;background-repeat:repeat;' + sCss0 + 'width:' + tt_aV[BALLOONEDGESIZE] + 'px;background-image:url(' + aImg[4].src + ');">'
+ // Image redundancy for bugous old Geckos that won't auto-expand TD height to 100%
+ + '<img width="' + tt_aV[BALLOONEDGESIZE] + '" height="100%" src="' + aImg[4].src + sImgZ
+ + '</td>'
+ + '</tr><tr>'
+ // Left-bottom corner
+ + '<td' + sCssCrn + sVaT + '>'
+ + '<img src="' + aImg[7].src + '" width="' + tt_aV[BALLOONEDGESIZE] + '" height="' + tt_aV[BALLOONEDGESIZE] + sImgZ
+ + '</td>'
+ // Bottom border
+ + '<td valign="top" style="position:relative;' + sCss0 + '">'
+ + '<div style="position:relative;left:0;top:0;' + sCss0 + 'width:auto;height:' + tt_aV[BALLOONEDGESIZE] + 'px;background-image:url(' + aImg[6].src + ');"></div>'
+ + '<img id="bALlOOnB" style="position:relative;top:-1px;left:2px;z-index:1;display:none;' + sCss0 + '" src="' + aImg[10].src + '" width="' + tt_aV[BALLOONSTEMWIDTH] + '" height="' + tt_aV[BALLOONSTEMHEIGHT] + '" />'
+ + '</td>'
+ // Right-bottom corner
+ + '<td' + sCssCrn + sVaT + '>'
+ + '<img src="' + aImg[5].src + '" width="' + tt_aV[BALLOONEDGESIZE] + '" height="' + tt_aV[BALLOONEDGESIZE] + sImgZ
+ + '</td>'
+ + '</tr></table>';//alert(tt_sContent);
+ return true;
+};
+balloon.OnSubDivsCreated = function()
+{
+ if(tt_aV[BALLOON])
+ {
+ var bdy = tt_GetElt("bALlO0nBdY");
+
+ // Insert a TagToTip() HTML element into the central body TD
+ if (tt_t2t && !tt_aV[COPYCONTENT] && bdy)
+ tt_MovDomNode(tt_t2t, tt_GetDad(tt_t2t), bdy);
+ balloon.iStem = tt_aV[ABOVE] * 1;
+ balloon.aStem = [tt_GetElt("bALlOOnT"), tt_GetElt("bALlOOnB")];
+ balloon.aStem[balloon.iStem].style.display = "inline";
+ if (balloon.width < -1)
+ Balloon_MaxW(bdy);
+ return true;
+ }
+ return false;
+};
+// Display the stem appropriately
+balloon.OnMoveAfter = function()
+{
+ if(tt_aV[BALLOON])
+ {
+ var iStem = (tt_aV[ABOVE] != tt_bJmpVert) * 1;
+
+ // Tooltip position vertically flipped?
+ if(iStem != balloon.iStem)
+ {
+ // Display opposite stem
+ balloon.aStem[balloon.iStem].style.display = "none";
+ balloon.aStem[iStem].style.display = "inline";
+ balloon.iStem = iStem;
+ }
+
+ balloon.aStem[iStem].style.left = Balloon_CalcStemX() + "px";
+ return true;
+ }
+ return false;
+};
+function Balloon_CalcStemX()
+{
+ var x = tt_musX - tt_x + tt_aV[BALLOONSTEMOFFSET] - tt_aV[BALLOONEDGESIZE];
+ return Math.max(Math.min(x, tt_w - tt_aV[BALLOONSTEMWIDTH] - (tt_aV[BALLOONEDGESIZE] << 1) - 2), 2);
+}
+function Balloon_CacheImgs(sPath, sExt)
+{
+ var asImg = ["background", "lt", "t", "rt", "r", "rb", "b", "lb", "l", "stemt", "stemb"],
+ n = asImg.length,
+ aImg = new Array(n),
+ img;
+
+ while(n)
+ {--n;
+ img = aImg[n] = new Image();
+ img.src = sPath + asImg[n] + "." + sExt;
+ }
+ return aImg;
+}
+function Balloon_MaxW(bdy)
+{
+ if (bdy)
+ {
+ var iAdd = tt_bBoxOld ? (balloon.padding << 1) : 0, w = tt_GetDivW(bdy);
+ if (w > -balloon.width + iAdd)
+ bdy.style.width = (-balloon.width + iAdd) + "px";
+ }
+}
+// This mechanism pre-caches the default images specified by
+// congif.BalloonImgPath, so, whenever a balloon tip using these default images
+// is created, no further server connection is necessary.
+function Balloon_PreCacheDefImgs()
+{
+ // Append slash to img path if missing
+ if(config.BalloonImgPath.charAt(config.BalloonImgPath.length - 1) != '/')
+ config.BalloonImgPath += "/";
+ // Preload default images into array
+ balloon.aDefImg = Balloon_CacheImgs(config.BalloonImgPath, config.BalloonImgExt);
+}
+Balloon_PreCacheDefImgs();
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/b.gif b/subsonic-main/src/main/webapp/script/tip_balloon/b.gif
new file mode 100644
index 00000000..ff0c04b2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/b.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/background.gif b/subsonic-main/src/main/webapp/script/tip_balloon/background.gif
new file mode 100644
index 00000000..cb61e7ff
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/background.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/l.gif b/subsonic-main/src/main/webapp/script/tip_balloon/l.gif
new file mode 100644
index 00000000..14a68d93
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/l.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/lb.gif b/subsonic-main/src/main/webapp/script/tip_balloon/lb.gif
new file mode 100644
index 00000000..713c3739
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/lb.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/lt.gif b/subsonic-main/src/main/webapp/script/tip_balloon/lt.gif
new file mode 100644
index 00000000..93c71f46
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/lt.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/r.gif b/subsonic-main/src/main/webapp/script/tip_balloon/r.gif
new file mode 100644
index 00000000..be041ff9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/r.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/rb.gif b/subsonic-main/src/main/webapp/script/tip_balloon/rb.gif
new file mode 100644
index 00000000..70b6834e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/rb.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/rt.gif b/subsonic-main/src/main/webapp/script/tip_balloon/rt.gif
new file mode 100644
index 00000000..2752a0cc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/rt.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/stemb.gif b/subsonic-main/src/main/webapp/script/tip_balloon/stemb.gif
new file mode 100644
index 00000000..92f2ad51
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/stemb.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/stemt.gif b/subsonic-main/src/main/webapp/script/tip_balloon/stemt.gif
new file mode 100644
index 00000000..d259a4ae
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/stemt.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/tip_balloon/t.gif b/subsonic-main/src/main/webapp/script/tip_balloon/t.gif
new file mode 100644
index 00000000..6fec86f8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/tip_balloon/t.gif
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/webfx/boxsizing.htc b/subsonic-main/src/main/webapp/script/webfx/boxsizing.htc
new file mode 100644
index 00000000..fbeaa56c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/boxsizing.htc
@@ -0,0 +1,157 @@
+<component lightWeight="true">
+<attach event="onpropertychange" onevent="checkPropertyChange()" />
+<attach event="ondetach" onevent="restore()" />
+<script>
+//<![CDATA[
+
+var doc = element.document;
+
+function init() {
+ updateBorderBoxWidth();
+ updateBorderBoxHeight();
+}
+
+function restore() {
+ element.runtimeStyle.width = "";
+ element.runtimeStyle.height = "";
+}
+
+/* border width getters */
+function getBorderWidth(sSide) {
+ if (element.currentStyle["border" + sSide + "Style"] == "none")
+ return 0;
+ var n = parseInt(element.currentStyle["border" + sSide + "Width"]);
+ return n || 0;
+}
+
+function getBorderLeftWidth() { return getBorderWidth("Left"); }
+function getBorderRightWidth() { return getBorderWidth("Right"); }
+function getBorderTopWidth() { return getBorderWidth("Top"); }
+function getBorderBottomWidth() { return getBorderWidth("Bottom"); }
+/* end border width getters */
+
+/* padding getters */
+function getPadding(sSide) {
+ var n = parseInt(element.currentStyle["padding" + sSide]);
+ return n || 0;
+}
+
+function getPaddingLeft() { return getPadding("Left"); }
+function getPaddingRight() { return getPadding("Right"); }
+function getPaddingTop() { return getPadding("Top"); }
+function getPaddingBottom() { return getPadding("Bottom"); }
+/* end padding getters */
+
+function getBoxSizing() {
+ var s = element.style;
+ var cs = element.currentStyle
+
+ if (typeof s.boxSizing != "undefined" && s.boxSizing != "")
+ return s.boxSizing;
+ if (typeof s["box-sizing"] != "undefined" && s["box-sizing"] != "")
+ return s["box-sizing"];
+ if (typeof cs.boxSizing != "undefined" && cs.boxSizing != "")
+ return cs.boxSizing;
+ if (typeof cs["box-sizing"] != "undefined" && cs["box-sizing"] != "")
+ return cs["box-sizing"];
+ return getDocumentBoxSizing();
+}
+
+function getDocumentBoxSizing() {
+ if (doc.compatMode == null || doc.compatMode == "BackCompat")
+ return "border-box";
+ return "content-box"
+}
+
+/* width and height setters */
+function setBorderBoxWidth(n) {
+ element.runtimeStyle.width = Math.max(0, n - getBorderLeftWidth() -
+ getPaddingLeft() - getPaddingRight() - getBorderRightWidth()) + "px";
+}
+
+function setBorderBoxHeight(n) {
+ element.runtimeStyle.height = Math.max(0, n - getBorderTopWidth() -
+ getPaddingTop() - getPaddingBottom() - getBorderBottomWidth()) + "px";
+}
+
+function setContentBoxWidth(n) {
+ element.runtimeStyle.width = Math.max(0, n + getBorderLeftWidth() +
+ getPaddingLeft() + getPaddingRight() + getBorderRightWidth()) + "px";
+}
+
+function setContentBoxHeight(n) {
+ element.runtimeStyle.height = Math.max(0, n + getBorderTopWidth() +
+ getPaddingTop() + getPaddingBottom() + getBorderBottomWidth()) + "px";
+}
+/* end width and height setters */
+
+function updateBorderBoxWidth() {
+ element.runtimeStyle.width = "";
+ if (getDocumentBoxSizing() == getBoxSizing())
+ return;
+ var csw = element.currentStyle.width;
+ if (csw != "auto" && csw.indexOf("px") != -1) {
+ if (getBoxSizing() == "border-box")
+ setBorderBoxWidth(parseInt(csw));
+ else
+ setContentBoxWidth(parseInt(csw));
+ }
+}
+
+function updateBorderBoxHeight() {
+ element.runtimeStyle.height = "";
+ if (getDocumentBoxSizing() == getBoxSizing())
+ return;
+ var csh = element.currentStyle.height;
+ if (csh != "auto" && csh.indexOf("px") != -1) {
+ if (getBoxSizing() == "border-box")
+ setBorderBoxHeight(parseInt(csh));
+ else
+ setContentBoxHeight(parseInt(csh));
+ }
+}
+
+function checkPropertyChange() {
+ var pn = event.propertyName;
+ var undef;
+
+ if (pn == "style.boxSizing" && element.style.boxSizing == "") {
+ element.style.removeAttribute("boxSizing");
+ element.runtimeStyle.boxSizing = undef;
+ }
+
+
+ switch (pn) {
+ case "style.width":
+ case "style.borderLeftWidth":
+ case "style.borderLeftStyle":
+ case "style.borderRightWidth":
+ case "style.borderRightStyle":
+ case "style.paddingLeft":
+ case "style.paddingRight":
+ updateBorderBoxWidth();
+ break;
+
+ case "style.height":
+ case "style.borderTopWidth":
+ case "style.borderTopStyle":
+ case "style.borderBottomWidth":
+ case "style.borderBottomStyle":
+ case "style.paddingTop":
+ case "style.paddingBottom":
+ updateBorderBoxHeight();
+ break;
+
+ case "className":
+ case "style.boxSizing":
+ updateBorderBoxWidth();
+ updateBorderBoxHeight();
+ break;
+ }
+}
+
+init();
+
+//]]>
+</script>
+</component> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/webfx/handle.horizontal.hover.png b/subsonic-main/src/main/webapp/script/webfx/handle.horizontal.hover.png
new file mode 100644
index 00000000..d2fd059d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/handle.horizontal.hover.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/webfx/handle.horizontal.png b/subsonic-main/src/main/webapp/script/webfx/handle.horizontal.png
new file mode 100644
index 00000000..b3af6bbf
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/handle.horizontal.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/webfx/handle.vertical.hover.png b/subsonic-main/src/main/webapp/script/webfx/handle.vertical.hover.png
new file mode 100644
index 00000000..379cdc29
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/handle.vertical.hover.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/webfx/handle.vertical.png b/subsonic-main/src/main/webapp/script/webfx/handle.vertical.png
new file mode 100644
index 00000000..537135e8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/handle.vertical.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/script/webfx/luna.css b/subsonic-main/src/main/webapp/script/webfx/luna.css
new file mode 100644
index 00000000..b01107cb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/luna.css
@@ -0,0 +1,75 @@
+.dynamic-slider-control {
+ position: relative;
+ /*background-color: ThreeDFace;*/
+ -moz-user-focus: normal;
+ -moz-user-select: none;
+ cursor: default;
+}
+
+.horizontal {
+ width: 200px;
+ height: 27px;
+}
+
+.vertical {
+ width: 29px;
+ height: 200px;
+}
+
+.dynamic-slider-control input {
+ display: none;
+}
+
+.dynamic-slider-control .handle {
+ position: absolute;
+ font-size: 1px;
+ overflow: hidden;
+ -moz-user-select: none;
+ cursor: default;
+}
+
+.dynamic-slider-control.horizontal .handle {
+ width: 11px;
+ height: 21px;
+ background-image: url("handle.horizontal.png");
+}
+
+.dynamic-slider-control.horizontal .handle div {}
+.dynamic-slider-control.horizontal .handle.hover {
+ background-image: url("handle.horizontal.hover.png");
+}
+
+.dynamic-slider-control.vertical .handle {
+ width: 25px;
+ height: 13px;
+ background-image: url("handle.vertical.png");
+}
+
+.dynamic-slider-control.vertical .handle.hover {
+ background-image: url("handle.vertical.hover.png");
+}
+
+.dynamic-slider-control .line {
+ position: absolute;
+ font-size: 0.01mm;
+ overflow: hidden;
+ border: 1px solid;
+ border-color: ThreeDShadow ThreeDHighlight
+ ThreeDHighlight ThreeDShadow;
+ -moz-border-radius: 50%;
+
+ behavior: url("boxsizing.htc"); /* ie path bug */
+ box-sizing: content-box;
+ -moz-box-sizing: content-box;
+}
+.dynamic-slider-control.vertical .line {
+ width: 2px;
+}
+
+.dynamic-slider-control.horizontal .line {
+ height: 2px;
+}
+
+.dynamic-slider-control .line div {
+ display: none;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/webfx/range.js b/subsonic-main/src/main/webapp/script/webfx/range.js
new file mode 100644
index 00000000..53c8f34e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/range.js
@@ -0,0 +1,132 @@
+/*----------------------------------------------------------------------------\
+| Range Class |
+|-----------------------------------------------------------------------------|
+| Created by Erik Arvidsson |
+| (http://webfx.eae.net/contact.html#erik) |
+| For WebFX (http://webfx.eae.net/) |
+|-----------------------------------------------------------------------------|
+| Used to model the data used when working with sliders, scrollbars and |
+| progress bars. Based on the ideas of the javax.swing.BoundedRangeModel |
+| interface defined by Sun for Java; http://java.sun.com/products/jfc/ |
+| swingdoc-api-1.0.3/com/sun/java/swing/BoundedRangeModel.html |
+|-----------------------------------------------------------------------------|
+| Copyright (c) 2002, 2005, 2006 Erik Arvidsson |
+|-----------------------------------------------------------------------------|
+| Licensed under the Apache License, Version 2.0 (the "License"); you may not |
+| use this file except in compliance with the License. You may obtain a copy |
+| of the License at http://www.apache.org/licenses/LICENSE-2.0 |
+| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
+| Unless required by applicable law or agreed to in writing, software |
+| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
+| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
+| License for the specific language governing permissions and limitations |
+| under the License. |
+|-----------------------------------------------------------------------------|
+| 2002-10-14 | Original version released |
+| 2005-10-27 | Use Math.round instead of Math.floor |
+| 2006-05-28 | Changed license to Apache Software License 2.0. |
+|-----------------------------------------------------------------------------|
+| Created 2002-10-14 | All changes are in the log above. | Updated 2006-05-28 |
+\----------------------------------------------------------------------------*/
+
+
+function Range() {
+ this._value = 0;
+ this._minimum = 0;
+ this._maximum = 100;
+ this._extent = 0;
+
+ this._isChanging = false;
+}
+
+Range.prototype.setValue = function (value) {
+ value = Math.round(parseFloat(value));
+ if (isNaN(value)) return;
+ if (this._value != value) {
+ if (value + this._extent > this._maximum)
+ this._value = this._maximum - this._extent;
+ else if (value < this._minimum)
+ this._value = this._minimum;
+ else
+ this._value = value;
+ if (!this._isChanging && typeof this.onchange == "function")
+ this.onchange();
+ }
+};
+
+Range.prototype.getValue = function () {
+ return this._value;
+};
+
+Range.prototype.setExtent = function (extent) {
+ if (this._extent != extent) {
+ if (extent < 0)
+ this._extent = 0;
+ else if (this._value + extent > this._maximum)
+ this._extent = this._maximum - this._value;
+ else
+ this._extent = extent;
+ if (!this._isChanging && typeof this.onchange == "function")
+ this.onchange();
+ }
+};
+
+Range.prototype.getExtent = function () {
+ return this._extent;
+};
+
+Range.prototype.setMinimum = function (minimum) {
+ if (this._minimum != minimum) {
+ var oldIsChanging = this._isChanging;
+ this._isChanging = true;
+
+ this._minimum = minimum;
+
+ if (minimum > this._value)
+ this.setValue(minimum);
+ if (minimum > this._maximum) {
+ this._extent = 0;
+ this.setMaximum(minimum);
+ this.setValue(minimum)
+ }
+ if (minimum + this._extent > this._maximum)
+ this._extent = this._maximum - this._minimum;
+
+ this._isChanging = oldIsChanging;
+ if (!this._isChanging && typeof this.onchange == "function")
+ this.onchange();
+ }
+};
+
+Range.prototype.getMinimum = function () {
+ return this._minimum;
+};
+
+Range.prototype.setMaximum = function (maximum) {
+ if (this._maximum != maximum) {
+ var oldIsChanging = this._isChanging;
+ this._isChanging = true;
+
+ this._maximum = maximum;
+
+ if (maximum < this._value)
+ this.setValue(maximum - this._extent);
+ if (maximum < this._minimum) {
+ this._extent = 0;
+ this.setMinimum(maximum);
+ this.setValue(this._maximum);
+ }
+ if (maximum < this._minimum + this._extent)
+ this._extent = this._maximum - this._minimum;
+ if (maximum < this._value + this._extent)
+ this._extent = this._maximum - this._value;
+
+ this._isChanging = oldIsChanging;
+ if (!this._isChanging && typeof this.onchange == "function")
+ this.onchange();
+ }
+};
+
+Range.prototype.getMaximum = function () {
+ return this._maximum;
+};
diff --git a/subsonic-main/src/main/webapp/script/webfx/slider.js b/subsonic-main/src/main/webapp/script/webfx/slider.js
new file mode 100644
index 00000000..ddc6d756
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/slider.js
@@ -0,0 +1,489 @@
+/*----------------------------------------------------------------------------\
+| Slider 1.02 |
+|-----------------------------------------------------------------------------|
+| Created by Erik Arvidsson |
+| (http://webfx.eae.net/contact.html#erik) |
+| For WebFX (http://webfx.eae.net/) |
+|-----------------------------------------------------------------------------|
+| A slider control that degrades to an input control for non supported |
+| browsers. |
+|-----------------------------------------------------------------------------|
+| Copyright (c) 2002, 2003, 2006 Erik Arvidsson |
+|-----------------------------------------------------------------------------|
+| Licensed under the Apache License, Version 2.0 (the "License"); you may not |
+| use this file except in compliance with the License. You may obtain a copy |
+| of the License at http://www.apache.org/licenses/LICENSE-2.0 |
+| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
+| Unless required by applicable law or agreed to in writing, software |
+| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
+| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
+| License for the specific language governing permissions and limitations |
+| under the License. |
+|-----------------------------------------------------------------------------|
+| Dependencies: timer.js - an OO abstraction of timers |
+| range.js - provides the data model for the slider |
+| winclassic.css or any other css file describing the look |
+|-----------------------------------------------------------------------------|
+| 2002-10-14 | Original version released |
+| 2003-03-27 | Added a test in the constructor for missing oElement arg |
+| 2003-11-27 | Only use mousewheel when focused |
+| 2006-05-28 | Changed license to Apache Software License 2.0. |
+|-----------------------------------------------------------------------------|
+| Created 2002-10-14 | All changes are in the log above. | Updated 2006-05-28 |
+\----------------------------------------------------------------------------*/
+
+Slider.isSupported = typeof document.createElement != "undefined" &&
+ typeof document.documentElement != "undefined" &&
+ typeof document.documentElement.offsetWidth == "number";
+
+
+function Slider(oElement, oInput, sOrientation) {
+ if (!oElement) return;
+ this._orientation = sOrientation || "horizontal";
+ this._range = new Range();
+ this._range.setExtent(0);
+ this._blockIncrement = 10;
+ this._unitIncrement = 1;
+ this._timer = new Timer(100);
+
+
+ if (Slider.isSupported && oElement) {
+
+ this.document = oElement.ownerDocument || oElement.document;
+
+ this.element = oElement;
+ this.element.slider = this;
+ this.element.unselectable = "on";
+
+ // add class name tag to class name
+ this.element.className = this._orientation + " " + this.classNameTag + " " + this.element.className;
+
+ // create line
+ this.line = this.document.createElement("DIV");
+ this.line.className = "line";
+ this.line.unselectable = "on";
+ this.line.appendChild(this.document.createElement("DIV"));
+ this.element.appendChild(this.line);
+
+ // create handle
+ this.handle = this.document.createElement("DIV");
+ this.handle.className = "handle";
+ this.handle.unselectable = "on";
+ this.handle.appendChild(this.document.createElement("DIV"));
+ this.handle.firstChild.appendChild(
+ this.document.createTextNode(String.fromCharCode(160)));
+ this.element.appendChild(this.handle);
+ }
+
+ this.input = oInput;
+
+ // events
+ var oThis = this;
+ this._range.onchange = function () {
+ oThis.recalculate();
+ if (typeof oThis.onchange == "function")
+ oThis.onchange();
+ };
+
+ if (Slider.isSupported && oElement) {
+ this.element.onfocus = Slider.eventHandlers.onfocus;
+ this.element.onblur = Slider.eventHandlers.onblur;
+ this.element.onmousedown = Slider.eventHandlers.onmousedown;
+ this.element.onmouseover = Slider.eventHandlers.onmouseover;
+ this.element.onmouseout = Slider.eventHandlers.onmouseout;
+ this.element.onkeydown = Slider.eventHandlers.onkeydown;
+ this.element.onkeypress = Slider.eventHandlers.onkeypress;
+ this.element.onmousewheel = Slider.eventHandlers.onmousewheel;
+ this.handle.onselectstart =
+ this.element.onselectstart = function () { return false; };
+
+ this._timer.ontimer = function () {
+ oThis.ontimer();
+ };
+
+ // extra recalculate for ie
+ window.setTimeout(function() {
+ oThis.recalculate();
+ }, 1);
+ }
+ else {
+ this.input.onchange = function (e) {
+ oThis.setValue(oThis.input.value);
+ };
+ }
+}
+
+Slider.eventHandlers = {
+
+ // helpers to make events a bit easier
+ getEvent: function (e, el) {
+ if (!e) {
+ if (el)
+ e = el.document.parentWindow.event;
+ else
+ e = window.event;
+ }
+ if (!e.srcElement) {
+ var el = e.target;
+ while (el != null && el.nodeType != 1)
+ el = el.parentNode;
+ e.srcElement = el;
+ }
+ if (typeof e.offsetX == "undefined") {
+ e.offsetX = e.layerX;
+ e.offsetY = e.layerY;
+ }
+
+ return e;
+ },
+
+ getDocument: function (e) {
+ if (e.target)
+ return e.target.ownerDocument;
+ return e.srcElement.document;
+ },
+
+ getSlider: function (e) {
+ var el = e.target || e.srcElement;
+ while (el != null && el.slider == null) {
+ el = el.parentNode;
+ }
+ if (el)
+ return el.slider;
+ return null;
+ },
+
+ getLine: function (e) {
+ var el = e.target || e.srcElement;
+ while (el != null && el.className != "line") {
+ el = el.parentNode;
+ }
+ return el;
+ },
+
+ getHandle: function (e) {
+ var el = e.target || e.srcElement;
+ var re = /handle/;
+ while (el != null && !re.test(el.className)) {
+ el = el.parentNode;
+ }
+ return el;
+ },
+ // end helpers
+
+ onfocus: function (e) {
+ var s = this.slider;
+ s._focused = true;
+ s.handle.className = "handle hover";
+ },
+
+ onblur: function (e) {
+ var s = this.slider
+ s._focused = false;
+ s.handle.className = "handle";
+ },
+
+ onmouseover: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ var s = this.slider;
+ if (e.srcElement == s.handle)
+ s.handle.className = "handle hover";
+ },
+
+ onmouseout: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ var s = this.slider;
+ if (e.srcElement == s.handle && !s._focused)
+ s.handle.className = "handle";
+ },
+
+ onmousedown: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ var s = this.slider;
+ if (s.element.focus)
+ s.element.focus();
+
+ Slider._currentInstance = s;
+ var doc = s.document;
+
+ if (doc.addEventListener) {
+ doc.addEventListener("mousemove", Slider.eventHandlers.onmousemove, true);
+ doc.addEventListener("mouseup", Slider.eventHandlers.onmouseup, true);
+ }
+ else if (doc.attachEvent) {
+ doc.attachEvent("onmousemove", Slider.eventHandlers.onmousemove);
+ doc.attachEvent("onmouseup", Slider.eventHandlers.onmouseup);
+ doc.attachEvent("onlosecapture", Slider.eventHandlers.onmouseup);
+ s.element.setCapture();
+ }
+
+ if (Slider.eventHandlers.getHandle(e)) { // start drag
+ Slider._sliderDragData = {
+ screenX: e.screenX,
+ screenY: e.screenY,
+ dx: e.screenX - s.handle.offsetLeft,
+ dy: e.screenY - s.handle.offsetTop,
+ startValue: s.getValue(),
+ slider: s
+ };
+ }
+ else {
+ var lineEl = Slider.eventHandlers.getLine(e);
+ s._mouseX = e.offsetX + (lineEl ? s.line.offsetLeft : 0);
+ s._mouseY = e.offsetY + (lineEl ? s.line.offsetTop : 0);
+ s._increasing = null;
+ s.ontimer();
+ }
+ },
+
+ onmousemove: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+
+ if (Slider._sliderDragData) { // drag
+ var s = Slider._sliderDragData.slider;
+
+ var boundSize = s.getMaximum() - s.getMinimum();
+ var size, pos, reset;
+
+ if (s._orientation == "horizontal") {
+ size = s.element.offsetWidth - s.handle.offsetWidth;
+ pos = e.screenX - Slider._sliderDragData.dx;
+ reset = Math.abs(e.screenY - Slider._sliderDragData.screenY) > 100;
+ }
+ else {
+ size = s.element.offsetHeight - s.handle.offsetHeight;
+ pos = s.element.offsetHeight - s.handle.offsetHeight -
+ (e.screenY - Slider._sliderDragData.dy);
+ reset = Math.abs(e.screenX - Slider._sliderDragData.screenX) > 100;
+ }
+ s.setValue(reset ? Slider._sliderDragData.startValue :
+ s.getMinimum() + boundSize * pos / size);
+ return false;
+ }
+ else {
+ var s = Slider._currentInstance;
+ if (s != null) {
+ var lineEl = Slider.eventHandlers.getLine(e);
+ s._mouseX = e.offsetX + (lineEl ? s.line.offsetLeft : 0);
+ s._mouseY = e.offsetY + (lineEl ? s.line.offsetTop : 0);
+ }
+ }
+
+ },
+
+ onmouseup: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ var s = Slider._currentInstance;
+ var doc = s.document;
+ if (doc.removeEventListener) {
+ doc.removeEventListener("mousemove", Slider.eventHandlers.onmousemove, true);
+ doc.removeEventListener("mouseup", Slider.eventHandlers.onmouseup, true);
+ }
+ else if (doc.detachEvent) {
+ doc.detachEvent("onmousemove", Slider.eventHandlers.onmousemove);
+ doc.detachEvent("onmouseup", Slider.eventHandlers.onmouseup);
+ doc.detachEvent("onlosecapture", Slider.eventHandlers.onmouseup);
+ s.element.releaseCapture();
+ }
+
+ if (Slider._sliderDragData) { // end drag
+ Slider._sliderDragData = null;
+ }
+ else {
+ s._timer.stop();
+ s._increasing = null;
+ }
+ Slider._currentInstance = null;
+ },
+
+ onkeydown: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ //var s = Slider.eventHandlers.getSlider(e);
+ var s = this.slider;
+ var kc = e.keyCode;
+ switch (kc) {
+ case 33: // page up
+ s.setValue(s.getValue() + s.getBlockIncrement());
+ break;
+ case 34: // page down
+ s.setValue(s.getValue() - s.getBlockIncrement());
+ break;
+ case 35: // end
+ s.setValue(s.getOrientation() == "horizontal" ?
+ s.getMaximum() :
+ s.getMinimum());
+ break;
+ case 36: // home
+ s.setValue(s.getOrientation() == "horizontal" ?
+ s.getMinimum() :
+ s.getMaximum());
+ break;
+ case 38: // up
+ case 39: // right
+ s.setValue(s.getValue() + s.getUnitIncrement());
+ break;
+
+ case 37: // left
+ case 40: // down
+ s.setValue(s.getValue() - s.getUnitIncrement());
+ break;
+ }
+
+ if (kc >= 33 && kc <= 40) {
+ return false;
+ }
+ },
+
+ onkeypress: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ var kc = e.keyCode;
+ if (kc >= 33 && kc <= 40) {
+ return false;
+ }
+ },
+
+ onmousewheel: function (e) {
+ e = Slider.eventHandlers.getEvent(e, this);
+ var s = this.slider;
+ if (s._focused) {
+ s.setValue(s.getValue() + e.wheelDelta / 120 * s.getUnitIncrement());
+ // windows inverts this on horizontal sliders. That does not
+ // make sense to me
+ return false;
+ }
+ }
+};
+
+
+
+Slider.prototype.classNameTag = "dynamic-slider-control";
+
+Slider.prototype.setValue = function (v) {
+ this._range.setValue(v);
+ this.input.value = this.getValue();
+};
+
+Slider.prototype.getValue = function () {
+ return this._range.getValue();
+};
+
+Slider.prototype.setMinimum = function (v) {
+ this._range.setMinimum(v);
+ this.input.value = this.getValue();
+};
+
+Slider.prototype.getMinimum = function () {
+ return this._range.getMinimum();
+};
+
+Slider.prototype.setMaximum = function (v) {
+ this._range.setMaximum(v);
+ this.input.value = this.getValue();
+};
+
+Slider.prototype.getMaximum = function () {
+ return this._range.getMaximum();
+};
+
+Slider.prototype.setUnitIncrement = function (v) {
+ this._unitIncrement = v;
+};
+
+Slider.prototype.getUnitIncrement = function () {
+ return this._unitIncrement;
+};
+
+Slider.prototype.setBlockIncrement = function (v) {
+ this._blockIncrement = v;
+};
+
+Slider.prototype.getBlockIncrement = function () {
+ return this._blockIncrement;
+};
+
+Slider.prototype.getOrientation = function () {
+ return this._orientation;
+};
+
+Slider.prototype.setOrientation = function (sOrientation) {
+ if (sOrientation != this._orientation) {
+ if (Slider.isSupported && this.element) {
+ // add class name tag to class name
+ this.element.className = this.element.className.replace(this._orientation,
+ sOrientation);
+ }
+ this._orientation = sOrientation;
+ this.recalculate();
+
+ }
+};
+
+Slider.prototype.recalculate = function() {
+ if (!Slider.isSupported || !this.element) return;
+
+ var w = this.element.offsetWidth;
+ var h = this.element.offsetHeight;
+ var hw = this.handle.offsetWidth;
+ var hh = this.handle.offsetHeight;
+ var lw = this.line.offsetWidth;
+ var lh = this.line.offsetHeight;
+
+ // this assumes a border-box layout
+
+ if (this._orientation == "horizontal") {
+ this.handle.style.left = (w - hw) * (this.getValue() - this.getMinimum()) /
+ (this.getMaximum() - this.getMinimum()) + "px";
+ this.handle.style.top = (h - hh) / 2 + "px";
+
+ this.line.style.top = (h - lh) / 2 + "px";
+ this.line.style.left = hw / 2 + "px";
+ //this.line.style.right = hw / 2 + "px";
+ this.line.style.width = Math.max(0, w - hw - 2)+ "px";
+ this.line.firstChild.style.width = Math.max(0, w - hw - 4)+ "px";
+ }
+ else {
+ this.handle.style.left = (w - hw) / 2 + "px";
+ this.handle.style.top = h - hh - (h - hh) * (this.getValue() - this.getMinimum()) /
+ (this.getMaximum() - this.getMinimum()) + "px";
+
+ this.line.style.left = (w - lw) / 2 + "px";
+ this.line.style.top = hh / 2 + "px";
+ this.line.style.height = Math.max(0, h - hh - 2) + "px"; //hard coded border width
+ //this.line.style.bottom = hh / 2 + "px";
+ this.line.firstChild.style.height = Math.max(0, h - hh - 4) + "px"; //hard coded border width
+ }
+};
+
+Slider.prototype.ontimer = function () {
+ var hw = this.handle.offsetWidth;
+ var hh = this.handle.offsetHeight;
+ var hl = this.handle.offsetLeft;
+ var ht = this.handle.offsetTop;
+
+ if (this._orientation == "horizontal") {
+ if (this._mouseX > hl + hw &&
+ (this._increasing == null || this._increasing)) {
+ this.setValue(this.getValue() + this.getBlockIncrement());
+ this._increasing = true;
+ }
+ else if (this._mouseX < hl &&
+ (this._increasing == null || !this._increasing)) {
+ this.setValue(this.getValue() - this.getBlockIncrement());
+ this._increasing = false;
+ }
+ }
+ else {
+ if (this._mouseY > ht + hh &&
+ (this._increasing == null || !this._increasing)) {
+ this.setValue(this.getValue() - this.getBlockIncrement());
+ this._increasing = false;
+ }
+ else if (this._mouseY < ht &&
+ (this._increasing == null || this._increasing)) {
+ this.setValue(this.getValue() + this.getBlockIncrement());
+ this._increasing = true;
+ }
+ }
+
+ this._timer.start();
+}; \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/webfx/timer.js b/subsonic-main/src/main/webapp/script/webfx/timer.js
new file mode 100644
index 00000000..0c1e897f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/webfx/timer.js
@@ -0,0 +1,62 @@
+/*----------------------------------------------------------------------------\
+| Timer Class |
+|-----------------------------------------------------------------------------|
+| Created by Erik Arvidsson |
+| (http://webfx.eae.net/contact.html#erik) |
+| For WebFX (http://webfx.eae.net/) |
+|-----------------------------------------------------------------------------|
+| Object Oriented Encapsulation of setTimeout fires ontimer when the timer |
+| is triggered. Does not work in IE 5.00 |
+|-----------------------------------------------------------------------------|
+| Copyright (c) 2002, 2006 Erik Arvidsson |
+|-----------------------------------------------------------------------------|
+| Licensed under the Apache License, Version 2.0 (the "License"); you may not |
+| use this file except in compliance with the License. You may obtain a copy |
+| of the License at http://www.apache.org/licenses/LICENSE-2.0 |
+| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
+| Unless required by applicable law or agreed to in writing, software |
+| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
+| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
+| License for the specific language governing permissions and limitations |
+| under the License. |
+|-----------------------------------------------------------------------------|
+| 2002-10-14 | Original version released |
+| 2006-05-28 | Changed license to Apache Software License 2.0. |
+|-----------------------------------------------------------------------------|
+| Created 2002-10-14 | All changes are in the log above. | Updated 2006-05-28 |
+\----------------------------------------------------------------------------*/
+
+function Timer(nPauseTime) {
+ this._pauseTime = typeof nPauseTime == "undefined" ? 1000 : nPauseTime;
+ this._timer = null;
+ this._isStarted = false;
+}
+
+Timer.prototype.start = function () {
+ if (this.isStarted())
+ this.stop();
+ var oThis = this;
+ this._timer = window.setTimeout(function () {
+ if (typeof oThis.ontimer == "function")
+ oThis.ontimer();
+ }, this._pauseTime);
+ this._isStarted = false;
+};
+
+Timer.prototype.stop = function () {
+ if (this._timer != null)
+ window.clearTimeout(this._timer);
+ this._isStarted = false;
+};
+
+Timer.prototype.isStarted = function () {
+ return this._isStarted;
+};
+
+Timer.prototype.getPauseTime = function () {
+ return this._pauseTime;
+};
+
+Timer.prototype.setPauseTime = function (nPauseTime) {
+ this._pauseTime = nPauseTime;
+}; \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/script/wz_tooltip.js b/subsonic-main/src/main/webapp/script/wz_tooltip.js
new file mode 100644
index 00000000..01f55f2d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/script/wz_tooltip.js
@@ -0,0 +1,1301 @@
+/* This notice must be untouched at all times.
+Copyright (c) 2002-2008 Walter Zorn. All rights reserved.
+
+wz_tooltip.js v. 5.31
+
+The latest version is available at
+http://www.walterzorn.com
+or http://www.devira.com
+or http://www.walterzorn.de
+
+Created 1.12.2002 by Walter Zorn (Web: http://www.walterzorn.com )
+Last modified: 7.11.2008
+
+Easy-to-use cross-browser tooltips.
+Just include the script at the beginning of the <body> section, and invoke
+Tip('Tooltip text') to show and UnTip() to hide the tooltip, from the desired
+HTML eventhandlers. Example:
+<a onmouseover="Tip('Some text')" onmouseout="UnTip()" href="index.htm">My home page</a>
+No container DIV required.
+By default, width and height of tooltips are automatically adapted to content.
+Is even capable of dynamically converting arbitrary HTML elements to tooltips
+by calling TagToTip('ID_of_HTML_element_to_be_converted') instead of Tip(),
+which means you can put important, search-engine-relevant stuff into tooltips.
+Appearance & behaviour of tooltips can be individually configured
+via commands passed to Tip() or TagToTip().
+
+Tab Width: 4
+LICENSE: LGPL
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License (LGPL) as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+For more details on the GNU Lesser General Public License,
+see http://www.gnu.org/copyleft/lesser.html
+*/
+
+var config = new Object();
+
+
+//=================== GLOBAL TOOLTIP CONFIGURATION =========================//
+var tt_Debug = true // false or true - recommended: false once you release your page to the public
+var tt_Enabled = true // Allows to (temporarily) suppress tooltips, e.g. by providing the user with a button that sets this global variable to false
+var TagsToTip = true // false or true - if true, HTML elements to be converted to tooltips via TagToTip() are automatically hidden;
+ // if false, you should hide those HTML elements yourself
+
+// For each of the following config variables there exists a command, which is
+// just the variablename in uppercase, to be passed to Tip() or TagToTip() to
+// configure tooltips individually. Individual commands override global
+// configuration. Order of commands is arbitrary.
+// Example: onmouseover="Tip('Tooltip text', LEFT, true, BGCOLOR, '#FF9900', FADEIN, 400)"
+
+config. Above = false // false or true - tooltip above mousepointer
+config. BgColor = '#E2E7FF' // Background colour (HTML colour value, in quotes)
+config. BgImg = '' // Path to background image, none if empty string ''
+config. BorderColor = '#003099'
+config. BorderStyle = 'solid' // Any permitted CSS value, but I recommend 'solid', 'dotted' or 'dashed'
+config. BorderWidth = 1
+config. CenterMouse = false // false or true - center the tip horizontally below (or above) the mousepointer
+config. ClickClose = false // false or true - close tooltip if the user clicks somewhere
+config. ClickSticky = false // false or true - make tooltip sticky if user left-clicks on the hovered element while the tooltip is active
+config. CloseBtn = false // false or true - closebutton in titlebar
+config. CloseBtnColors = ['#990000', '#FFFFFF', '#DD3333', '#FFFFFF'] // [Background, text, hovered background, hovered text] - use empty strings '' to inherit title colours
+config. CloseBtnText = '&nbsp;X&nbsp;' // Close button text (may also be an image tag)
+config. CopyContent = true // When converting a HTML element to a tooltip, copy only the element's content, rather than converting the element by its own
+config. Delay = 400 // Time span in ms until tooltip shows up
+config. Duration = 0 // Time span in ms after which the tooltip disappears; 0 for infinite duration, < 0 for delay in ms _after_ the onmouseout until the tooltip disappears
+config. Exclusive = false // false or true - no other tooltip can appear until the current one has actively been closed
+config. FadeIn = 100 // Fade-in duration in ms, e.g. 400; 0 for no animation
+config. FadeOut = 100
+config. FadeInterval = 30 // Duration of each fade step in ms (recommended: 30) - shorter is smoother but causes more CPU-load
+config. Fix = null // Fixated position, two modes. Mode 1: x- an y-coordinates in brackets, e.g. [210, 480]. Mode 2: Show tooltip at a position related to an HTML element: [ID of HTML element, x-offset, y-offset from HTML element], e.g. ['SomeID', 10, 30]. Value null (default) for no fixated positioning.
+config. FollowMouse = true // false or true - tooltip follows the mouse
+config. FontColor = '#000044'
+config. FontFace = 'Verdana,Geneva,sans-serif'
+config. FontSize = '8pt' // E.g. '9pt' or '12px' - unit is mandatory
+config. FontWeight = 'normal' // 'normal' or 'bold';
+config. Height = 0 // Tooltip height; 0 for automatic adaption to tooltip content, < 0 (e.g. -100) for a maximum for automatic adaption
+config. JumpHorz = false // false or true - jump horizontally to other side of mouse if tooltip would extend past clientarea boundary
+config. JumpVert = true // false or true - jump vertically "
+config. Left = false // false or true - tooltip on the left of the mouse
+config. OffsetX = 14 // Horizontal offset of left-top corner from mousepointer
+config. OffsetY = 8 // Vertical offset
+config. Opacity = 100 // Integer between 0 and 100 - opacity of tooltip in percent
+config. Padding = 3 // Spacing between border and content
+config. Shadow = false // false or true
+config. ShadowColor = '#C0C0C0'
+config. ShadowWidth = 5
+config. Sticky = false // false or true - fixate tip, ie. don't follow the mouse and don't hide on mouseout
+config. TextAlign = 'left' // 'left', 'right' or 'justify'
+config. Title = '' // Default title text applied to all tips (no default title: empty string '')
+config. TitleAlign = 'left' // 'left' or 'right' - text alignment inside the title bar
+config. TitleBgColor = '' // If empty string '', BorderColor will be used
+config. TitleFontColor = '#FFFFFF' // Color of title text - if '', BgColor (of tooltip body) will be used
+config. TitleFontFace = '' // If '' use FontFace (boldified)
+config. TitleFontSize = '' // If '' use FontSize
+config. TitlePadding = 2
+config. Width = 0 // Tooltip width; 0 for automatic adaption to tooltip content; < -1 (e.g. -240) for a maximum width for that automatic adaption;
+ // -1: tooltip width confined to the width required for the titlebar
+//======= END OF TOOLTIP CONFIG, DO NOT CHANGE ANYTHING BELOW ==============//
+
+
+
+
+//===================== PUBLIC =============================================//
+function Tip()
+{
+ tt_Tip(arguments, null);
+}
+function TagToTip()
+{
+ var t2t = tt_GetElt(arguments[0]);
+ if(t2t)
+ tt_Tip(arguments, t2t);
+}
+function UnTip()
+{
+ tt_OpReHref();
+ if(tt_aV[DURATION] < 0 && (tt_iState & 0x2))
+ tt_tDurt.Timer("tt_HideInit()", -tt_aV[DURATION], true);
+ else if(!(tt_aV[STICKY] && (tt_iState & 0x2)))
+ tt_HideInit();
+}
+
+//================== PUBLIC PLUGIN API =====================================//
+// Extension eventhandlers currently supported:
+// OnLoadConfig, OnCreateContentString, OnSubDivsCreated, OnShow, OnMoveBefore,
+// OnMoveAfter, OnHideInit, OnHide, OnKill
+
+var tt_aElt = new Array(10), // Container DIV, outer title & body DIVs, inner title & body TDs, closebutton SPAN, shadow DIVs, and IFRAME to cover windowed elements in IE
+tt_aV = new Array(), // Caches and enumerates config data for currently active tooltip
+tt_sContent, // Inner tooltip text or HTML
+tt_t2t, tt_t2tDad, // Tag converted to tip, and its DOM parent element
+tt_musX, tt_musY,
+tt_over,
+tt_x, tt_y, tt_w, tt_h; // Position, width and height of currently displayed tooltip
+
+function tt_Extension()
+{
+ tt_ExtCmdEnum();
+ tt_aExt[tt_aExt.length] = this;
+ return this;
+}
+function tt_SetTipPos(x, y)
+{
+ var css = tt_aElt[0].style;
+
+ tt_x = x;
+ tt_y = y;
+ css.left = x + "px";
+ css.top = y + "px";
+ if(tt_ie56)
+ {
+ var ifrm = tt_aElt[tt_aElt.length - 1];
+ if(ifrm)
+ {
+ ifrm.style.left = css.left;
+ ifrm.style.top = css.top;
+ }
+ }
+}
+function tt_HideInit()
+{
+ if(tt_iState)
+ {
+ tt_ExtCallFncs(0, "HideInit");
+ tt_iState &= ~(0x4 | 0x8);
+ if(tt_flagOpa && tt_aV[FADEOUT])
+ {
+ tt_tFade.EndTimer();
+ if(tt_opa)
+ {
+ var n = Math.round(tt_aV[FADEOUT] / (tt_aV[FADEINTERVAL] * (tt_aV[OPACITY] / tt_opa)));
+ tt_Fade(tt_opa, tt_opa, 0, n);
+ return;
+ }
+ }
+ tt_tHide.Timer("tt_Hide();", 1, false);
+ }
+}
+function tt_Hide()
+{
+ if(tt_db && tt_iState)
+ {
+ tt_OpReHref();
+ if(tt_iState & 0x2)
+ {
+ tt_aElt[0].style.visibility = "hidden";
+ tt_ExtCallFncs(0, "Hide");
+ }
+ tt_tShow.EndTimer();
+ tt_tHide.EndTimer();
+ tt_tDurt.EndTimer();
+ tt_tFade.EndTimer();
+ if(!tt_op && !tt_ie)
+ {
+ tt_tWaitMov.EndTimer();
+ tt_bWait = false;
+ }
+ if(tt_aV[CLICKCLOSE] || tt_aV[CLICKSTICKY])
+ tt_RemEvtFnc(document, "mouseup", tt_OnLClick);
+ tt_ExtCallFncs(0, "Kill");
+ // In case of a TagToTip tip, hide converted DOM node and
+ // re-insert it into DOM
+ if(tt_t2t && !tt_aV[COPYCONTENT])
+ tt_UnEl2Tip();
+ tt_iState = 0;
+ tt_over = null;
+ tt_ResetMainDiv();
+ if(tt_aElt[tt_aElt.length - 1])
+ tt_aElt[tt_aElt.length - 1].style.display = "none";
+ }
+}
+function tt_GetElt(id)
+{
+ return(document.getElementById ? document.getElementById(id)
+ : document.all ? document.all[id]
+ : null);
+}
+function tt_GetDivW(el)
+{
+ return(el ? (el.offsetWidth || el.style.pixelWidth || 0) : 0);
+}
+function tt_GetDivH(el)
+{
+ return(el ? (el.offsetHeight || el.style.pixelHeight || 0) : 0);
+}
+function tt_GetScrollX()
+{
+ return(window.pageXOffset || (tt_db ? (tt_db.scrollLeft || 0) : 0));
+}
+function tt_GetScrollY()
+{
+ return(window.pageYOffset || (tt_db ? (tt_db.scrollTop || 0) : 0));
+}
+function tt_GetClientW()
+{
+ return tt_GetWndCliSiz("Width");
+}
+function tt_GetClientH()
+{
+ return tt_GetWndCliSiz("Height");
+}
+function tt_GetEvtX(e)
+{
+ return (e ? ((typeof(e.pageX) != tt_u) ? e.pageX : (e.clientX + tt_GetScrollX())) : 0);
+}
+function tt_GetEvtY(e)
+{
+ return (e ? ((typeof(e.pageY) != tt_u) ? e.pageY : (e.clientY + tt_GetScrollY())) : 0);
+}
+function tt_AddEvtFnc(el, sEvt, PFnc)
+{
+ if(el)
+ {
+ if(el.addEventListener)
+ el.addEventListener(sEvt, PFnc, false);
+ else
+ el.attachEvent("on" + sEvt, PFnc);
+ }
+}
+function tt_RemEvtFnc(el, sEvt, PFnc)
+{
+ if(el)
+ {
+ if(el.removeEventListener)
+ el.removeEventListener(sEvt, PFnc, false);
+ else
+ el.detachEvent("on" + sEvt, PFnc);
+ }
+}
+function tt_GetDad(el)
+{
+ return(el.parentNode || el.parentElement || el.offsetParent);
+}
+function tt_MovDomNode(el, dadFrom, dadTo)
+{
+ if(dadFrom)
+ dadFrom.removeChild(el);
+ if(dadTo)
+ dadTo.appendChild(el);
+}
+
+//====================== PRIVATE ===========================================//
+var tt_aExt = new Array(), // Array of extension objects
+
+tt_db, tt_op, tt_ie, tt_ie56, tt_bBoxOld, // Browser flags
+tt_body,
+tt_ovr_, // HTML element the mouse is currently over
+tt_flagOpa, // Opacity support: 1=IE, 2=Khtml, 3=KHTML, 4=Moz, 5=W3C
+tt_maxPosX, tt_maxPosY,
+tt_iState = 0, // Tooltip active |= 1, shown |= 2, move with mouse |= 4, exclusive |= 8
+tt_opa, // Currently applied opacity
+tt_bJmpVert, tt_bJmpHorz,// Tip temporarily on other side of mouse
+tt_elDeHref, // The tag from which we've removed the href attribute
+// Timer
+tt_tShow = new Number(0), tt_tHide = new Number(0), tt_tDurt = new Number(0),
+tt_tFade = new Number(0), tt_tWaitMov = new Number(0),
+tt_bWait = false,
+tt_u = "undefined";
+
+
+function tt_Init()
+{
+ tt_MkCmdEnum();
+ // Send old browsers instantly to hell
+ if(!tt_Browser() || !tt_MkMainDiv())
+ return;
+ tt_IsW3cBox();
+ tt_OpaSupport();
+ tt_AddEvtFnc(document, "mousemove", tt_Move);
+ // In Debug mode we search for TagToTip() calls in order to notify
+ // the user if they've forgotten to set the TagsToTip config flag
+ if(TagsToTip || tt_Debug)
+ tt_SetOnloadFnc();
+ // Ensure the tip be hidden when the page unloads
+ tt_AddEvtFnc(window, "unload", tt_Hide);
+}
+// Creates command names by translating config variable names to upper case
+function tt_MkCmdEnum()
+{
+ var n = 0;
+ for(var i in config)
+ eval("window." + i.toString().toUpperCase() + " = " + n++);
+ tt_aV.length = n;
+}
+function tt_Browser()
+{
+ var n, nv, n6, w3c;
+
+ n = navigator.userAgent.toLowerCase(),
+ nv = navigator.appVersion;
+ tt_op = (document.defaultView && typeof(eval("w" + "indow" + "." + "o" + "p" + "er" + "a")) != tt_u);
+ tt_ie = n.indexOf("msie") != -1 && document.all && !tt_op;
+ if(tt_ie)
+ {
+ var ieOld = (!document.compatMode || document.compatMode == "BackCompat");
+ tt_db = !ieOld ? document.documentElement : (document.body || null);
+ if(tt_db)
+ tt_ie56 = parseFloat(nv.substring(nv.indexOf("MSIE") + 5)) >= 5.5
+ && typeof document.body.style.maxHeight == tt_u;
+ }
+ else
+ {
+ tt_db = document.documentElement || document.body ||
+ (document.getElementsByTagName ? document.getElementsByTagName("body")[0]
+ : null);
+ if(!tt_op)
+ {
+ n6 = document.defaultView && typeof document.defaultView.getComputedStyle != tt_u;
+ w3c = !n6 && document.getElementById;
+ }
+ }
+ tt_body = (document.getElementsByTagName ? document.getElementsByTagName("body")[0]
+ : (document.body || null));
+ if(tt_ie || n6 || tt_op || w3c)
+ {
+ if(tt_body && tt_db)
+ {
+ if(document.attachEvent || document.addEventListener)
+ return true;
+ }
+ else
+ tt_Err("wz_tooltip.js must be included INSIDE the body section,"
+ + " immediately after the opening <body> tag.", false);
+ }
+ tt_db = null;
+ return false;
+}
+function tt_MkMainDiv()
+{
+ // Create the tooltip DIV
+ if(tt_body.insertAdjacentHTML)
+ tt_body.insertAdjacentHTML("afterBegin", tt_MkMainDivHtm());
+ else if(typeof tt_body.innerHTML != tt_u && document.createElement && tt_body.appendChild)
+ tt_body.appendChild(tt_MkMainDivDom());
+ if(window.tt_GetMainDivRefs /* FireFox Alzheimer */ && tt_GetMainDivRefs())
+ return true;
+ tt_db = null;
+ return false;
+}
+function tt_MkMainDivHtm()
+{
+ return(
+ '<div id="WzTtDiV"></div>' +
+ (tt_ie56 ? ('<iframe id="WzTtIfRm" src="javascript:false" scrolling="no" frameborder="0" style="filter:Alpha(opacity=0);position:absolute;top:0px;left:0px;display:none;"></iframe>')
+ : '')
+ );
+}
+function tt_MkMainDivDom()
+{
+ var el = document.createElement("div");
+ if(el)
+ el.id = "WzTtDiV";
+ return el;
+}
+function tt_GetMainDivRefs()
+{
+ tt_aElt[0] = tt_GetElt("WzTtDiV");
+ if(tt_ie56 && tt_aElt[0])
+ {
+ tt_aElt[tt_aElt.length - 1] = tt_GetElt("WzTtIfRm");
+ if(!tt_aElt[tt_aElt.length - 1])
+ tt_aElt[0] = null;
+ }
+ if(tt_aElt[0])
+ {
+ var css = tt_aElt[0].style;
+
+ css.visibility = "hidden";
+ css.position = "absolute";
+ css.overflow = "hidden";
+ return true;
+ }
+ return false;
+}
+function tt_ResetMainDiv()
+{
+ tt_SetTipPos(0, 0);
+ tt_aElt[0].innerHTML = "";
+ tt_aElt[0].style.width = "0px";
+ tt_h = 0;
+}
+function tt_IsW3cBox()
+{
+ var css = tt_aElt[0].style;
+
+ css.padding = "10px";
+ css.width = "40px";
+ tt_bBoxOld = (tt_GetDivW(tt_aElt[0]) == 40);
+ css.padding = "0px";
+ tt_ResetMainDiv();
+}
+function tt_OpaSupport()
+{
+ var css = tt_body.style;
+
+ tt_flagOpa = (typeof(css.KhtmlOpacity) != tt_u) ? 2
+ : (typeof(css.KHTMLOpacity) != tt_u) ? 3
+ : (typeof(css.MozOpacity) != tt_u) ? 4
+ : (typeof(css.opacity) != tt_u) ? 5
+ : (typeof(css.filter) != tt_u) ? 1
+ : 0;
+}
+// Ported from http://dean.edwards.name/weblog/2006/06/again/
+// (Dean Edwards et al.)
+function tt_SetOnloadFnc()
+{
+ tt_AddEvtFnc(document, "DOMContentLoaded", tt_HideSrcTags);
+ tt_AddEvtFnc(window, "load", tt_HideSrcTags);
+ if(tt_body.attachEvent)
+ tt_body.attachEvent("onreadystatechange",
+ function() {
+ if(tt_body.readyState == "complete")
+ tt_HideSrcTags();
+ } );
+ if(/WebKit|KHTML/i.test(navigator.userAgent))
+ {
+ var t = setInterval(function() {
+ if(/loaded|complete/.test(document.readyState))
+ {
+ clearInterval(t);
+ tt_HideSrcTags();
+ }
+ }, 10);
+ }
+}
+function tt_HideSrcTags()
+{
+ if(!window.tt_HideSrcTags || window.tt_HideSrcTags.done)
+ return;
+ window.tt_HideSrcTags.done = true;
+ if(!tt_HideSrcTagsRecurs(tt_body))
+ tt_Err("There are HTML elements to be converted to tooltips.\nIf you"
+ + " want these HTML elements to be automatically hidden, you"
+ + " must edit wz_tooltip.js, and set TagsToTip in the global"
+ + " tooltip configuration to true.", true);
+}
+function tt_HideSrcTagsRecurs(dad)
+{
+ var ovr, asT2t;
+ // Walk the DOM tree for tags that have an onmouseover or onclick attribute
+ // containing a TagToTip('...') call.
+ // (.childNodes first since .children is bugous in Safari)
+ var a = dad.childNodes || dad.children || null;
+
+ for(var i = a ? a.length : 0; i;)
+ {--i;
+ if(!tt_HideSrcTagsRecurs(a[i]))
+ return false;
+ ovr = a[i].getAttribute ? (a[i].getAttribute("onmouseover") || a[i].getAttribute("onclick"))
+ : (typeof a[i].onmouseover == "function") ? (a[i].onmouseover || a[i].onclick)
+ : null;
+ if(ovr)
+ {
+ asT2t = ovr.toString().match(/TagToTip\s*\(\s*'[^'.]+'\s*[\),]/);
+ if(asT2t && asT2t.length)
+ {
+ if(!tt_HideSrcTag(asT2t[0]))
+ return false;
+ }
+ }
+ }
+ return true;
+}
+function tt_HideSrcTag(sT2t)
+{
+ var id, el;
+
+ // The ID passed to the found TagToTip() call identifies an HTML element
+ // to be converted to a tooltip, so hide that element
+ id = sT2t.replace(/.+'([^'.]+)'.+/, "$1");
+ el = tt_GetElt(id);
+ if(el)
+ {
+ if(tt_Debug && !TagsToTip)
+ return false;
+ else
+ el.style.display = "none";
+ }
+ else
+ tt_Err("Invalid ID\n'" + id + "'\npassed to TagToTip()."
+ + " There exists no HTML element with that ID.", true);
+ return true;
+}
+function tt_Tip(arg, t2t)
+{
+ if(!tt_db || (tt_iState & 0x8))
+ return;
+ if(tt_iState)
+ tt_Hide();
+ if(!tt_Enabled)
+ return;
+ tt_t2t = t2t;
+ if(!tt_ReadCmds(arg))
+ return;
+ tt_iState = 0x1 | 0x4;
+ tt_AdaptConfig1();
+ tt_MkTipContent(arg);
+ tt_MkTipSubDivs();
+ tt_FormatTip();
+ tt_bJmpVert = false;
+ tt_bJmpHorz = false;
+ tt_maxPosX = tt_GetClientW() + tt_GetScrollX() - tt_w - 1;
+ tt_maxPosY = tt_GetClientH() + tt_GetScrollY() - tt_h - 1;
+ tt_AdaptConfig2();
+ // Ensure the tip be shown and positioned before the first onmousemove
+ tt_OverInit();
+ tt_ShowInit();
+ tt_Move();
+}
+function tt_ReadCmds(a)
+{
+ var i;
+
+ // First load the global config values, to initialize also values
+ // for which no command is passed
+ i = 0;
+ for(var j in config)
+ tt_aV[i++] = config[j];
+ // Then replace each cached config value for which a command is
+ // passed (ensure the # of command args plus value args be even)
+ if(a.length & 1)
+ {
+ for(i = a.length - 1; i > 0; i -= 2)
+ tt_aV[a[i - 1]] = a[i];
+ return true;
+ }
+ tt_Err("Incorrect call of Tip() or TagToTip().\n"
+ + "Each command must be followed by a value.", true);
+ return false;
+}
+function tt_AdaptConfig1()
+{
+ tt_ExtCallFncs(0, "LoadConfig");
+ // Inherit unspecified title formattings from body
+ if(!tt_aV[TITLEBGCOLOR].length)
+ tt_aV[TITLEBGCOLOR] = tt_aV[BORDERCOLOR];
+ if(!tt_aV[TITLEFONTCOLOR].length)
+ tt_aV[TITLEFONTCOLOR] = tt_aV[BGCOLOR];
+ if(!tt_aV[TITLEFONTFACE].length)
+ tt_aV[TITLEFONTFACE] = tt_aV[FONTFACE];
+ if(!tt_aV[TITLEFONTSIZE].length)
+ tt_aV[TITLEFONTSIZE] = tt_aV[FONTSIZE];
+ if(tt_aV[CLOSEBTN])
+ {
+ // Use title colours for non-specified closebutton colours
+ if(!tt_aV[CLOSEBTNCOLORS])
+ tt_aV[CLOSEBTNCOLORS] = new Array("", "", "", "");
+ for(var i = 4; i;)
+ {--i;
+ if(!tt_aV[CLOSEBTNCOLORS][i].length)
+ tt_aV[CLOSEBTNCOLORS][i] = (i & 1) ? tt_aV[TITLEFONTCOLOR] : tt_aV[TITLEBGCOLOR];
+ }
+ // Enforce titlebar be shown
+ if(!tt_aV[TITLE].length)
+ tt_aV[TITLE] = " ";
+ }
+ // Circumvents broken display of images and fade-in flicker in Geckos < 1.8
+ if(tt_aV[OPACITY] == 100 && typeof tt_aElt[0].style.MozOpacity != tt_u && !Array.every)
+ tt_aV[OPACITY] = 99;
+ // Smartly shorten the delay for fade-in tooltips
+ if(tt_aV[FADEIN] && tt_flagOpa && tt_aV[DELAY] > 100)
+ tt_aV[DELAY] = Math.max(tt_aV[DELAY] - tt_aV[FADEIN], 100);
+}
+function tt_AdaptConfig2()
+{
+ if(tt_aV[CENTERMOUSE])
+ {
+ tt_aV[OFFSETX] -= ((tt_w - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0)) >> 1);
+ tt_aV[JUMPHORZ] = false;
+ }
+}
+// Expose content globally so extensions can modify it
+function tt_MkTipContent(a)
+{
+ if(tt_t2t)
+ {
+ if(tt_aV[COPYCONTENT])
+ tt_sContent = tt_t2t.innerHTML;
+ else
+ tt_sContent = "";
+ }
+ else
+ tt_sContent = a[0];
+ tt_ExtCallFncs(0, "CreateContentString");
+}
+function tt_MkTipSubDivs()
+{
+ var sCss = 'position:relative;margin:0px;padding:0px;border-width:0px;left:0px;top:0px;line-height:normal;width:auto;',
+ sTbTrTd = ' cellspacing="0" cellpadding="0" border="0" style="' + sCss + '"><tbody style="' + sCss + '"><tr><td ';
+
+ tt_aElt[0].style.width = tt_GetClientW() + "px";
+ tt_aElt[0].innerHTML =
+ (''
+ + (tt_aV[TITLE].length ?
+ ('<div id="WzTiTl" style="position:relative;z-index:1;">'
+ + '<table id="WzTiTlTb"' + sTbTrTd + 'id="WzTiTlI" style="' + sCss + '">'
+ + tt_aV[TITLE]
+ + '</td>'
+ + (tt_aV[CLOSEBTN] ?
+ ('<td align="right" style="' + sCss
+ + 'text-align:right;">'
+ + '<span id="WzClOsE" style="position:relative;left:2px;padding-left:2px;padding-right:2px;'
+ + 'cursor:' + (tt_ie ? 'hand' : 'pointer')
+ + ';" onmouseover="tt_OnCloseBtnOver(1)" onmouseout="tt_OnCloseBtnOver(0)" onclick="tt_HideInit()">'
+ + tt_aV[CLOSEBTNTEXT]
+ + '</span></td>')
+ : '')
+ + '</tr></tbody></table></div>')
+ : '')
+ + '<div id="WzBoDy" style="position:relative;z-index:0;">'
+ + '<table' + sTbTrTd + 'id="WzBoDyI" style="' + sCss + '">'
+ + tt_sContent
+ + '</td></tr></tbody></table></div>'
+ + (tt_aV[SHADOW]
+ ? ('<div id="WzTtShDwR" style="position:absolute;overflow:hidden;"></div>'
+ + '<div id="WzTtShDwB" style="position:relative;overflow:hidden;"></div>')
+ : '')
+ );
+ tt_GetSubDivRefs();
+ // Convert DOM node to tip
+ if(tt_t2t && !tt_aV[COPYCONTENT])
+ tt_El2Tip();
+ tt_ExtCallFncs(0, "SubDivsCreated");
+}
+function tt_GetSubDivRefs()
+{
+ var aId = new Array("WzTiTl", "WzTiTlTb", "WzTiTlI", "WzClOsE", "WzBoDy", "WzBoDyI", "WzTtShDwB", "WzTtShDwR");
+
+ for(var i = aId.length; i; --i)
+ tt_aElt[i] = tt_GetElt(aId[i - 1]);
+}
+function tt_FormatTip()
+{
+ var css, w, h, pad = tt_aV[PADDING], padT, wBrd = tt_aV[BORDERWIDTH],
+ iOffY, iOffSh, iAdd = (pad + wBrd) << 1;
+
+ //--------- Title DIV ----------
+ if(tt_aV[TITLE].length)
+ {
+ padT = tt_aV[TITLEPADDING];
+ css = tt_aElt[1].style;
+ css.background = tt_aV[TITLEBGCOLOR];
+ css.paddingTop = css.paddingBottom = padT + "px";
+ css.paddingLeft = css.paddingRight = (padT + 2) + "px";
+ css = tt_aElt[3].style;
+ css.color = tt_aV[TITLEFONTCOLOR];
+ if(tt_aV[WIDTH] == -1)
+ css.whiteSpace = "nowrap";
+ css.fontFamily = tt_aV[TITLEFONTFACE];
+ css.fontSize = tt_aV[TITLEFONTSIZE];
+ css.fontWeight = "bold";
+ css.textAlign = tt_aV[TITLEALIGN];
+ // Close button DIV
+ if(tt_aElt[4])
+ {
+ css = tt_aElt[4].style;
+ css.background = tt_aV[CLOSEBTNCOLORS][0];
+ css.color = tt_aV[CLOSEBTNCOLORS][1];
+ css.fontFamily = tt_aV[TITLEFONTFACE];
+ css.fontSize = tt_aV[TITLEFONTSIZE];
+ css.fontWeight = "bold";
+ }
+ if(tt_aV[WIDTH] > 0)
+ tt_w = tt_aV[WIDTH];
+ else
+ {
+ tt_w = tt_GetDivW(tt_aElt[3]) + tt_GetDivW(tt_aElt[4]);
+ // Some spacing between title DIV and closebutton
+ if(tt_aElt[4])
+ tt_w += pad;
+ // Restrict auto width to max width
+ if(tt_aV[WIDTH] < -1 && tt_w > -tt_aV[WIDTH])
+ tt_w = -tt_aV[WIDTH];
+ }
+ // Ensure the top border of the body DIV be covered by the title DIV
+ iOffY = -wBrd;
+ }
+ else
+ {
+ tt_w = 0;
+ iOffY = 0;
+ }
+
+ //-------- Body DIV ------------
+ css = tt_aElt[5].style;
+ css.top = iOffY + "px";
+ if(wBrd)
+ {
+ css.borderColor = tt_aV[BORDERCOLOR];
+ css.borderStyle = tt_aV[BORDERSTYLE];
+ css.borderWidth = wBrd + "px";
+ }
+ if(tt_aV[BGCOLOR].length)
+ css.background = tt_aV[BGCOLOR];
+ if(tt_aV[BGIMG].length)
+ css.backgroundImage = "url(" + tt_aV[BGIMG] + ")";
+ css.padding = pad + "px";
+ css.textAlign = tt_aV[TEXTALIGN];
+ if(tt_aV[HEIGHT])
+ {
+ css.overflow = "auto";
+ if(tt_aV[HEIGHT] > 0)
+ css.height = (tt_aV[HEIGHT] + iAdd) + "px";
+ else
+ tt_h = iAdd - tt_aV[HEIGHT];
+ }
+ // TD inside body DIV
+ css = tt_aElt[6].style;
+ css.color = tt_aV[FONTCOLOR];
+ css.fontFamily = tt_aV[FONTFACE];
+ css.fontSize = tt_aV[FONTSIZE];
+ css.fontWeight = tt_aV[FONTWEIGHT];
+ css.textAlign = tt_aV[TEXTALIGN];
+ if(tt_aV[WIDTH] > 0)
+ w = tt_aV[WIDTH];
+ // Width like title (if existent)
+ else if(tt_aV[WIDTH] == -1 && tt_w)
+ w = tt_w;
+ else
+ {
+ // Measure width of the body's inner TD, as some browsers would expand
+ // the container and outer body DIV to 100%
+ w = tt_GetDivW(tt_aElt[6]);
+ // Restrict auto width to max width
+ if(tt_aV[WIDTH] < -1 && w > -tt_aV[WIDTH])
+ w = -tt_aV[WIDTH];
+ }
+ if(w > tt_w)
+ tt_w = w;
+ tt_w += iAdd;
+
+ //--------- Shadow DIVs ------------
+ if(tt_aV[SHADOW])
+ {
+ tt_w += tt_aV[SHADOWWIDTH];
+ iOffSh = Math.floor((tt_aV[SHADOWWIDTH] * 4) / 3);
+ // Bottom shadow
+ css = tt_aElt[7].style;
+ css.top = iOffY + "px";
+ css.left = iOffSh + "px";
+ css.width = (tt_w - iOffSh - tt_aV[SHADOWWIDTH]) + "px";
+ css.height = tt_aV[SHADOWWIDTH] + "px";
+ css.background = tt_aV[SHADOWCOLOR];
+ // Right shadow
+ css = tt_aElt[8].style;
+ css.top = iOffSh + "px";
+ css.left = (tt_w - tt_aV[SHADOWWIDTH]) + "px";
+ css.width = tt_aV[SHADOWWIDTH] + "px";
+ css.background = tt_aV[SHADOWCOLOR];
+ }
+ else
+ iOffSh = 0;
+
+ //-------- Container DIV -------
+ tt_SetTipOpa(tt_aV[FADEIN] ? 0 : tt_aV[OPACITY]);
+ tt_FixSize(iOffY, iOffSh);
+}
+// Fixate the size so it can't dynamically change while the tooltip is moving.
+function tt_FixSize(iOffY, iOffSh)
+{
+ var wIn, wOut, h, add, pad = tt_aV[PADDING], wBrd = tt_aV[BORDERWIDTH], i;
+
+ tt_aElt[0].style.width = tt_w + "px";
+ tt_aElt[0].style.pixelWidth = tt_w;
+ wOut = tt_w - ((tt_aV[SHADOW]) ? tt_aV[SHADOWWIDTH] : 0);
+ // Body
+ wIn = wOut;
+ if(!tt_bBoxOld)
+ wIn -= (pad + wBrd) << 1;
+ tt_aElt[5].style.width = wIn + "px";
+ // Title
+ if(tt_aElt[1])
+ {
+ wIn = wOut - ((tt_aV[TITLEPADDING] + 2) << 1);
+ if(!tt_bBoxOld)
+ wOut = wIn;
+ tt_aElt[1].style.width = wOut + "px";
+ tt_aElt[2].style.width = wIn + "px";
+ }
+ // Max height specified
+ if(tt_h)
+ {
+ h = tt_GetDivH(tt_aElt[5]);
+ if(h > tt_h)
+ {
+ if(!tt_bBoxOld)
+ tt_h -= (pad + wBrd) << 1;
+ tt_aElt[5].style.height = tt_h + "px";
+ }
+ }
+ tt_h = tt_GetDivH(tt_aElt[0]) + iOffY;
+ // Right shadow
+ if(tt_aElt[8])
+ tt_aElt[8].style.height = (tt_h - iOffSh) + "px";
+ i = tt_aElt.length - 1;
+ if(tt_aElt[i])
+ {
+ tt_aElt[i].style.width = tt_w + "px";
+ tt_aElt[i].style.height = tt_h + "px";
+ }
+}
+function tt_DeAlt(el)
+{
+ var aKid;
+
+ if(el)
+ {
+ if(el.alt)
+ el.alt = "";
+ if(el.title)
+ el.title = "";
+ aKid = el.childNodes || el.children || null;
+ if(aKid)
+ {
+ for(var i = aKid.length; i;)
+ tt_DeAlt(aKid[--i]);
+ }
+ }
+}
+// This hack removes the native tooltips over links in Opera
+function tt_OpDeHref(el)
+{
+ if(!tt_op)
+ return;
+ if(tt_elDeHref)
+ tt_OpReHref();
+ while(el)
+ {
+ if(el.hasAttribute && el.hasAttribute("href"))
+ {
+ el.t_href = el.getAttribute("href");
+ el.t_stats = window.status;
+ el.removeAttribute("href");
+ el.style.cursor = "hand";
+ tt_AddEvtFnc(el, "mousedown", tt_OpReHref);
+ window.status = el.t_href;
+ tt_elDeHref = el;
+ break;
+ }
+ el = tt_GetDad(el);
+ }
+}
+function tt_OpReHref()
+{
+ if(tt_elDeHref)
+ {
+ tt_elDeHref.setAttribute("href", tt_elDeHref.t_href);
+ tt_RemEvtFnc(tt_elDeHref, "mousedown", tt_OpReHref);
+ window.status = tt_elDeHref.t_stats;
+ tt_elDeHref = null;
+ }
+}
+function tt_El2Tip()
+{
+ var css = tt_t2t.style;
+
+ // Store previous positioning
+ tt_t2t.t_cp = css.position;
+ tt_t2t.t_cl = css.left;
+ tt_t2t.t_ct = css.top;
+ tt_t2t.t_cd = css.display;
+ // Store the tag's parent element so we can restore that DOM branch
+ // when the tooltip is being hidden
+ tt_t2tDad = tt_GetDad(tt_t2t);
+ tt_MovDomNode(tt_t2t, tt_t2tDad, tt_aElt[6]);
+ css.display = "block";
+ css.position = "static";
+ css.left = css.top = css.marginLeft = css.marginTop = "0px";
+}
+function tt_UnEl2Tip()
+{
+ // Restore positioning and display
+ var css = tt_t2t.style;
+
+ css.display = tt_t2t.t_cd;
+ tt_MovDomNode(tt_t2t, tt_GetDad(tt_t2t), tt_t2tDad);
+ css.position = tt_t2t.t_cp;
+ css.left = tt_t2t.t_cl;
+ css.top = tt_t2t.t_ct;
+ tt_t2tDad = null;
+}
+function tt_OverInit()
+{
+ if(window.event)
+ tt_over = window.event.target || window.event.srcElement;
+ else
+ tt_over = tt_ovr_;
+ tt_DeAlt(tt_over);
+ tt_OpDeHref(tt_over);
+}
+function tt_ShowInit()
+{
+ tt_tShow.Timer("tt_Show()", tt_aV[DELAY], true);
+ if(tt_aV[CLICKCLOSE] || tt_aV[CLICKSTICKY])
+ tt_AddEvtFnc(document, "mouseup", tt_OnLClick);
+}
+function tt_Show()
+{
+ var css = tt_aElt[0].style;
+
+ // Override the z-index of the topmost wz_dragdrop.js D&D item
+ css.zIndex = Math.max((window.dd && dd.z) ? (dd.z + 2) : 0, 1010);
+ if(tt_aV[STICKY] || !tt_aV[FOLLOWMOUSE])
+ tt_iState &= ~0x4;
+ if(tt_aV[EXCLUSIVE])
+ tt_iState |= 0x8;
+ if(tt_aV[DURATION] > 0)
+ tt_tDurt.Timer("tt_HideInit()", tt_aV[DURATION], true);
+ tt_ExtCallFncs(0, "Show")
+ css.visibility = "visible";
+ tt_iState |= 0x2;
+ if(tt_aV[FADEIN])
+ tt_Fade(0, 0, tt_aV[OPACITY], Math.round(tt_aV[FADEIN] / tt_aV[FADEINTERVAL]));
+ tt_ShowIfrm();
+}
+function tt_ShowIfrm()
+{
+ if(tt_ie56)
+ {
+ var ifrm = tt_aElt[tt_aElt.length - 1];
+ if(ifrm)
+ {
+ var css = ifrm.style;
+ css.zIndex = tt_aElt[0].style.zIndex - 1;
+ css.display = "block";
+ }
+ }
+}
+function tt_Move(e)
+{
+ if(e)
+ tt_ovr_ = e.target || e.srcElement;
+ e = e || window.event;
+ if(e)
+ {
+ tt_musX = tt_GetEvtX(e);
+ tt_musY = tt_GetEvtY(e);
+ }
+ if(tt_iState & 0x4)
+ {
+ // Prevent jam of mousemove events
+ if(!tt_op && !tt_ie)
+ {
+ if(tt_bWait)
+ return;
+ tt_bWait = true;
+ tt_tWaitMov.Timer("tt_bWait = false;", 1, true);
+ }
+ if(tt_aV[FIX])
+ {
+ tt_iState &= ~0x4;
+ tt_PosFix();
+ }
+ else if(!tt_ExtCallFncs(e, "MoveBefore"))
+ tt_SetTipPos(tt_Pos(0), tt_Pos(1));
+ tt_ExtCallFncs([tt_musX, tt_musY], "MoveAfter")
+ }
+}
+function tt_Pos(iDim)
+{
+ var iX, bJmpMod, cmdAlt, cmdOff, cx, iMax, iScrl, iMus, bJmp;
+
+ // Map values according to dimension to calculate
+ if(iDim)
+ {
+ bJmpMod = tt_aV[JUMPVERT];
+ cmdAlt = ABOVE;
+ cmdOff = OFFSETY;
+ cx = tt_h;
+ iMax = tt_maxPosY;
+ iScrl = tt_GetScrollY();
+ iMus = tt_musY;
+ bJmp = tt_bJmpVert;
+ }
+ else
+ {
+ bJmpMod = tt_aV[JUMPHORZ];
+ cmdAlt = LEFT;
+ cmdOff = OFFSETX;
+ cx = tt_w;
+ iMax = tt_maxPosX;
+ iScrl = tt_GetScrollX();
+ iMus = tt_musX;
+ bJmp = tt_bJmpHorz;
+ }
+ if(bJmpMod)
+ {
+ if(tt_aV[cmdAlt] && (!bJmp || tt_CalcPosAlt(iDim) >= iScrl + 16))
+ iX = tt_PosAlt(iDim);
+ else if(!tt_aV[cmdAlt] && bJmp && tt_CalcPosDef(iDim) > iMax - 16)
+ iX = tt_PosAlt(iDim);
+ else
+ iX = tt_PosDef(iDim);
+ }
+ else
+ {
+ iX = iMus;
+ if(tt_aV[cmdAlt])
+ iX -= cx + tt_aV[cmdOff] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0);
+ else
+ iX += tt_aV[cmdOff];
+ }
+ // Prevent tip from extending past clientarea boundary
+ if(iX > iMax)
+ iX = bJmpMod ? tt_PosAlt(iDim) : iMax;
+ // In case of insufficient space on both sides, ensure the left/upper part
+ // of the tip be visible
+ if(iX < iScrl)
+ iX = bJmpMod ? tt_PosDef(iDim) : iScrl;
+ return iX;
+}
+function tt_PosDef(iDim)
+{
+ if(iDim)
+ tt_bJmpVert = tt_aV[ABOVE];
+ else
+ tt_bJmpHorz = tt_aV[LEFT];
+ return tt_CalcPosDef(iDim);
+}
+function tt_PosAlt(iDim)
+{
+ if(iDim)
+ tt_bJmpVert = !tt_aV[ABOVE];
+ else
+ tt_bJmpHorz = !tt_aV[LEFT];
+ return tt_CalcPosAlt(iDim);
+}
+function tt_CalcPosDef(iDim)
+{
+ return iDim ? (tt_musY + tt_aV[OFFSETY]) : (tt_musX + tt_aV[OFFSETX]);
+}
+function tt_CalcPosAlt(iDim)
+{
+ var cmdOff = iDim ? OFFSETY : OFFSETX;
+ var dx = tt_aV[cmdOff] - (tt_aV[SHADOW] ? tt_aV[SHADOWWIDTH] : 0);
+ if(tt_aV[cmdOff] > 0 && dx <= 0)
+ dx = 1;
+ return((iDim ? (tt_musY - tt_h) : (tt_musX - tt_w)) - dx);
+}
+function tt_PosFix()
+{
+ var iX, iY;
+
+ if(typeof(tt_aV[FIX][0]) == "number")
+ {
+ iX = tt_aV[FIX][0];
+ iY = tt_aV[FIX][1];
+ }
+ else
+ {
+ if(typeof(tt_aV[FIX][0]) == "string")
+ el = tt_GetElt(tt_aV[FIX][0]);
+ // First slot in array is direct reference to HTML element
+ else
+ el = tt_aV[FIX][0];
+ iX = tt_aV[FIX][1];
+ iY = tt_aV[FIX][2];
+ // By default, vert pos is related to bottom edge of HTML element
+ if(!tt_aV[ABOVE] && el)
+ iY += tt_GetDivH(el);
+ for(; el; el = el.offsetParent)
+ {
+ iX += el.offsetLeft || 0;
+ iY += el.offsetTop || 0;
+ }
+ }
+ // For a fixed tip positioned above the mouse, use the bottom edge as anchor
+ // (recommended by Christophe Rebeschini, 31.1.2008)
+ if(tt_aV[ABOVE])
+ iY -= tt_h;
+ tt_SetTipPos(iX, iY);
+}
+function tt_Fade(a, now, z, n)
+{
+ if(n)
+ {
+ now += Math.round((z - now) / n);
+ if((z > a) ? (now >= z) : (now <= z))
+ now = z;
+ else
+ tt_tFade.Timer(
+ "tt_Fade("
+ + a + "," + now + "," + z + "," + (n - 1)
+ + ")",
+ tt_aV[FADEINTERVAL],
+ true
+ );
+ }
+ now ? tt_SetTipOpa(now) : tt_Hide();
+}
+function tt_SetTipOpa(opa)
+{
+ // To circumvent the opacity nesting flaws of IE, we set the opacity
+ // for each sub-DIV separately, rather than for the container DIV.
+ tt_SetOpa(tt_aElt[5], opa);
+ if(tt_aElt[1])
+ tt_SetOpa(tt_aElt[1], opa);
+ if(tt_aV[SHADOW])
+ {
+ opa = Math.round(opa * 0.8);
+ tt_SetOpa(tt_aElt[7], opa);
+ tt_SetOpa(tt_aElt[8], opa);
+ }
+}
+function tt_OnCloseBtnOver(iOver)
+{
+ var css = tt_aElt[4].style;
+
+ iOver <<= 1;
+ css.background = tt_aV[CLOSEBTNCOLORS][iOver];
+ css.color = tt_aV[CLOSEBTNCOLORS][iOver + 1];
+}
+function tt_OnLClick(e)
+{
+ // Ignore right-clicks
+ e = e || window.event;
+ if(!((e.button && e.button & 2) || (e.which && e.which == 3)))
+ {
+ if(tt_aV[CLICKSTICKY] && (tt_iState & 0x4))
+ {
+ tt_aV[STICKY] = true;
+ tt_iState &= ~0x4;
+ }
+ else if(tt_aV[CLICKCLOSE])
+ tt_HideInit();
+ }
+}
+function tt_Int(x)
+{
+ var y;
+
+ return(isNaN(y = parseInt(x)) ? 0 : y);
+}
+Number.prototype.Timer = function(s, iT, bUrge)
+{
+ if(!this.value || bUrge)
+ this.value = window.setTimeout(s, iT);
+}
+Number.prototype.EndTimer = function()
+{
+ if(this.value)
+ {
+ window.clearTimeout(this.value);
+ this.value = 0;
+ }
+}
+function tt_GetWndCliSiz(s)
+{
+ var db, y = window["inner" + s], sC = "client" + s, sN = "number";
+ if(typeof y == sN)
+ {
+ var y2;
+ return(
+ // Gecko or Opera with scrollbar
+ // ... quirks mode
+ ((db = document.body) && typeof(y2 = db[sC]) == sN && y2 && y2 <= y) ? y2
+ // ... strict mode
+ : ((db = document.documentElement) && typeof(y2 = db[sC]) == sN && y2 && y2 <= y) ? y2
+ // No scrollbar, or clientarea size == 0, or other browser (KHTML etc.)
+ : y
+ );
+ }
+ // IE
+ return(
+ // document.documentElement.client+s functional, returns > 0
+ ((db = document.documentElement) && (y = db[sC])) ? y
+ // ... not functional, in which case document.body.client+s
+ // is the clientarea size, fortunately
+ : document.body[sC]
+ );
+}
+function tt_SetOpa(el, opa)
+{
+ var css = el.style;
+
+ tt_opa = opa;
+ if(tt_flagOpa == 1)
+ {
+ if(opa < 100)
+ {
+ // Hacks for bugs of IE:
+ // 1.) Once a CSS filter has been applied, fonts are no longer
+ // anti-aliased, so we store the previous 'non-filter' to be
+ // able to restore it
+ if(typeof(el.filtNo) == tt_u)
+ el.filtNo = css.filter;
+ // 2.) A DIV cannot be made visible in a single step if an
+ // opacity < 100 has been applied while the DIV was hidden
+ var bVis = css.visibility != "hidden";
+ // 3.) In IE6, applying an opacity < 100 has no effect if the
+ // element has no layout (position, size, zoom, ...)
+ css.zoom = "100%";
+ if(!bVis)
+ css.visibility = "visible";
+ css.filter = "alpha(opacity=" + opa + ")";
+ if(!bVis)
+ css.visibility = "hidden";
+ }
+ else if(typeof(el.filtNo) != tt_u)
+ // Restore 'non-filter'
+ css.filter = el.filtNo;
+ }
+ else
+ {
+ opa /= 100.0;
+ switch(tt_flagOpa)
+ {
+ case 2:
+ css.KhtmlOpacity = opa; break;
+ case 3:
+ css.KHTMLOpacity = opa; break;
+ case 4:
+ css.MozOpacity = opa; break;
+ case 5:
+ css.opacity = opa; break;
+ }
+ }
+}
+function tt_Err(sErr, bIfDebug)
+{
+ if(tt_Debug || !bIfDebug)
+ alert("Tooltip Script Error Message:\n\n" + sErr);
+}
+
+//============ EXTENSION (PLUGIN) MANAGER ===============//
+function tt_ExtCmdEnum()
+{
+ var s;
+
+ // Add new command(s) to the commands enum
+ for(var i in config)
+ {
+ s = "window." + i.toString().toUpperCase();
+ if(eval("typeof(" + s + ") == tt_u"))
+ {
+ eval(s + " = " + tt_aV.length);
+ tt_aV[tt_aV.length] = null;
+ }
+ }
+}
+function tt_ExtCallFncs(arg, sFnc)
+{
+ var b = false;
+ for(var i = tt_aExt.length; i;)
+ {--i;
+ var fnc = tt_aExt[i]["On" + sFnc];
+ // Call the method the extension has defined for this event
+ if(fnc && fnc(arg))
+ b = true;
+ }
+ return b;
+}
+
+tt_Init();
diff --git a/subsonic-main/src/main/webapp/style/barents.css b/subsonic-main/src/main/webapp/style/barents.css
new file mode 100644
index 00000000..75fd3323
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/barents.css
@@ -0,0 +1,30 @@
+/*
+ * CSS styleshet for the "Barents Sea" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "hd.css";
+
+body {
+ margin:0.75em;
+ font-size: 9pt;
+}
+
+.detail, .albumComment, .log {
+ font-size: 8pt;
+}
+
+/* Font 1 */
+body, h2, form, label, table, a {
+ font-family: verdana, arial, sans-serif;
+}
+
+/* Font 2 */
+h1, h1 a, input, select {
+ font-family: arial, sans-serif;
+}
+
+h1, h2, th {
+ font-weight:bold;
+}
diff --git a/subsonic-main/src/main/webapp/style/black.css b/subsonic-main/src/main/webapp/style/black.css
new file mode 100644
index 00000000..a28d8d93
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/black.css
@@ -0,0 +1,17 @@
+/*
+ * CSS styleshet for the "Back In Black" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "midnight.css";
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: #333333
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: black
+}
diff --git a/subsonic-main/src/main/webapp/style/buuftheme.css b/subsonic-main/src/main/webapp/style/buuftheme.css
new file mode 100644
index 00000000..5f84f239
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/buuftheme.css
@@ -0,0 +1,120 @@
+/*
+ * CSS styleshet for the "Cool and Clean" theme.
+ *
+ * Author: Dan Eriksen (dan.o.eriksen[at]gmail.com)
+ *
+ * Icons used are released under Creative Commons.
+ * Heading icons are part of the Albook icon pack created by Laurent Baumann (http://lbaumann.com/)
+ * Main theme icon part of itunes icon pack created by Michael Flarup (http://pixelresort.com)
+ *
+ *
+ * Edited for BUUF Theme by Fractal Systems
+ * BUUF artwork: Based on icons by Paul Davey aka Mattahan. All rights reserved.
+ */
+
+@import "default.css";
+
+/* The primary background colour, light gray. Images used to force background on left frame headings and playlist. */
+.bgcolor1 {
+ background-image:url( "../icons/buuftheme/list_heading.png" );
+ background-repeat:repeat;
+ background-color: #3C2D22;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #4C4039;
+}
+
+/* Background for selected headers. */
+.headerSelected {
+ background-image:url( "../icons/buuftheme/home_hover.png" );
+ background-repeat:repeat;
+ color: #333333;
+}
+
+
+/* Foreground colour used for h1, h2, b, tr, details and albumcomments. */
+h1, h2, b, tr, .detail, .albumComment {
+ color: #ffAE00;
+}
+
+/* Table sizing */
+table {
+ font-size: 100%;
+ line-height: 130%;
+ padding: 0;
+ border: 0;
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-image:url( "../icons/buuftheme/background_main.png" );
+ background-repeat:no-repeat;
+ background-attachment:fixed;
+ background-color: #4C4039;
+ background-position:right;
+}
+
+/* Right frame background. */
+.rightframe {
+ background-color: #4C4039;
+ background-image:none;
+}
+
+/* Back image */
+.back {
+ background-image:url( "../icons/buuftheme/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/buuftheme/forward.png" );
+}
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #ffAE00;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: none;
+ color: #169F30;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #AA1F15;
+}
+
+/* The primary foreground colour. */
+body {
+ background-color: #D1D9E1;
+ scrollbar-arrow-color: #636363;
+ scrollbar-track-color: #D1D9E1;
+}
+
+/* The album comments. */
+.albumComment {
+ width: 50em;
+ font-size: 8pt;
+ line-height: 1.4em;
+ padding-top: 0.25em;
+}
+
+/* The log. */
+.log {
+ font-weight: bold;
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1em;
+}
+
+/* The help, status & web payer settings tables. */
+.ruleTableHeader, .ruleTableCell {
+ margin: 5px;
+ padding: 5px;
+}
+
diff --git a/subsonic-main/src/main/webapp/style/coolandclean.css b/subsonic-main/src/main/webapp/style/coolandclean.css
new file mode 100644
index 00000000..0786b15b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/coolandclean.css
@@ -0,0 +1,116 @@
+/*
+ * CSS styleshet for the "Cool and Clean" theme.
+ *
+ * Author: Dan Eriksen (dan.o.eriksen[at]gmail.com)
+ *
+ * Icons used are released under Creative Commons.
+ * Heading icons are part of the Albook icon pack created by Laurent Baumann (http://lbaumann.com/)
+ * Main theme icon part of itunes icon pack created by Michael Flarup (http://pixelresort.com)
+ */
+
+@import "default.css";
+
+/* The primary background colour, light gray. Images used to force background on left frame headings and playlist. */
+.bgcolor1 {
+ background-image:url( "../icons/coolandclean/list_heading.png" );
+ background-repeat:repeat;
+ background-color: #F7F7F7;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #D1D9E1;
+}
+
+/* Background for selected headers. */
+.headerSelected {
+ background-image:url( "../icons/coolandclean/home_hover.png" );
+ background-repeat:repeat;
+ color: #333333;
+}
+
+
+/* Foreground colour used for h1, h2, b, tr, details and albumcomments. */
+h1, h2, b, tr, .detail, .albumComment {
+ color: #333333;
+}
+
+/* Table sizing */
+table {
+ font-size: 100%;
+ line-height: 130%;
+ padding: 0;
+ border: 0;
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-image:url( "../icons/coolandclean/background_main.png" );
+ background-repeat:no-repeat;
+ background-attachment:fixed;
+ background-color: #D1D9E1;
+ background-position:right;
+}
+
+/* Right frame background. */
+.rightframe {
+ background-color: #D1D9E1;
+ background-image:none;
+}
+
+/* Back image */
+.back {
+ background-image:url( "../icons/coolandclean/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/coolandclean/forward.png" );
+}
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #656569;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: none;
+ color: #333333;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #AA1F15;
+}
+
+/* The primary foreground colour. */
+body {
+ background-color: #D1D9E1;
+ scrollbar-arrow-color: #636363;
+ scrollbar-track-color: #D1D9E1;
+}
+
+/* The album comments. */
+.albumComment {
+ width: 50em;
+ font-size: 8pt;
+ line-height: 1.4em;
+ padding-top: 0.25em;
+}
+
+/* The log. */
+.log {
+ font-weight: bold;
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1em;
+}
+
+/* The help, status & web payer settings tables. */
+.ruleTableHeader, .ruleTableCell {
+ margin: 5px;
+ padding: 5px;
+}
+
diff --git a/subsonic-main/src/main/webapp/style/default.css b/subsonic-main/src/main/webapp/style/default.css
new file mode 100644
index 00000000..50182327
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/default.css
@@ -0,0 +1,245 @@
+/*
+ * CSS styleshet for default theme.
+ *
+ * Note that attributes that are typically changed by theme authors are
+ * placed at the top.
+ *
+ * Author: Sindre Mehus
+ */
+
+/* The primary background color (light blue). */
+.bgcolor1 {
+ background-color: #EFEFEF;
+}
+
+/* The secondary background color (darker blue). */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #DEE3E7
+}
+
+/* Put stuff here if you need to customize any of the frames. */
+.mainframe {
+}
+.topframe {
+}
+.leftframe {
+}
+.rightframe {
+}
+.playlistframe {
+}
+
+/* Background color for selected header. */
+.headerSelected {
+ background-color: lightyellow;
+}
+
+/* Background color for form controls (use default). */
+input, select {
+}
+
+/* Hover color for form controls (use default). */
+input:hover, select:hover {
+}
+
+/* The primary foreground color (black). */
+body {
+ color: black;
+}
+
+/* The secondary foreground color used for h1, details etc (gray). */
+h1, .detail, .albumComment {
+ color: #696969;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #333333;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #006699
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #DD6900
+}
+
+/* Color for warning messages. */
+.warning {
+ color: red;
+}
+
+/* Simple dark border. */
+.border1, .ruleTableHeader, .ruleTableCell, .headerSelected, .log {
+ border: 1px solid black;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: #DEE3E7;
+ scrollbar-highlight-color: #FFFFFF;
+ scrollbar-shadow-color: #DEE3E7;
+ scrollbar-3dlight-color: #D1D7DC;
+ scrollbar-arrow-color: #006699;
+ scrollbar-track-color: #EFEFEF;
+ scrollbar-darkshadow-color: #98AAB1;
+}
+
+/* Font 1 */
+body, h2, form, label, table, a {
+ font-family: verdana, arial, sans-serif;
+}
+
+/* Font 2 */
+h1, h1 a, .logo {
+ font-family: arial, sans-serif;
+}
+
+/***************************************************************************************
+ * The rest of the CSS is typically not changed in other themes (but not necessarily so).
+ ***************************************************************************************/
+
+body {
+ padding:0;
+ border:0;
+ margin:0.75em;
+ font-size: 9pt;
+ line-height: 1.5em;
+}
+
+p {
+ padding:0;
+ border:0;
+ margin:0 0 1em 0;
+}
+
+.dense {
+ white-space: nowrap;
+ margin: 0;
+ line-height: 1.3em
+}
+
+h1 {
+ white-space: nowrap;
+ font-size: 140%;
+ padding: 0 0 0.2em 0;
+ border: 0;
+ margin: 0;
+}
+
+h2 {
+ white-space: nowrap;
+ font-size: 100%;
+ margin: 1em 0 0.2em 0;
+}
+
+form {
+ font-size: 100%;
+ line-height: 140%;
+ padding: 0;
+ border: 0;
+ margin: 0;
+}
+
+input, select, textarea {
+ font-size: 9pt;
+}
+
+label {
+ font-size: 100%;
+ line-height: 140%;
+}
+
+table {
+ font-size: 100%;
+ line-height: 140%;
+ padding: 0;
+ border: 0;
+ margin: 0 0 0.4em 0;
+}
+
+/* Table with some white space above it.*/
+table.indent {
+ margin: 1em 0 0.4em 0;
+}
+
+a {
+ font-size: 100%;
+ text-decoration: none
+}
+
+img {
+ border-style: none;
+ border: 0;
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+}
+
+.headerSelected {
+ padding: 0.25em;
+}
+
+.detail {
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1.25em;
+}
+
+.warning {
+ white-space: nowrap;
+}
+
+.logo {
+ white-space: nowrap;
+ font-size: 16pt;
+}
+
+.back, .forward {
+ background-position:center left;
+ background-repeat:no-repeat;
+ padding-left: 16px;
+ line-height: 16px;
+}
+
+.back {
+ background-image:url("../icons/back.gif");
+}
+
+.forward {
+ background-image:url("../icons/forward.gif");
+}
+
+.albumComment {
+ width: 50em;
+ font-size: 8pt;
+ line-height: 1.4em;
+ padding-top: 0.25em;
+}
+
+.log {
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1em;
+}
+
+.checkbox {
+}
+
+/* Table with simple lines between the cells. */
+.ruleTable {
+ border-collapse: collapse;
+}
+
+.ruleTableHeader, .ruleTableCell {
+ margin: 5px;
+ padding: 5px;
+}
+
+.ruleTableHeader {
+ font-weight: bold;
+}
diff --git a/subsonic-main/src/main/webapp/style/denim.css b/subsonic-main/src/main/webapp/style/denim.css
new file mode 100644
index 00000000..b273a559
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/denim.css
@@ -0,0 +1,90 @@
+/*
+ * CSS styleshet for the "Denim" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #456993;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #456993;
+ color: #EFE9D9;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #EFE9D9;
+ background-color: #456993;
+ border: 1px solid #EFE9D9;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #EFE9D9;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #EFE9D9;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-color: #456993;
+ background-position:right;
+}
+
+
+/* Back image */
+.back {
+ background-image:url( "../icons/denim/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/denim/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #B6BEC2;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #B6BEC2;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+/* The primary foreground colour. */
+body {
+ color: #EFE9D9;
+ background-color: #DDDDDD;
+}
+
+html {
+ background-color: transparent;
+}
+
+label, p {
+ color: #CCC;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/groove.css b/subsonic-main/src/main/webapp/style/groove.css
new file mode 100644
index 00000000..88ae3e2d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/groove.css
@@ -0,0 +1,102 @@
+/*
+ * CSS styleshet for the "Groove" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ * Moified version of the "Cool and Clean" and "Monochrome Black" themes.
+ * (Cool and Clean created by: Dan Eriksen - dan.o.eriksen[at]gmail.com )
+ * (Monochrome Black created by: David D - ddavis1[at]gmail.com )
+ *
+ *
+ * Icons used are released under Creative Commons.
+ * Heading icons: (http://toffeenut.deviantart.com/art/Black-Neon-Agua-iPhone-Theme-85452072)
+ * Main theme icon part of itunes icon pack created by Michael Flarup (http://pixelresort.com)
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #222;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #222;
+ color: #FFFFFF;
+
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #FFFFFF;
+ background-color: #222;
+ border: 1px solid #eee;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #FFFFFF;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #FFFFFF;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-image:url( "../icons/groove/background_main.png" );
+ background-repeat:no-repeat;
+ background-attachment:fixed;
+ background-color: #222;
+ background-position:right;
+}
+
+
+/* Back image */
+.back {
+ background-image:url( "../icons/groove/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/groove/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #FFFFFF;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #FFFFFF;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+/* The primary foreground colour. */
+body {
+ color: #FFFFFF;
+ background-color: #DDDDDD;
+}
+
+html {
+ background-color: transparent;
+}
+
+label, p {
+ color: #CCC;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/groove_simple.css b/subsonic-main/src/main/webapp/style/groove_simple.css
new file mode 100644
index 00000000..83b3ac98
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/groove_simple.css
@@ -0,0 +1,32 @@
+/*
+ * CSS styleshet for the "Groove (Simple)" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ * Moified version of the "Cool and Clean" and "Monochrome Black" themes.
+ * (Cool and Clean created by: Dan Eriksen - dan.o.eriksen[at]gmail.com )
+ * (Monochrome Black created by: David D - ddavis1[at]gmail.com )
+ *
+ *
+ * Icons used are released under Creative Commons.
+ * Heading icons: (http://toffeenut.deviantart.com/art/Black-Neon-Agua-iPhone-Theme-85452072)
+ * Main theme icon part of itunes icon pack created by Michael Flarup (http://pixelresort.com)
+ */
+
+@import "groove.css";
+
+/* Main frame image */
+.mainframe {
+ background-image:url( "../icons/groove_simple/background_main_blank.png" );
+}
+
+
+/* Back image */
+.back {
+ background-image:url( "../icons/groove/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/groove/forward.png" );
+}
diff --git a/subsonic-main/src/main/webapp/style/hd.css b/subsonic-main/src/main/webapp/style/hd.css
new file mode 100644
index 00000000..3a5c3f7d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hd.css
@@ -0,0 +1,93 @@
+/*
+ * CSS styleshet for the "HD-1080" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "midnight.css";
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: #000843;
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #00042E
+}
+
+/* Put stuff here if you need to customize the body of the main frame. */
+.mainframe {
+ background-image:url('../icons/hd/background.png');
+ background-repeat:no-repeat;
+ background-attachment:fixed;
+ background-color: #00042E
+}
+
+/* Background color for selected header, log etc. */
+.headerSelected {
+ background-color: #CFD1D6;
+}
+
+/* The primary foreground color. */
+body {
+ color: #CFD1D6;
+}
+
+/* The secondary foreground color used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #CFD1D6;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #CFD1D6;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #97D9FF
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ color: orange;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: #00042E;
+ scrollbar-highlight-color: #000843;
+ scrollbar-shadow-color: #00042E;
+ scrollbar-3dlight-color: #00042E;
+ scrollbar-arrow-color: #000843;
+ scrollbar-track-color: #000843;
+ scrollbar-darkshadow-color: #000843;
+}
+
+/* Font 1 */
+body, h2, form, input, select, label, table, a, h1, h1 a, .logo {
+ font-family: calibri, arial, sans-serif;
+}
+
+body {
+ margin:1.5em;
+ font-size: 14pt;
+}
+
+h1, h2, th {
+ font-weight:normal;
+}
+
+input, select {
+ font-size: 100%;
+}
+
+.detail {
+ font-size: 12pt;
+}
+
+.albumComment, .log {
+ font-size: 12pt;
+}
+
diff --git a/subsonic-main/src/main/webapp/style/hd1080.css b/subsonic-main/src/main/webapp/style/hd1080.css
new file mode 100644
index 00000000..b11e7f1e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hd1080.css
@@ -0,0 +1,17 @@
+/*
+ * CSS styleshet for the "HD-1080" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "hd.css";
+
+body {
+ margin:1.5em;
+ font-size: 14pt;
+}
+
+.detail, .albumComment, .log {
+ font-size: 12pt;
+}
+
diff --git a/subsonic-main/src/main/webapp/style/hd720.css b/subsonic-main/src/main/webapp/style/hd720.css
new file mode 100644
index 00000000..0edb8ee4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hd720.css
@@ -0,0 +1,16 @@
+/*
+ * CSS styleshet for the "HD-720" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "hd.css";
+
+body {
+ margin:0.5em;
+ font-size: 11pt;
+}
+
+.detail, .albumComment, .log {
+ font-size: 9pt;
+}
diff --git a/subsonic-main/src/main/webapp/style/hd768.css b/subsonic-main/src/main/webapp/style/hd768.css
new file mode 100644
index 00000000..c7b7b25a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hd768.css
@@ -0,0 +1,16 @@
+/*
+ * CSS styleshet for the "HD-768" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "hd.css";
+
+body {
+ margin:0.5em;
+ font-size: 12pt;
+}
+
+.detail, .albumComment, .log {
+ font-size: 10pt;
+}
diff --git a/subsonic-main/src/main/webapp/style/hicon.css b/subsonic-main/src/main/webapp/style/hicon.css
new file mode 100644
index 00000000..ea13f95c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hicon.css
@@ -0,0 +1,85 @@
+/*
+ * CSS styleshet for the "High Contast" theme.
+ *
+ * Author: Jeebs (Fisher Evans)
+ */
+
+@import "default.css";
+
+table {
+ color: #5c5c5c;
+}
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: white;
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: white;
+}
+
+body.bgcolor2 {
+ border-bottom: 1px solid #5c5c5c;
+}
+
+/* Background color for selected header, log etc. */
+.headerSelected {
+ color: #5c5c5c;
+ background-color: white;
+}
+
+/* The primary foreground color. */
+body {
+ color: #5c5c5c;
+}
+
+/* The secondary foreground color used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #5c5c5c;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #5c5c5c;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: black
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #5c5c5c;
+}
+
+/* Color for warning messages. */
+.warning {
+ color: red;
+}
+
+/* Simple border. */
+.border1, .ruleTableHeader, .ruleTableCell, .headerSelected, .log {
+ border: 1px solid #5c5c5c;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: white;
+ scrollbar-highlight-color: #5c5c5c;
+ scrollbar-shadow-color: white;
+ scrollbar-3dlight-color: white;
+ scrollbar-arrow-color: #5c5c5c;
+ scrollbar-track-color: #5c5c5c;
+ scrollbar-darkshadow-color: #5c5c5c;
+}
+
+.back {
+ background-image:url("../icons/midnight/back.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
+.forward {
+ background-image:url("../icons/midnight/forward.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
diff --git a/subsonic-main/src/main/webapp/style/hiconi.css b/subsonic-main/src/main/webapp/style/hiconi.css
new file mode 100644
index 00000000..81e8ed01
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hiconi.css
@@ -0,0 +1,85 @@
+/*
+ * CSS styleshet for the "High Contast (Inverted)" theme.
+ *
+ * Author: Jeebs (Fisher Evans)
+ */
+
+@import "default.css";
+
+table {
+ color: #a3a3a3;
+}
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: black;
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: black;
+}
+
+body.bgcolor2 {
+ border-bottom: 1px solid #a3a3a3;
+}
+
+/* Background color for selected header, log etc. */
+.headerSelected {
+ color: #a3a3a3;
+ background-color: black;
+}
+
+/* The primary foreground color. */
+body {
+ color: #a3a3a3;
+}
+
+/* The secondary foreground color used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #a3a3a3;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #a3a3a3;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: white
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #a3a3a3;
+}
+
+/* Color for warning messages. */
+.warning {
+ color: red;
+}
+
+/* Simple border. */
+.border1, .ruleTableHeader, .ruleTableCell, .headerSelected, .log {
+ border: 1px solid #a3a3a3;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: black;
+ scrollbar-highlight-color: #a3a3a3;
+ scrollbar-shadow-color: black;
+ scrollbar-3dlight-color: black;
+ scrollbar-arrow-color: #a3a3a3;
+ scrollbar-track-color: #a3a3a3;
+ scrollbar-darkshadow-color: #a3a3a3;
+}
+
+.back {
+ background-image:url("../icons/midnight/back.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
+.forward {
+ background-image:url("../icons/midnight/forward.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
diff --git a/subsonic-main/src/main/webapp/style/hitech.css b/subsonic-main/src/main/webapp/style/hitech.css
new file mode 100644
index 00000000..a5fc6b3e
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/hitech.css
@@ -0,0 +1,96 @@
+/*
+ * CSS styleshet for the "High Tech" theme.
+ *
+ * Author: Jeebs (Fisher Evans)
+ */
+
+@import "default.css";
+
+table {
+ color: #d2f1d5;
+}
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: #090a09;
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #090a09;
+}
+
+body.bgcolor2 {
+ border-bottom: 1px solid #272727;
+}
+
+.bgcolor2 {
+ background-image:url(../icons/hitech/bg.jpg);
+}
+
+/* Background color for selected header, log etc. */
+.headerSelected {
+ color: #d2f1d5;
+ background-color: #090a09;
+}
+
+/* The primary foreground color. */
+body {
+ color: #d2f1d5;
+}
+
+/* The secondary foreground color used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #d2f1d5;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #d2f1d5;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #00d61b
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #ffffff;
+}
+
+/* Color for warning messages. */
+.warning {
+ color: white;
+}
+
+/* Simple border. */
+.border1, .ruleTableHeader, .ruleTableCell, .headerSelected, .log {
+ border: 1px solid #272727;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: #253126;
+ scrollbar-highlight-color: #00c52c;
+ scrollbar-shadow-color: #253126;
+ scrollbar-3dlight-color: #253126;
+ scrollbar-arrow-color: #00c52c;
+ scrollbar-track-color: #00c52c;
+ scrollbar-darkshadow-color: #00c52c;
+}
+
+.back {
+ background-image:url("../icons/midnight/back.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
+.forward {
+ background-image:url("../icons/midnight/forward.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
+
+.mainframe {
+ background-image:url( "../icons/hitech/bg2.jpg" );
+ background-repeat:no-repeat;
+ background-attachment:fixed;
+ background-position:bottom right;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/lowerleftfade.png b/subsonic-main/src/main/webapp/style/lowerleftfade.png
new file mode 100644
index 00000000..cc0665b4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/lowerleftfade.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/midnight.css b/subsonic-main/src/main/webapp/style/midnight.css
new file mode 100644
index 00000000..b6519bb8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/midnight.css
@@ -0,0 +1,77 @@
+/*
+ * CSS styleshet for the "2 Minutes To Midnight" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "default.css";
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: #656569;
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #4D4C52
+}
+
+/* Background color for selected header, log etc. */
+.headerSelected {
+ color: black;
+ background-color: #DDDDDD;
+}
+
+/* The primary foreground color. */
+body {
+ color: #DDDDDD;
+}
+
+/* The secondary foreground color used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #DDDDDD;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #DDDDDD;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #BAD9F2
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: orange;
+}
+
+/* Color for warning messages. */
+.warning {
+ color: orange;
+}
+
+/* Simple border. */
+.border1, .ruleTableHeader, .ruleTableCell, .headerSelected, .log {
+ border: 1px solid white;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: #4D4C52;
+ scrollbar-highlight-color: #656569;
+ scrollbar-shadow-color: #4D4C52;
+ scrollbar-3dlight-color: #4D4C52;
+ scrollbar-arrow-color: #656569;
+ scrollbar-track-color: #656569;
+ scrollbar-darkshadow-color: #656569;
+}
+
+.back {
+ background-image:url("../icons/midnight/back.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
+.forward {
+ background-image:url("../icons/midnight/forward.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
diff --git a/subsonic-main/src/main/webapp/style/midnightfun.css b/subsonic-main/src/main/webapp/style/midnightfun.css
new file mode 100644
index 00000000..07f69420
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/midnightfun.css
@@ -0,0 +1,158 @@
+/*
+ * CSS styleshet for the http://www.midnightfun.co.uk "MidnightFun" theme.
+ *
+ * Author: Don Pearson
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-image:url('../icons/midnightfun/midnightfun_text_back.jpg');
+ background-repeat:repeat;
+ background-color: #AAAAAA;
+}
+
+/* The secondary background colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-image:url('../icons/midnightfun/midnightfun_background.gif');
+ background-repeat:repeat;
+ background-attachment:fixed;
+ background-color: #C0C0C0;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ background-image:url('../icons/midnightfun/midnightfun_home_hover.jpg');
+ background-repeat:repeat;
+ color: #000000;
+ background-color: #C0C0C0;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #333333;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #000000;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe, .rightframe {
+ background-image:url('../icons/midnightfun/midnightfun_background.gif');
+ background-repeat:repeat;
+ background-attachment:fixed;
+ background-color: #C0C0C0;
+}
+
+/* Back image */
+.back {
+ background-image:url("../icons/midnightfun/midnightfun_back.png");
+}
+
+/* Forward image */
+.forward {
+ background-image:url("../icons/midnightfun/midnightfun_forward.png");
+}
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #656569;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #000000;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+/* Background colour for form controls. */
+input, select {
+ background-image:url('../icons/midnightfun/midnightfun_form_controls.jpg');
+ background-repeat:repeat;
+ color: #FFFFFF;
+ background-color: #AAAAAA;
+}
+
+/* Hover colour for form controls. */
+input:hover, select:hover {
+ background-image:url('../icons/midnightfun/midnightfun_form_controls_hover.jpg');
+ background-repeat:repeat;
+ color: #000000;
+ background-color: #C0C0C0;
+}
+
+/* Background colour for option controls. */
+option {
+ font-weight: bold;
+ background-color: #666666;
+ color: #FFFFFF;
+}
+
+/* Background colour for option comments. */
+textarea {
+ background-image:url('../icons/midnightfun/midnightfun_form_controls.jpg');
+ background-repeat:repeat;
+ background-color: #AAAAAA;
+ color: #FFFFFF;
+}
+
+/* The primary foreground colour. */
+body {
+ background-color: #C0C0C0;
+ scrollbar-face-color: #CCCCCC;
+ scrollbar-highlight-color: #FFFFFF;
+ scrollbar-shadow-color: #CCCCCC;
+ scrollbar-3dlight-color: #D1D7DC;
+ scrollbar-arrow-color: #000000;
+ scrollbar-track-color: #C0C0C0;
+ scrollbar-darkshadow-color: #98AAB1;
+}
+
+/* The album comments. */
+.albumComment {
+ width: 50em;
+ font-size: 8pt;
+ line-height: 1.4em;
+ padding-top: 0.25em;
+ color: #000000;
+}
+
+/* The log. */
+.log {
+ font-weight: bold;
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1em;
+ background-image:url('../icons/midnightfun/midnightfun_table.jpg');
+ background-repeat:repeat;
+ color: #000000;
+ background-color: #C0C0C0;
+}
+
+/* The help, status & web payer settings tables. */
+.ruleTableHeader, .ruleTableCell {
+ margin: 5px;
+ padding: 5px;
+ background-image:url('../icons/midnightfun/midnightfun_table.jpg');
+ background-repeat:repeat;
+ background-attachment:fixed;
+ color: #FFFFFF;
+ background-color: #C0C0C0;
+}
+
+
+/* MidnightFun http://www.midnightfun.co.uk */
diff --git a/subsonic-main/src/main/webapp/style/monochrome.css b/subsonic-main/src/main/webapp/style/monochrome.css
new file mode 100644
index 00000000..ce1bf47d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/monochrome.css
@@ -0,0 +1,111 @@
+/*
+ * CSS styleshet for monochrome style
+ *
+ * Author: David D ddavis1@gmail.com
+ */
+
+@import "default.css";
+
+/* highlights to contrast with bgcolor2. */
+.bgcolor1 {
+ background-color: #EEE;
+}
+
+/* The primary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #CCC;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #000000;
+ background-color: #AAAAAA;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #000000;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #000000;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe, .rightframe {
+ background-color: #EEE;
+}
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #000000;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #FFFFFF;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+/* Background colour for form controls. */
+input, select {
+ color: #333;
+ background-color: #EEE;
+}
+
+/* Hover colour for form controls. */
+input:hover, select:hover {
+ color: #000000;
+ background-color: #FFF;
+}
+
+/* Background colour for option controls. */
+option {
+ font-weight: bold;
+ background-color: #666666;
+ color: #FFFFFF;
+}
+
+/* The primary foreground colour. */
+body {
+ background-color: #EEEEEE;
+}
+
+/* The album comments. */
+.albumComment {
+ width: 50em;
+ font-size: 8pt;
+ line-height: 1.4em;
+ padding-top: 0.25em;
+ color: #000000;
+}
+
+/* The log. */
+.log {
+ font-weight: bold;
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1em;
+ color: #000000;
+ background-color: #C0C0C0;
+}
+
+/* The help, status & web payer settings tables. */
+.ruleTableHeader, .ruleTableCell {
+ margin: 5px;
+ padding: 5px;
+ color: #FFFFFF;
+ background-color: #C0C0C0;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/monochrome_black.css b/subsonic-main/src/main/webapp/style/monochrome_black.css
new file mode 100644
index 00000000..0a9f42d3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/monochrome_black.css
@@ -0,0 +1,115 @@
+/*
+ * CSS styleshet for monochrome style
+ *
+ * Author: David D ddavis1@gmail.com
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #222;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #000000;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #FFFFFF;
+ background-color: #333333;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #FFFFFF;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #FFFFFF;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe, .rightframe {
+ background-color: #222;
+}
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #FFFFFF;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #FFFFFF;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+/* Background colour for form controls. */
+input, select {
+ color: #000;
+ background-color: #EEE;
+}
+
+/* Hover colour for form controls. */
+input:hover, select:hover {
+ color: #000000;
+ background-color: #FFF;
+}
+
+/* Background colour for option controls. */
+option {
+ font-weight: bold;
+ background-color: #666666;
+ color: #FFFFFF;
+}
+
+/* The primary foreground colour. */
+body {
+ background-color: #DDDDDD;
+}
+
+/* The album comments. */
+.albumComment {
+ width: 50em;
+ font-size: 8pt;
+ line-height: 1.4em;
+ padding-top: 0.25em;
+ color: #000000;
+}
+
+/* The log. */
+.log {
+ font-weight: bold;
+ white-space: nowrap;
+ font-size: 8pt;
+ line-height: 1em;
+ color: #000000;
+ background-color: #C0C0C0;
+}
+
+/* The help, status & web payer settings tables. */
+.ruleTableHeader, .ruleTableCell {
+ margin: 5px;
+ padding: 5px;
+ color: #FFFFFF;
+ background-color: #C0C0C0;
+}
+
+label, p {
+ color: #CCC;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/pinkpanther.css b/subsonic-main/src/main/webapp/style/pinkpanther.css
new file mode 100644
index 00000000..f72494d7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/pinkpanther.css
@@ -0,0 +1,91 @@
+/*
+ * CSS styleshet for the "PinkPanther" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #402c31;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #402c31;
+ color: #EFE9D9;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #EFE9D9;
+ background-color: #402c31;
+ border: 1px solid #EFE9D9;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #EFE9D9;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #EFE9D9;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-color: #402c31;
+ background-position:right;
+}
+
+
+/* Back image */
+.back {
+ background-image:url( "../icons/pinkpanther/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/pinkpanther/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #11bdcb;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #11bdcb;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+
+/* The primary foreground colour. */
+body {
+ color: #EFE9D9;
+ background-color: #DDDDDD;
+}
+
+html {
+ background-color: transparent;
+}
+
+label, p {
+ color: #CCC;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/ripserver.css b/subsonic-main/src/main/webapp/style/ripserver.css
new file mode 100644
index 00000000..9959fabe
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/ripserver.css
@@ -0,0 +1,44 @@
+/*
+ * CSS styleshet for the "Ripserver" theme.
+ *
+ * Author: Ralph Hill
+ */
+
+@import "default.css";
+
+/* The primary background color (light blue). */
+.bgcolor1 {
+ background-color: #FFFFFF;
+}
+
+/* The secondary background color (darker blue). */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #EBEFF9
+}
+
+.mainframe {
+ background-image:url('../icons/ripserver/background.png');
+ background-repeat:no-repeat;
+ background-attachment:fixed;
+ background-color: #FFFFFF
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #0076B6
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ color: orange
+}
+
+body {
+ scrollbar-face-color: #DEE3E7;
+ scrollbar-highlight-color: #FFFFFF;
+ scrollbar-shadow-color: #DEE3E7;
+ scrollbar-3dlight-color: #D1D7DC;
+ scrollbar-arrow-color: #006699;
+ scrollbar-track-color: #EFEFEF;
+ scrollbar-darkshadow-color: #98AAB1;
+}
diff --git a/subsonic-main/src/main/webapp/style/sandstorm.css b/subsonic-main/src/main/webapp/style/sandstorm.css
new file mode 100644
index 00000000..89556bc9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/sandstorm.css
@@ -0,0 +1,39 @@
+/*
+ * CSS styleshet for the "Sandstorm" theme.
+ *
+ * Author: Sindre Mehus
+ */
+
+@import "default.css";
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: #F5F5D0
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #CCCC99
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #057368
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #DD6900
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: #CCCC99;
+ scrollbar-highlight-color: #F5F5D0;
+ scrollbar-shadow-color: #CCCC99;
+ scrollbar-3dlight-color: #CCCC99;
+ scrollbar-arrow-color: #F5F5D0;
+ scrollbar-track-color: #F5F5D0;
+ scrollbar-darkshadow-color: #F5F5D0;
+}
diff --git a/subsonic-main/src/main/webapp/style/shadow.css b/subsonic-main/src/main/webapp/style/shadow.css
new file mode 100644
index 00000000..144a34bc
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/shadow.css
@@ -0,0 +1,107 @@
+/*
+ From http://www.positioniseverything.net/articles/dropshadows.html
+*/
+
+html>body .outerpair1 {
+ background: url( upperrightfade.png ) right top no-repeat;
+}
+
+/* .outerpair1 must be given a width contraint, via either a width,
+or by floating or absolute positioning. In this demo these are
+applied from the second class name on the .outerpair1 DIV's.
+This box also has one of the corner .png's. */
+
+html>body .outerpair2 {
+ background: url( lowerleftfade.png ) left bottom no-repeat;
+ padding-top: 8px;
+ padding-left: 8px;
+}
+
+/* .outerpair2 has padding equal to the shadow
+thickness, and also has one of the corner .png's */
+
+html>body .shadowbox {
+ background: url( shadow.png ) bottom right;
+}
+
+/* .shadowbox holds the main shadow .png */
+
+html>body .innerbox {
+ position: relative;
+ left: -8px;
+ top: -8px;
+}
+
+/* .innerbox is made "relative" and is "pulled" up and to
+the left, by a distance equal to the thickness of the shadow.
+Because this is a relative-based shift, the box retains its
+exact dimensions without change. */
+
+.shadowbox img {
+ border: 0;
+ vertical-align: bottom;
+}
+
+/* Shadowed images should not be made "block" for eliminating the baseline
+space under the images, because this may trigger IE background bugs.
+Instead, use "vertical-align: bottom;" for this purpose. */
+
+/*XXXXXXXXXXXXXXXXXX Custom width constraints and extra styling XXXXXXXXXXXXXXX*/
+
+.floatimage {
+ float: left; /* Floating causes this box to shrinkwrap around sized content elements. */
+ margin: 130px 0 0 450px;
+ display: inline; /* IE doubled margin bug is defeated via this fixer rule. */
+}
+
+.flashbox {
+/* Absolute positioning also causes the shrinkwrap behavior. */
+ position: absolute;
+ left: 377px;
+ top: 30px;
+}
+
+.flashbox .innerbox {
+ background: #eed;
+ border: 1px solid #ccb;
+}
+
+.absoluteimage {
+/* Again, absolute positioning causes shrinkwrapping. */
+ position: absolute;
+ left: 40px;
+ top: 200px;
+}
+
+.textbox {
+ position: absolute; /* AP once more... */
+ left: 20px;
+ top: 1.8em;
+}
+
+.textbox .innerbox {
+ border: 1px solid #ccc;
+ background: #e8e8e8;
+ width: 330px;
+ height: 210px;
+ overflow: auto;
+}
+
+/* Unlike the other items, the .textbox content is just text without a natural
+width, and so shrinkwrapping fails, unless .innerbox is given a specific width.
+All shadowed text elements will need a width of some kind to avoid a full-width
+shadowed box, unless that is the desired effect. The width may be appied to
+div.inner, div.outerpair1, or an external wrapper element. */
+
+.linkbox {
+ position: absolute; /* AP once more... */
+ left: 10px;
+ top: 6px;
+}
+
+.linkbox .innerbox {
+ display: block;
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 3px 5px;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/shadow.png b/subsonic-main/src/main/webapp/style/shadow.png
new file mode 100644
index 00000000..7862c9bb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/shadow.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/simplify.css b/subsonic-main/src/main/webapp/style/simplify.css
new file mode 100644
index 00000000..d3ac78c1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/simplify.css
@@ -0,0 +1,90 @@
+/*
+ * CSS styleshet for the "Simplify" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #1b1b1b;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #1b1b1b;
+ color: #FFFFFF;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #FFFFFF;
+ background-color: #1b1b1b;
+ border: 1px solid #FFFFFF;
+}
+
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #FFFFFF;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #FFFFFF;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-color: #1b1b1b;
+ background-position:right;
+}
+
+
+/* Back image */
+.back {
+ background-image:url( "../icons/simplify/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/simplify/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #FFFFFF;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #FFFFFF;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #e7238b;
+}
+
+/* The primary foreground colour. */
+body {
+ color: #FFFFFF;
+ background-color: #DDDDDD;
+}
+
+html {
+ background-color: transparent;
+}
+
+label, p {
+ color: #CCC;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/slick.css b/subsonic-main/src/main/webapp/style/slick.css
new file mode 100644
index 00000000..1ae02240
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/slick.css
@@ -0,0 +1,89 @@
+/*
+ * CSS styleshet for the "Slick" theme.
+ *
+ * Author: Jeebs (Fisher Evans)
+ */
+
+@import "default.css";
+
+table {
+ color: #0e0e0e;
+}
+
+/* The primary background color. */
+.bgcolor1 {
+ background-color: #0e0e0e;
+}
+
+/* The secondary background color. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #0e0e0e;
+}
+
+body.bgcolor2 {
+ border-bottom: 1px solid #67778c;
+}
+
+.bgcolor2 {
+ background-image:url(../icons/slick/top_bg.jpg);
+}
+
+/* Background color for selected header, log etc. */
+.headerSelected {
+ color: #0e0e0e;
+ background-color: #8eacc4;
+}
+
+/* The primary foreground color. */
+body {
+ color: #ccd5dc;
+}
+
+/* The secondary foreground color used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #ccd5dc;
+}
+
+/* Foreground color used for h2, bold and tr. */
+h2, b, tr {
+ color: #ccd5dc;
+}
+
+/* Link color */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #8a9aa7
+}
+
+/* Link hover color */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #d2d9df;
+}
+
+/* Color for warning messages. */
+.warning {
+ color: #ff4800;
+}
+
+/* Simple border. */
+.border1, .ruleTableHeader, .ruleTableCell, .headerSelected, .log {
+ border: 1px solid #67778c;
+}
+
+/* Scrollbar colors (supported on IE and Opera) */
+body {
+ scrollbar-face-color: #8eacc4;
+ scrollbar-highlight-color: #67778c;
+ scrollbar-shadow-color: #8eacc4;
+ scrollbar-3dlight-color: #8eacc4;
+ scrollbar-arrow-color: #67778c;
+ scrollbar-track-color: #67778c;
+ scrollbar-darkshadow-color: #67778c;
+}
+
+.back {
+ background-image:url("../icons/midnight/back.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
+.forward {
+ background-image:url("../icons/midnight/forward.png");background-position: 0px 3px; background-repeat:no-repeat; padding-left: 16px; line-height: 16px;
+}
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 00000000..5b5dab2a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_75_ffffff_40x100.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 00000000..ac8b229a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_flat_75_ffffff_40x100.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 00000000..ad3d6346
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_65_ffffff_1x400.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 00000000..42ccba26
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_65_ffffff_1x400.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_dadada_1x400.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 00000000..5a46b47c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_dadada_1x400.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 00000000..86c2baa6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 00000000..4443fdc1
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 00000000..7c9fa6c6
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_222222_256x240.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_222222_256x240.png
new file mode 100644
index 00000000..b273ff11
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_222222_256x240.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_2e83ff_256x240.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 00000000..09d1cdc8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_2e83ff_256x240.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_454545_256x240.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_454545_256x240.png
new file mode 100644
index 00000000..59bd45b9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_454545_256x240.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_888888_256x240.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_888888_256x240.png
new file mode 100644
index 00000000..6d02426c
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_888888_256x240.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_cd0a0a_256x240.png b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 00000000..2ab019b7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/images/ui-icons_cd0a0a_256x240.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/style/smoothness/jquery-ui-1.8.18.custom.css b/subsonic-main/src/main/webapp/style/smoothness/jquery-ui-1.8.18.custom.css
new file mode 100644
index 00000000..4cfb50a4
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/smoothness/jquery-ui-1.8.18.custom.css
@@ -0,0 +1,565 @@
+/*
+ * jQuery UI CSS Framework 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+
+
+/*
+ * jQuery UI CSS Framework 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
+.ui-widget-content a { color: #222222; }
+.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
+.ui-widget-header a { color: #222222; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; }
+.ui-widget :active { outline: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
+.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/*
+ * jQuery UI Resizable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*
+ * jQuery UI Selectable 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectable#theming
+ */
+.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
+/*
+ * jQuery UI Accordion 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion#theming
+ */
+/* IE/Win - Fix animation bug - #4615 */
+.ui-accordion { width: 100%; }
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
+.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
+.ui-accordion .ui-accordion-content-active { display: block; }
+/*
+ * jQuery UI Autocomplete 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete { position: absolute; cursor: default; }
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
+
+/*
+ * jQuery UI Menu 1.8.18
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu {
+ list-style:none;
+ padding: 2px;
+ margin: 0;
+ display:block;
+ float: left;
+}
+.ui-menu .ui-menu {
+ margin-top: -3px;
+}
+.ui-menu .ui-menu-item {
+ margin:0;
+ padding: 0;
+ zoom: 1;
+ float: left;
+ clear: left;
+ width: 100%;
+}
+.ui-menu .ui-menu-item a {
+ text-decoration:none;
+ display:block;
+ padding:.2em .4em;
+ line-height:1.5;
+ zoom:1;
+}
+.ui-menu .ui-menu-item a.ui-state-hover,
+.ui-menu .ui-menu-item a.ui-state-active {
+ font-weight: normal;
+ margin: -1px;
+}
+/*
+ * jQuery UI Button 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button#theming
+ */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: hidden; *overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; }
+button.ui-button-icons-only { width: 3.7em; }
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4; }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
+.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+/*
+ * jQuery UI Dialog 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; }
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+/*
+ * jQuery UI Slider 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }/*
+ * jQuery UI Tabs 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs#theming
+ */
+.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
+.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
+/*
+ * jQuery UI Datepicker 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month,
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+ display: none; /*sorry for IE5*/
+ display/**/: block; /*sorry for IE5*/
+ position: absolute; /*must have*/
+ z-index: -1; /*must have*/
+ filter: mask(); /*must have*/
+ top: -4px; /*must have*/
+ left: -4px; /*must have*/
+ width: 200px; /*must have*/
+ height: 200px; /*must have*/
+}/*
+ * jQuery UI Progressbar 1.8.18
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar#theming
+ */
+.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/sonic.css b/subsonic-main/src/main/webapp/style/sonic.css
new file mode 100644
index 00000000..526ac59d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/sonic.css
@@ -0,0 +1,88 @@
+/*
+ * CSS styleshet for the "Sonic" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ */
+
+@import "default.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #e7e7e7;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #e7e7e7;
+ color: #696969;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #696969;
+ background-color: #e7e7e7;
+ border: 1px solid #696969;
+}
+
+
+/* The primary foreground color (black). */
+body {
+ color: #696969;
+ background-color: #DDDDDD;
+}
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #696969;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #696969;
+}
+
+/* Table sizing */
+table {
+ margin: 0 0 0 0;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-color: #e7e7e7;
+}
+
+/* Back image */
+.back {
+ background-image:url( "../icons/sonic/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/sonic/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #333333;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ text-decoration: underline;
+ color: #696969;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #e7238b;
+}
+
+html {
+ background-color: transparent;
+}
+
+label, p {
+ color: #696969;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/sonic_blue.css b/subsonic-main/src/main/webapp/style/sonic_blue.css
new file mode 100644
index 00000000..7122ad59
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/sonic_blue.css
@@ -0,0 +1,77 @@
+/*
+ * CSS styleshet for the "Sonic Blue" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ */
+
+@import "sonic.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #456993;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #456993;
+ color: #EFE9D9;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #EFE9D9;
+ background-color: #456993;
+ border: 1px solid #EFE9D9;
+}
+
+/* The primary foreground color (black). */
+body {
+ color: #EFE9D9;
+ background-color: #DDDDDD;
+}
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #EFE9D9;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #EFE9D9;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-color: #456993;
+}
+
+/* Back image */
+.back {
+ background-image:url( "../icons/sonic_blue/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/sonic_blue/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #B6BEC2;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ color: #B6BEC2;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #990099;
+}
+
+label, p {
+ color: #CCC;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/sonic_white.css b/subsonic-main/src/main/webapp/style/sonic_white.css
new file mode 100644
index 00000000..0c28f138
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/sonic_white.css
@@ -0,0 +1,78 @@
+/*
+ * CSS styleshet for the "Sonic White" theme.
+ *
+ * Author: Thomas Bruce Dyrud (thomasbrucedyrud[at]gmail.com)
+ *
+ */
+
+@import "sonic.css";
+
+/* The primary background colour. */
+.bgcolor1 {
+ background-color: #FFFFFF;
+}
+
+/* The secondary background colour, light blue, primary theme colour. */
+.bgcolor2, .ruleTableHeader, .log {
+ background-color: #FFFFFF;
+ color: #696969;
+}
+
+/* Background colour for selected header, log etc. */
+.headerSelected {
+ color: #696969;
+ background-color: #FFFFFF;
+ border: 1px solid #696969;
+}
+
+
+/* The primary foreground color (black). */
+body {
+ color: #696969;
+ background-color: #DDDDDD;
+}
+
+/* The secondary foreground colour used for h1, details etc. */
+h1, .detail, .albumComment {
+ color: #696969;
+}
+
+/* Foreground colour used for h2, bold and tr. */
+h2, b, tr {
+ color: #696969;
+}
+
+/* Main frame image & colour */
+.mainframe {
+ background-color: #FFFFFF;
+}
+
+/* Back image */
+.back {
+ background-image:url( "../icons/sonic_white/back.png" );
+}
+
+/* Forward image */
+.forward {
+ background-image:url( "../icons/sonic_white/forward.png" );
+}
+
+
+/* Link colour */
+a:link, a:active, a:visited, a:link *, a:active *, a:visited * {
+ color: #333333;
+}
+
+/* Link hover colour */
+a:hover, a:hover * {
+ color: #696969;
+}
+
+/* Colour for warning messages. */
+.warning {
+ color: #e7238b;
+}
+
+label, p {
+ color: #696969;
+} \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/style/upperrightfade.png b/subsonic-main/src/main/webapp/style/upperrightfade.png
new file mode 100644
index 00000000..cad0f2c7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/style/upperrightfade.png
Binary files differ
diff --git a/subsonic-main/src/main/webapp/xsd/albumList2_example_1.xml b/subsonic-main/src/main/webapp/xsd/albumList2_example_1.xml
new file mode 100644
index 00000000..21684156
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/albumList2_example_1.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0">
+ <albumList2>
+ <album id="1768" name="Duets" coverArt="al-1768" songCount="2" created="2002-11-09T15:44:40" duration="514" artist="Nik Kershaw" artistId="829"/>
+ <album id="2277" name="Hot" coverArt="al-2277" songCount="4" created="2004-11-28T00:06:52" duration="1110" artist="Melanie B" artistId="1242"/>
+ <album id="4201" name="Bande A Part" coverArt="al-4201" songCount="14" created="2007-10-29T19:25:05" duration="3061" artist="Nouvelle Vague" artistId="2060"/>
+ <album id="2910" name="Soundtrack From Twin Peaks" coverArt="al-2910" songCount="6" created="2002-11-17T09:58:42" duration="1802" artist="Angelo Badalamenti" artistId="1515"/>
+ <album id="3109" name="Wild One" coverArt="al-3109" songCount="38" created="2001-04-17T00:20:08" duration="9282" artist="Thin Lizzy" artistId="661"/>
+ <album id="1151" name="Perleporten" coverArt="al-1151" songCount="2" created="2002-11-16T22:24:22" duration="494" artist="Magnus Gr&#248;nneberg" artistId="747"/>
+ <album id="2204" name="Wholesale Meats And Fish" coverArt="al-2204" songCount="24" created="2004-11-27T23:44:31" duration="5362" artist="Letters To Cleo" artistId="1216"/>
+ <album id="114" name="Sounds of the Seventies: AM Nuggets" coverArt="al-114" songCount="2" created="2004-03-09T07:32:46" duration="420" artist="Rubettes" artistId="97"/>
+ <album id="279" name="Waiting for the Day" coverArt="al-279" songCount="2" created="2004-11-27T17:49:19" duration="448" artist="Bachelor Girl" artistId="231"/>
+ <album id="4414" name="For Sale" songCount="14" created="2007-10-30T00:11:58" duration="2046" artist="The Beatles" artistId="509"/>
+ </albumList2>
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/albumList_example_1.xml b/subsonic-main/src/main/webapp/xsd/albumList_example_1.xml
new file mode 100644
index 00000000..d83482a9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/albumList_example_1.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.6.0">
+
+ <albumList>
+ <album id="11" parent="1" title="Arrival" artist="ABBA" isDir="true" coverArt="22" userRating="4" averageRating="4.5"/>
+ <album id="12" parent="1" title="Super Trouper" artist="ABBA" isDir="true" coverArt="23" averageRating="4.4"/>
+ </albumList>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/album_example_1.xml b/subsonic-main/src/main/webapp/xsd/album_example_1.xml
new file mode 100644
index 00000000..c1a8df27
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/album_example_1.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.8.0">
+
+ <album id="11053" name="High Voltage" coverArt="al-11053" songCount="8" created="2004-11-27T20:23:32" duration="2414" artist="AC/DC" artistId="5432">
+ <song id="71463" parent="71381" title="The Jack" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-08T23:36:11" duration="352" bitRate="128" size="5624132" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - The Jack.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71464" parent="71381" title="Tnt" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-08T23:36:11" duration="215" bitRate="128" size="3433798" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - TNT.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71458" parent="71381" title="It&apos;s A Long Way To The Top" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-27T20:23:32" duration="315" bitRate="128" year="1976" genre="Rock" size="5037357" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - It&apos;s a long way to the top if you wanna rock &apos;n &apos;roll.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71461" parent="71381" title="Rock &apos;n&apos; Roll Singer." album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-27T20:23:33" duration="303" bitRate="128" track="2" year="1976" genre="Rock" size="4861680" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - Rock N Roll Singer.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71460" parent="71381" title="Live Wire" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-27T20:23:33" duration="349" bitRate="128" track="4" year="1976" genre="Rock" size="5600206" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - Live Wire.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71456" parent="71381" title="Can I sit next to you girl" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-27T20:23:32" duration="251" bitRate="128" track="6" year="1976" genre="Rock" size="4028276" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - Can I Sit Next To You Girl.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71459" parent="71381" title="Little Lover" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-27T20:23:33" duration="339" bitRate="128" track="7" year="1976" genre="Rock" size="5435119" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - Little Lover.mp3" albumId="11053" artistId="5432" type="music"/>
+ <song id="71462" parent="71381" title="She&apos;s Got Balls" album="High Voltage" artist="AC/DC" isDir="false" coverArt="71381" created="2004-11-27T20:23:34" duration="290" bitRate="128" track="8" year="1976" genre="Rock" size="4651866" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/High voltage/ACDC - Shes Got Balls.mp3" albumId="11053" artistId="5432" type="music"/>
+ </album>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/artist_example_1.xml b/subsonic-main/src/main/webapp/xsd/artist_example_1.xml
new file mode 100644
index 00000000..86fc4e67
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/artist_example_1.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.8.0">
+
+ <artist id="5432" name="AC/DC" coverArt="ar-5432" albumCount="15">
+ <album id="11047" name="Back In Black" coverArt="al-11047" songCount="10" created="2004-11-08T23:33:11" duration="2534" artist="AC/DC" artistId="5432"/>
+ <album id="11048" name="Black Ice" coverArt="al-11048" songCount="15" created="2008-10-30T09:20:52" duration="3332" artist="AC/DC" artistId="5432"/>
+ <album id="11049" name="Blow up your Video" coverArt="al-11049" songCount="10" created="2004-11-27T19:22:45" duration="2578" artist="AC/DC" artistId="5432"/>
+ <album id="11050" name="Flick Of The Switch" coverArt="al-11050" songCount="10" created="2004-11-27T19:22:51" duration="2222" artist="AC/DC" artistId="5432"/>
+ <album id="11051" name="Fly On The Wall" coverArt="al-11051" songCount="10" created="2004-11-27T19:22:57" duration="2405" artist="AC/DC" artistId="5432"/>
+ <album id="11052" name="For Those About To Rock" coverArt="al-11052" songCount="10" created="2004-11-08T23:35:02" duration="2403" artist="AC/DC" artistId="5432"/>
+ <album id="11053" name="High Voltage" coverArt="al-11053" songCount="8" created="2004-11-27T20:23:32" duration="2414" artist="AC/DC" artistId="5432"/>
+ <album id="10489" name="Highway To Hell" coverArt="al-10489" songCount="12" created="2009-06-15T09:41:54" duration="2745" artist="AC/DC" artistId="5432"/>
+ <album id="11054" name="If You Want Blood..." coverArt="al-11054" songCount="1" created="2004-11-27T20:23:32" duration="304" artist="AC/DC" artistId="5432"/>
+ <album id="11056" name="Let There Be Rock" coverArt="al-11056" songCount="8" created="2004-11-27T20:33:40" duration="2449" artist="AC/DC" artistId="5432"/>
+ <album id="11057" name="Live - Special Collector&apos;s Edition" coverArt="al-11057" songCount="22" created="2004-11-08T23:37:09" duration="6999" artist="AC/DC" artistId="5432"/>
+ <album id="11058" name="Powerage" coverArt="al-11058" songCount="9" created="2004-11-27T20:33:41" duration="2380" artist="AC/DC" artistId="5432"/>
+ <album id="11059" name="Stiff Upper Lip" coverArt="al-11059" songCount="11" created="2004-11-08T23:41:13" duration="2595" artist="AC/DC" artistId="5432"/>
+ <album id="11060" name="The Razors Edge" coverArt="al-11060" songCount="12" created="2004-11-27T20:33:42" duration="2787" artist="AC/DC" artistId="5432"/>
+ <album id="11061" name="Who Made Who" coverArt="al-11061" songCount="9" created="2004-11-08T23:43:18" duration="2291" artist="AC/DC" artistId="5432"/>
+ </artist>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/artists_example_1.xml b/subsonic-main/src/main/webapp/xsd/artists_example_1.xml
new file mode 100644
index 00000000..0c2498b2
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/artists_example_1.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.8.0">
+ <artists>
+ <artist id="5449" name="A-Ha" coverArt="ar-5449" albumCount="4"/>
+ <artist id="5421" name="ABBA" coverArt="ar-5421" albumCount="6"/>
+ <artist id="5432" name="AC/DC" coverArt="ar-5432" albumCount="15"/>
+ <artist id="6633" name="Aaron Neville" coverArt="ar-6633" albumCount="1"/>
+ <artist id="5950" name="Bob Marley" coverArt="ar-5950" albumCount="8"/>
+ <artist id="5957" name="Bruce Dickinson" coverArt="ar-5957" albumCount="2"/>
+</artists>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/chatMessages_example_1.xml b/subsonic-main/src/main/webapp/xsd/chatMessages_example_1.xml
new file mode 100644
index 00000000..b633d534
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/chatMessages_example_1.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.2.0">
+
+ <chatMessages>
+ <chatMessage username="sindre" time="1269771845310" message="Sindre was here"/>
+ <chatMessage username="ben" time="1269771842504" message="Ben too"/>
+ </chatMessages>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/directory_example_1.xml b/subsonic-main/src/main/webapp/xsd/directory_example_1.xml
new file mode 100644
index 00000000..c2ec1582
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/directory_example_1.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <directory id="1" name="ABBA">
+ <child id="11" parent="1" title="Arrival" artist="ABBA" isDir="true" coverArt="22"/>
+ <child id="12" parent="1" title="Super Trouper" artist="ABBA" isDir="true" coverArt="23"/>
+ </directory>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/directory_example_2.xml b/subsonic-main/src/main/webapp/xsd/directory_example_2.xml
new file mode 100644
index 00000000..246e7b51
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/directory_example_2.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <directory id="11" parent="1" name="Arrival">
+ <child id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ size="8421341" contentType="audio/mpeg" suffix="mp3" duration="146" bitRate="128"
+ path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <child id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="208" bitRate="128"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </directory>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/error_example_1.xml b/subsonic-main/src/main/webapp/xsd/error_example_1.xml
new file mode 100644
index 00000000..80cafabf
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/error_example_1.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="failed" version="1.1.1">
+
+ <error code="40" message="Wrong username or password."/>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/indexes_example_1.xml b/subsonic-main/src/main/webapp/xsd/indexes_example_1.xml
new file mode 100644
index 00000000..5438ad51
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/indexes_example_1.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.1.1">
+
+ <indexes lastModified="237462836472342">
+ <shortcut id="11" name="Audio books"/>
+ <shortcut id="10" name="Podcasts"/>
+ <index name="A">
+ <artist id="1" name="ABBA"/>
+ <artist id="2" name="Alanis Morisette"/>
+ <artist id="3" name="Alphaville"/>
+ </index>
+ <index name="B">
+ <artist name="Bob Dylan" id="4"/>
+ </index>
+
+ <child id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ size="8421341" contentType="audio/mpeg" suffix="mp3" duration="146" bitRate="128"
+ path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <child id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="208" bitRate="128"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </indexes>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/jukeboxPlaylist_example_1.xml b/subsonic-main/src/main/webapp/xsd/jukeboxPlaylist_example_1.xml
new file mode 100644
index 00000000..e214a860
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/jukeboxPlaylist_example_1.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <jukeboxPlaylist currentIndex="0" playing="true" gain="0.67" position="67">
+ <entry id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ duration="345" size="8421341" contentType="audio/mpeg" suffix="mp3"
+ path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <entry id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ duration="240" size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </jukeboxPlaylist>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/jukeboxStatus_example_1.xml b/subsonic-main/src/main/webapp/xsd/jukeboxStatus_example_1.xml
new file mode 100644
index 00000000..a0200494
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/jukeboxStatus_example_1.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.7.0">
+
+ <jukeboxStatus currentIndex="7" playing="true" gain="0.9" position="67"/>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/license_example_1.xml b/subsonic-main/src/main/webapp/xsd/license_example_1.xml
new file mode 100644
index 00000000..720381ab
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/license_example_1.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.1.1">
+
+ <license valid="true" email="foo@bar.com" key="ABC123DEF" date="2009-09-03T14:46:43"/>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/lyrics_example_1.xml b/subsonic-main/src/main/webapp/xsd/lyrics_example_1.xml
new file mode 100644
index 00000000..1a2831cb
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/lyrics_example_1.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.2.0">
+ <lyrics artist="Muse" title="Hysteria">
+It&apos;s bugging me
+Grating me
+And twisting me around
+Yeah I&apos;m endlessly
+Caving in
+And turning inside out
+
+Cause I want it now
+I want it now
+Give me your heart and your soul
+And I&apos;m breaking out
+I&apos;m breaking out
+That&apos;s when she&apos;ll lose control
+
+It&apos;s holding me
+Morphing me
+And forcing me to strive
+To be endlessly
+Cold within
+And dreaming I&apos;m alive
+
+Cause I want it now
+I want it now
+Give me your heart and your soul
+I&apos;m not breaking down
+I&apos;m breaking out
+That&apos;s when she&apos;ll lose control
+
+And I want you now
+I want you now
+I&apos;ll feel my heart implode
+And I&apos;m breaking out
+Escaping now
+Feeling my faith erode
+ </lyrics>
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/musicFolders_example_1.xml b/subsonic-main/src/main/webapp/xsd/musicFolders_example_1.xml
new file mode 100644
index 00000000..8531045b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/musicFolders_example_1.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.1.1">
+
+ <musicFolders>
+ <musicFolder id="1" name="Music"/>
+ <musicFolder id="2" name="Movies"/>
+ <musicFolder id="3" name="Incoming"/>
+ </musicFolders>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/nowPlaying_example_1.xml b/subsonic-main/src/main/webapp/xsd/nowPlaying_example_1.xml
new file mode 100644
index 00000000..b492fbb7
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/nowPlaying_example_1.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <nowPlaying>
+ <entry username="sindre" minutesAgo="12" playerId="2"
+ id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ size="8421341" contentType="audio/mpeg" suffix="mp3" path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <entry username="bente" minutesAgo="1" playerId="4" playerName="Kitchen" id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac" transcodedContentType="audio/mpeg"
+ transcodedSuffix="mp3" path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </nowPlaying>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/ping_example_1.xml b/subsonic-main/src/main/webapp/xsd/ping_example_1.xml
new file mode 100644
index 00000000..b475286d
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/ping_example_1.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.1.1">
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/playlist_example_1.xml b/subsonic-main/src/main/webapp/xsd/playlist_example_1.xml
new file mode 100644
index 00000000..58966868
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/playlist_example_1.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0">
+ <playlist id="15" name="kokos" comment="fan" owner="admin" public="true" songCount="6" duration="1391"
+ created="2012-04-17T19:53:44">
+ <allowedUser>sindre</allowedUser>
+ <allowedUser>john</allowedUser>
+ <entry id="657" parent="655" title="Making Me Nervous" album="I Don&apos;t Know What I&apos;m Doing"
+ artist="Brad Sucks" isDir="false" coverArt="655" created="2008-04-10T07:10:32" duration="159"
+ bitRate="202" track="1" year="2003" size="4060113" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Brad Sucks/I Don&apos;t Know What I&apos;m Doing/01 - Making Me Nervous.mp3" albumId="58"
+ artistId="45" type="music"/>
+ <entry id="823" parent="784" title="Piano escena" album="BSO Sebastian" artist="PeerGynt Lobogris"
+ isDir="false" coverArt="784" created="2009-01-14T22:26:29" duration="129" bitRate="170" track="8"
+ year="2008" genre="Blues" size="2799954" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="PeerGynt Lobogris/BSO Sebastian/08 - Piano escena.mp3" albumId="75" artistId="54" type="music"/>
+ <entry id="748" parent="746" title="Stories from Emona II" album="Between two worlds" artist="Maya Filipi&#269;"
+ isDir="false" coverArt="746" created="2008-07-30T22:05:40" duration="335" bitRate="176" track="2"
+ year="2008" genre="Classical" size="7458214" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Maya Filipic/Between two worlds/02 - Stories from Emona II.mp3" albumId="68" artistId="51"
+ type="music"/>
+ <entry id="848" parent="827" title="Run enemy" album="Eve" artist="Shearer" isDir="false" coverArt="827"
+ created="2009-01-15T22:54:38" duration="331" bitRate="195" track="14" year="2008" genre="Rock"
+ size="8160185" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Shearer/Eve/14 - Run enemy.mp3" albumId="77" artistId="55" type="music"/>
+ <entry id="884" parent="874" title="Isolation" album="Kosmonaut" artist="Ugress" isDir="false" coverArt="874"
+ created="2009-01-14T21:34:49" duration="320" bitRate="160" track="4" year="2006" genre="Electronic"
+ size="6412176" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Ugress/Kosmonaut/Ugress-KosmonautEP-04-Isolation.mp3" albumId="81" artistId="57" type="music"/>
+ <entry id="805" parent="783" title="Bajo siete lunas (intro)" album="Broken Dreams" artist="PeerGynt Lobogris"
+ isDir="false" coverArt="783" created="2008-12-19T14:13:58" duration="117" bitRate="225" track="1"
+ year="2008" genre="Blues" size="3363271" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="PeerGynt Lobogris/Broken Dreams/01 - Bajo siete lunas (intro).mp3" albumId="74" artistId="54"
+ type="music"/>
+ </playlist>
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/playlists_example_1.xml b/subsonic-main/src/main/webapp/xsd/playlists_example_1.xml
new file mode 100644
index 00000000..9b4b60a3
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/playlists_example_1.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0">
+ <playlists>
+ <playlist id="15" name="Some random songs" comment="Just something I tossed together" owner="admin" public="false" songCount="6" duration="1391" created="2012-04-17T19:53:44">
+ <allowedUser>sindre</allowedUser>
+ <allowedUser>john</allowedUser>
+ </playlist>
+ <playlist id="16" name="More random songs" comment="No comment" owner="admin" public="true" songCount="5" duration="1018" created="2012-04-17T19:55:49"/>
+ </playlists>
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/podcasts_example_1.xml b/subsonic-main/src/main/webapp/xsd/podcasts_example_1.xml
new file mode 100644
index 00000000..23b8d2ae
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/podcasts_example_1.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.6.0">
+
+ <podcasts>
+ <channel id="1"
+ url="http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/rss.xml"
+ title="Dr Karl and the Naked Scientist"
+ description="Dr Chris Smith aka The Naked Scientist with the latest news from the world of science and Dr Karl answers listeners' science questions."
+ status="completed">
+ <episode id="34"
+ streamId="523"
+ title="Scorpions have re-evolved eyes"
+ description="This week Dr Chris fills us in on the UK's largest free science festival, plus all this week's big scientific discoveries."
+ publishDate="2011-02-03T14:46:43"
+ status="completed"
+ parent="11" isDir="false" year="2011" genre="Podcast" coverArt="24"
+ size="78421341" contentType="audio/mpeg" suffix="mp3" duration="3146" bitRate="128"
+ path="Podcast/drkarl/20110203.mp3"/>
+ <episode id="35"
+ streamId="524"
+ title="Scar tissue and snake venom treatment"
+ description="This week Dr Karl tells the gruesome tale of a surgeon who operated on himself."
+ publishDate="2011-09-03T16:47:52"
+ status="completed"
+ parent="11" isDir="false" year="2011" genre="Podcast" coverArt="27"
+ size="45624671" contentType="audio/mpeg" suffix="mp3" duration="3099" bitRate="128"
+ path="Podcast/drkarl/20110903.mp3"/>
+ </channel>
+ <channel id="2"
+ url="http://podkast.nrk.no/program/herreavdelingen.rss"
+ title="NRK P1 - Herreavdelingen"
+ description="Et program der herrene Yan Friis og Finn Bjelke møtes og musikk nytes."
+ status="completed">
+ </channel>
+ <channel id="3"
+ url="http://foo.bar.com/xyz.rss"
+ status="error"
+ errorMessage="Not found."/>
+ </podcasts>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/randomSongs_example_1.xml b/subsonic-main/src/main/webapp/xsd/randomSongs_example_1.xml
new file mode 100644
index 00000000..dc3a6a99
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/randomSongs_example_1.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <randomSongs>
+ <song id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ size="8421341" contentType="audio/mpeg" suffix="mp3" duration="146" bitRate="128"
+ path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <song id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="208" bitRate="128"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </randomSongs>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/searchResult2_example_1.xml b/subsonic-main/src/main/webapp/xsd/searchResult2_example_1.xml
new file mode 100644
index 00000000..dd7a2c10
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/searchResult2_example_1.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <searchResult2>
+ <artist id="1" name="ABBA"/>
+ <album id="11" parent="1" title="Arrival" artist="ABBA" isDir="true" coverArt="22"/>
+ <album id="12" parent="1" title="Super Trouper" artist="ABBA" isDir="true" coverArt="23"/>
+ <song id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </searchResult2>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/searchResult3_example_1.xml b/subsonic-main/src/main/webapp/xsd/searchResult3_example_1.xml
new file mode 100644
index 00000000..ef1b1df9
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/searchResult3_example_1.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0">
+ <searchResult3>
+ <artist id="5944" name="Black" coverArt="ar-5944" albumCount="2"/>
+ <artist id="5785" name="Black Sabbath" coverArt="ar-5785" albumCount="22"/>
+ <artist id="5945" name="Black Debbath" coverArt="ar-5945" albumCount="7"/>
+ <artist id="6063" name="Mary Black" coverArt="ar-6063" albumCount="1"/>
+ <artist id="6065" name="Frances Black" coverArt="ar-6065" albumCount="1"/>
+ <artist id="6131" name="Black Box" coverArt="ar-6131" albumCount="1"/>
+ <artist id="6973" name="The Black Crowes" coverArt="ar-6973" albumCount="2"/>
+ <artist id="6974" name="The Black Sorrows" coverArt="ar-6974" albumCount="1"/>
+ <artist id="6061" name="Eleanor Mcevoy With Mary Black" coverArt="ar-6061" albumCount="1"/>
+ <album id="11241" name="Black" coverArt="al-11241" songCount="10" created="2004-11-14T13:02:03" duration="2575" artist="Black" artistId="5944"/>
+ <album id="12768" name="Black" coverArt="al-12768" songCount="1" created="2000-07-31T15:13:50" duration="448" artist="Metallica" artistId="6308"/>
+ <album id="11242" name="Black Debbath Hyller Kvinnen!" coverArt="al-11242" songCount="11" created="2010-05-25T13:04:41" duration="2778" artist="Black Debbath" artistId="5945"/>
+ <album id="11047" name="Back In Black" coverArt="al-11047" songCount="10" created="2004-11-08T23:33:11" duration="2534" artist="AC/DC" artistId="5432"/>
+ <album id="11048" name="Black Ice" coverArt="al-11048" songCount="15" created="2008-10-30T09:20:52" duration="3332" artist="AC/DC" artistId="5432"/>
+ <album id="11615" name="The Black Parade" coverArt="al-11615" songCount="15" created="2007-06-21T07:52:46" duration="3356" artist="My Chemical Romance" artistId="6159"/>
+ <album id="12132" name="Black Celebration" coverArt="al-12132" songCount="10" created="2005-01-18T23:19:33" duration="2225" artist="Depeche Mode" artistId="6355"/>
+ <album id="12544" name="The Black Halo" coverArt="al-12544" songCount="3" created="2010-04-07T13:41:39" duration="668" artist="Kamelot" artistId="5433"/>
+ <album id="12771" name="The Black Album" coverArt="al-12771" songCount="1" created="1999-10-15T00:00:00" duration="386" artist="Metallica" artistId="6308"/>
+ <album id="13554" name="Black Angel" coverArt="al-13554" songCount="12" created="2002-11-16T15:37:20" duration="3160" artist="Savage Rose" artistId="6999"/>
+ <album id="13609" name="The Black Rider" coverArt="al-13609" songCount="20" created="2002-02-22T14:50:34" duration="3327" artist="Tom Waits" artistId="6920"/>
+ <album id="11243" name="Black Debbaths Beste-Ti &#197;r Med Rock Mot Alt Som Er Kult" coverArt="al-11243" songCount="27" created="2009-05-27T14:43:12" duration="6710" artist="Black Debbath" artistId="5945"/>
+ <album id="11250" name="Black sabbath &amp; Rob Halford live 11-15-92 Costa mesa" coverArt="al-11250" songCount="13" created="2003-07-21T13:52:14" duration="4500" artist="Black Sabbath" artistId="5785"/>
+ <album id="10490" name="The Black Halo [Bonus Track]" coverArt="al-10490" songCount="42" created="2010-04-07T12:58:16" duration="10305" artist="Kamelot" artistId="5433"/>
+ <album id="10559" name="Back to Black: 1900-1999" coverArt="al-10559" songCount="1" created="2004-03-09T07:32:42" duration="99" artist="Earth, Wind &amp; Fire, The Emotions" artistId="5489"/>
+ <album id="11087" name="Back To Black (Deluxe Edition)" coverArt="al-11087" songCount="19" created="2008-06-25T10:01:30" duration="3663" artist="Amy Winehouse" artistId="5870"/>
+ <album id="11604" name="Black Holes &amp; Revelations" coverArt="al-11604" songCount="12" created="2007-06-21T07:52:44" duration="3025" artist="Muse" artistId="6150"/>
+ <album id="12993" name="Black Market Music" coverArt="al-12993" songCount="1" created="2003-07-21T14:48:38" duration="233" artist="Placebo" artistId="6313"/>
+ <album id="13580" name="Black Rose [Remastered]" coverArt="al-13580" songCount="9" created="2009-07-06T08:55:22" duration="2324" artist="Thin Lizzy" artistId="6084"/>
+ <album id="13904" name="Down Under The Black Light" songCount="1" created="2003-10-07T07:22:26" duration="90" artist="The Molecules" artistId="7202"/>
+ <song id="77451" parent="77433" title="Black" album="Angry Machines" artist="Dio" isDir="false" coverArt="77433" created="2007-03-15T06:46:06" duration="190" bitRate="192" track="3" year="1996" genre="Hard Rock" size="4575589" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Dio/Angry Machines/Angry Machines 2.mp3" albumId="12168" artistId="6357" type="music"/>
+ <song id="84902" parent="84883" title="Black" album="Ten" artist="Pearl Jam" isDir="false" coverArt="84883" created="2001-06-11T22:15:52" duration="344" bitRate="160" year="1992" genre="Rock" size="6882991" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Pearl Jam/Ten/05 - Black.mp3" albumId="12944" artistId="6745" type="music"/>
+ <song id="84916" parent="84884" title="Black" album="Unplugged at MTV" artist="Pearl Jam" isDir="false" coverArt="84884" created="1999-07-18T13:40:38" duration="449" bitRate="128" size="7180644" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Pearl Jam/Unplugged at MTV/4- Pearl Jam Unplugged - Black.mp3" albumId="12945" artistId="6745" type="music"/>
+ <song id="85038" parent="85036" title="Black" album="Musicforthemorningafter" artist="Pete Yorn" isDir="false" coverArt="85036" created="2004-11-28T00:42:29" duration="250" bitRate="192" track="4" year="2001" genre="Rock/Pop" size="6026430" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Pete Yorn/Pete Yorn/Pete Yorn - 04 - Black.mp3" albumId="12951" artistId="6747" type="music"/>
+ <song id="73890" parent="73888" title="Black Sabbath" album="Best Of...CD1" artist="Black Sabbath" isDir="false" coverArt="73888" created="2003-07-21T12:19:32" duration="380" bitRate="160" track="1" genre="Hard Rock" size="8124439" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Black Sabbath/Best Of Black Sabbath/CD1/01 - Black Sabbath.mp3" albumId="11248" artistId="5785" type="music"/>
+ <song id="73934" parent="73875" title="Black Moon" album="Headless Cross" artist="Black Sabbath" isDir="false" coverArt="73875" created="2002-09-05T23:00:52" duration="246" bitRate="128" track="7" genre="(255)" size="3949687" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Black Sabbath/Headless Cross/07 - Black Moon.mp3" albumId="11251" artistId="5785" type="music"/>
+ <song id="73844" parent="73790" title="Datidas Black Debbath" album="Naar Vi D&#248;de Rocker" artist="Black Debbath" isDir="false" coverArt="73790" created="2010-05-03T09:40:22" duration="192" bitRate="320" track="7" year="2006" genre="Rock" size="7704160" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Black Debbath/N&#229;r vi d&#248;de rocker/Black Debbath - Datidas Black Debbath.mp3" albumId="11245" artistId="5945" type="music"/>
+ <song id="73924" parent="73874" title="Black Sabbath (with Ozzy)" album="Black sabbath &amp; Rob Halford live 11-15-92 Costa mesa" artist="Black Sabbath" isDir="false" coverArt="73874" created="2003-07-21T13:54:14" duration="407" bitRate="128" track="10" genre="Hard Rock" size="6518578" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Black Sabbath/Black sabbath &amp; Rob Halford live 11-15-92 Costa mesa/10 - Black Sabbath (with Ozzy).mp3" albumId="11250" artistId="5785" type="music"/>
+ <song id="75272" parent="75055" title="Black Sabbath - Paranoid" album="All Time Greatest Rock Songs (Disc 1)" artist="Black Sabbath" isDir="false" coverArt="75055" created="2002-11-23T19:31:34" duration="169" bitRate="160" track="18" genre="General Rock" size="3379328" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Compilations/All Time Greatest Rock Songs (CD 1)/18 - Black Sabbath - Paranoid.mp3" albumId="11482" artistId="5785" type="music"/>
+ <song id="89111" parent="89093" title="Black Moon Creeping" album="The Southern Harmony And Musical Companion" artist="The Black Crowes" isDir="false" coverArt="89093" created="2002-11-03T13:51:24" duration="294" bitRate="160" track="7" size="5891285" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="The Black Crowes/The Southern Harmony And Musical Companion/07 - Black Moon Creeping.mp3" albumId="13473" artistId="6973" type="music"/>
+ <song id="92435" parent="92347" title="Belle Epoque - Black Is Black" album="Cd4" artist="Summer Hits - Top 100" isDir="false" created="1999-07-18T10:48:14" duration="204" bitRate="160" size="4087948" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="_Various/Summer Hits - Top 100/Cd4/16. Belle Epoque - Black Is Black.mp3" albumId="13948" artistId="7223" type="music"/>
+ <song id="70221" parent="70211" title="The Black Halo" album="The Black Halo [Bonus Track]" artist="Kamelot" isDir="false" coverArt="70211" created="2010-04-07T12:58:17" duration="223" bitRate="192" track="10" year="2005" genre="Progressive Metal" size="5363128" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Incoming/Kamelot/08 - The Black Halo - 2005/10 - The Black Halo.mp3" albumId="10490" artistId="5433" type="music"/>
+ <song id="71056" parent="70678" title="Black Coffee" album="Saints &amp; Sinners" artist="All Saints" isDir="false" coverArt="70678" created="2004-03-09T07:34:28" duration="290" bitRate="192" track="5" year="2000" genre="Rock/Pop" size="6975616" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Absolute/Absolute music 36/Absolute Music 36 - 12 All Saints - Black Coffee.mp3" albumId="10811" artistId="5692" type="music"/>
+ <song id="71393" parent="71375" title="Back In Black" album="Back In Black" artist="AC/DC" isDir="false" coverArt="71375" created="2004-11-08T23:33:11" duration="268" bitRate="128" genre="Blues" size="4292608" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/Back in black/ACDC - Back In Black.mp3" albumId="11047" artistId="5432" type="music"/>
+ <song id="71415" parent="71376" title="Black Ice" album="Black Ice" artist="AC/DC" isDir="false" coverArt="71376" created="2008-10-30T10:08:28" duration="205" bitRate="320" track="15" year="2008" genre="Heavy Metal" size="8210893" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/Black Ice/15 AC-DC - Black Ice.mp3" albumId="11048" artistId="5432" type="music"/>
+ <song id="71495" parent="71385" title="Back In Black" album="Live - Special Collector&apos;s Edition" artist="AC/DC" isDir="false" coverArt="71385" created="2004-11-08T23:37:09" duration="266" bitRate="128" year="1992" genre="Metal" size="4255615" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="ACDC/Live (CD1)/ac dc - live - special collector edition - disc 1 - 03 - ba.mp3" albumId="11057" artistId="5432" type="music"/>
+ <song id="71763" parent="71758" title="Back To Black" album="Back To Black (Deluxe Edition)" artist="Amy Winehouse" isDir="false" coverArt="71758" created="2008-06-25T10:12:14" duration="241" bitRate="153" track="5" year="2007" genre="Blues" size="4639525" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Amy Winehouse/Back To Black/105-amy_winehouse-back_to_black-ukp.mp3" albumId="11087" artistId="5870" type="music"/>
+ <song id="75149" parent="75052" title="Black Sabbath" album="70" artist="Paranoid" isDir="false" coverArt="75052" created="2004-11-14T13:38:38" duration="169" bitRate="160" track="18" genre="Rock" size="3406066" suffix="wma" contentType="audio/x-ms-wma" isVideo="false" path="Compilations/70/Black Sabbath - Paranoid.wma" albumId="11396" artistId="6010" type="music"/>
+ <song id="75301" parent="75057" title="Black Betty" album="Born To Be Wild - Vol. 2" artist="Ram Jam" isDir="false" coverArt="75057" created="2006-02-04T20:30:44" duration="239" bitRate="160" track="12" year="1994" genre="Hard Rock" size="4792458" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Compilations/Born To Be Wild - Vol. 2/12 - Black Betty.mp3" albumId="11510" artistId="6091" type="music"/>
+ <song id="76048" parent="75100" title="Black Velvet" album="The Good" artist="Alannah Myles" isDir="false" coverArt="75100" created="2002-11-17T15:44:08" duration="289" bitRate="128" genre="Other" size="4628480" suffix="mp3" contentType="audio/mpeg" isVideo="false" path="Compilations/The Good/Alannah Myles - Black Velvet.mp3" albumId="11818" artistId="6286" type="music"/>
+ </searchResult3>
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/searchResult_example_1.xml b/subsonic-main/src/main/webapp/xsd/searchResult_example_1.xml
new file mode 100644
index 00000000..fb1e9c4a
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/searchResult_example_1.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.4.0">
+
+ <searchResult offset="0" totalHits="2">
+ <match id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ size="8421341" contentType="audio/mpeg" suffix="mp3"
+ path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <match id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </searchResult>
+
+</subsonic-response>
+
diff --git a/subsonic-main/src/main/webapp/xsd/shares_example_1.xml b/subsonic-main/src/main/webapp/xsd/shares_example_1.xml
new file mode 100644
index 00000000..1fb8732f
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/shares_example_1.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.6.0">
+
+ <shares>
+ <share id="1" url="http://sindre.subsonic.org/share/sKoYn" description="Check this out" username="sindre"
+ created="2011-06-04T12:34:56" lastVisited="2011-06-04T13:14:15" expires="2013-06-04T00:00:00" visitCount="0">
+
+ <entry id="111" parent="11" title="Dancing Queen" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="24"
+ size="8421341" contentType="audio/mpeg" suffix="mp3" duration="146" bitRate="128"
+ path="ABBA/Arrival/Dancing Queen.mp3"/>
+
+ <entry id="112" parent="11" title="Money, Money, Money" isDir="false"
+ album="Arrival" artist="ABBA" track="7" year="1978" genre="Pop" coverArt="25"
+ size="4910028" contentType="audio/flac" suffix="flac"
+ transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="208" bitRate="128"
+ path="ABBA/Arrival/Money, Money, Money.mp3"/>
+ </share>
+ </shares>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/song_example_1.xml b/subsonic-main/src/main/webapp/xsd/song_example_1.xml
new file mode 100644
index 00000000..58b8773b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/song_example_1.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.8.0">
+
+ <song id="48228" parent="48203" title="You Shook Me All Night Long" album="Back In Black" artist="AC/DC"
+ isDir="false" coverArt="48203" created="2004-11-08T23:33:11" duration="210" bitRate="112" size="2945619"
+ suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="ACDC/Back in black/ACDC - You Shook Me All Night Long.mp3"/>
+
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/starred2_example_1.xml b/subsonic-main/src/main/webapp/xsd/starred2_example_1.xml
new file mode 100644
index 00000000..8c0104dd
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/starred2_example_1.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0">
+ <starred2>
+ <artist id="126" name="Iron Maiden" coverArt="ar-126" albumCount="1" starred="2012-04-05T19:03:31"/>
+ <artist id="133" name="Dimmu Borgir" coverArt="ar-133" albumCount="1" starred="2012-04-05T19:03:17"/>
+ <artist id="141" name="Kvelertak" albumCount="1" starred="2012-04-05T19:03:05"/>
+ <album id="180" name="Collapse Into Now" artist="R.E.M." artistId="144" songCount="12" duration="2459"
+ created="2011-03-23T09:37:55" starred="2012-04-05T19:02:02"/>
+ <album id="178" name="Postcards From A Young Man" artist="Manic Street Preachers" artistId="142"
+ coverArt="al-178" songCount="12" duration="2665" created="2011-02-26T10:47:19"
+ starred="2012-04-05T19:01:03"/>
+ <song id="143588" parent="143586" title="Born Treacherous" album="Abrahadabra" artist="Dimmu Borgir"
+ isDir="false" coverArt="143586" created="2010-09-27T20:52:23" starred="2012-04-02T17:17:01" duration="302"
+ bitRate="320" track="2" year="2010" genre="Scene-core" size="12087601" suffix="mp3"
+ contentType="audio/mpeg" isVideo="false" path="Dimmu Borgir/Abrahadabra/02 - Born Treacherous.mp3"
+ albumId="163" artistId="133" type="music"/>
+ <song id="143600" parent="143386" title="Satellite 15....The Final Frontier"
+ album="The Final Frontier (Mission Edition)" artist="Iron Maiden" isDir="false" coverArt="143386"
+ created="2010-08-16T21:08:01" starred="2012-04-02T14:12:54" duration="521" bitRate="320" track="1"
+ year="2010" genre="Heavy Metal" size="21855635" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Iron Maiden/2010 The Final Frontier/01 Satellite 15....The Final Frontier.mp3" albumId="156"
+ artistId="126" type="music"/>
+ <song id="143604" parent="143386" title="The Alchemist" album="The Final Frontier (Mission Edition)"
+ artist="Iron Maiden" isDir="false" coverArt="143386" created="2010-08-16T21:07:51"
+ starred="2012-04-02T14:12:52" duration="269" bitRate="320" track="5" year="2010" genre="Heavy Metal"
+ size="11774455" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Iron Maiden/2010 The Final Frontier/05 The Alchemist.mp3" albumId="156" artistId="126"
+ type="music"/>
+ </starred2>
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/starred_example_1.xml b/subsonic-main/src/main/webapp/xsd/starred_example_1.xml
new file mode 100644
index 00000000..916b2a7b
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/starred_example_1.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0">
+ <starred>
+ <artist name="Kvelertak" id="143408"/>
+ <artist name="Dimmu Borgir" id="143402"/>
+ <artist name="Iron Maiden" id="143403"/>
+ <album id="143862" parent="143410" title="Postcards From A Young Man" album="Postcards From A Young Man"
+ artist="Manic Street Preachers" isDir="true" coverArt="143862" created="2011-02-26T10:45:30"
+ starred="2012-04-05T18:40:08"/>
+ <album id="143888" parent="143412" title="Collapse Into Now" album="Collapse Into Now" artist="R.E.M."
+ isDir="true" created="2011-03-23T09:29:13" starred="2012-04-05T18:40:02"/>
+ <song id="143588" parent="143586" title="Born Treacherous" album="Abrahadabra" artist="Dimmu Borgir"
+ isDir="false" coverArt="143586" created="2010-09-27T20:52:23" starred="2012-04-02T17:17:01" duration="302"
+ bitRate="320" track="2" year="2010" genre="Scene-core" size="12087601" suffix="mp3"
+ contentType="audio/mpeg" isVideo="false" path="Dimmu Borgir/Abrahadabra/02 - Born Treacherous.mp3"
+ albumId="163" artistId="133" type="music"/>
+ <song id="143600" parent="143386" title="Satellite 15....The Final Frontier"
+ album="The Final Frontier (Mission Edition)" artist="Iron Maiden" isDir="false" coverArt="143386"
+ created="2010-08-16T21:08:01" starred="2012-04-02T14:12:54" duration="521" bitRate="320" track="1"
+ year="2010" genre="Heavy Metal" size="21855635" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Iron Maiden/2010 The Final Frontier/01 Satellite 15....The Final Frontier.mp3" albumId="156"
+ artistId="126" type="music"/>
+ <song id="143604" parent="143386" title="The Alchemist" album="The Final Frontier (Mission Edition)"
+ artist="Iron Maiden" isDir="false" coverArt="143386" created="2010-08-16T21:07:51"
+ starred="2012-04-02T14:12:52" duration="269" bitRate="320" track="5" year="2010" genre="Heavy Metal"
+ size="11774455" suffix="mp3" contentType="audio/mpeg" isVideo="false"
+ path="Iron Maiden/2010 The Final Frontier/05 The Alchemist.mp3" albumId="156" artistId="126"
+ type="music"/>
+ </starred>
+</subsonic-response>
diff --git a/subsonic-main/src/main/webapp/xsd/subsonic-rest-api.xsd b/subsonic-main/src/main/webapp/xsd/subsonic-rest-api.xsd
new file mode 100644
index 00000000..449b49e8
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/subsonic-rest-api.xsd
@@ -0,0 +1,434 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:sub="http://subsonic.org/restapi"
+ targetNamespace="http://subsonic.org/restapi"
+ attributeFormDefault="unqualified"
+ elementFormDefault="qualified"
+ version="1.8.0">
+
+ <xs:element name="subsonic-response" type="sub:Response"/>
+
+ <xs:complexType name="Response">
+ <xs:choice minOccurs="0" maxOccurs="1">
+ <xs:element name="musicFolders" type="sub:MusicFolders" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="indexes" type="sub:Indexes" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="directory" type="sub:Directory" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="artists" type="sub:ArtistsID3" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="artist" type="sub:ArtistWithAlbumsID3" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="album" type="sub:AlbumWithSongsID3" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="song" type="sub:Child" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="videos" type="sub:Videos" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="nowPlaying" type="sub:NowPlaying" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="searchResult" type="sub:SearchResult" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="searchResult2" type="sub:SearchResult2" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="searchResult3" type="sub:SearchResult3" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="playlists" type="sub:Playlists" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="playlist" type="sub:PlaylistWithSongs" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="jukeboxStatus" type="sub:JukeboxStatus" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="jukeboxPlaylist" type="sub:JukeboxPlaylist" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="license" type="sub:License" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="user" type="sub:User" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="chatMessages" type="sub:ChatMessages" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="albumList" type="sub:AlbumList" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="albumList2" type="sub:AlbumList2" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="randomSongs" type="sub:RandomSongs" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="lyrics" type="sub:Lyrics" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="podcasts" type="sub:Podcasts" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="shares" type="sub:Shares" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="starred" type="sub:Starred" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="starred2" type="sub:Starred2" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="error" type="sub:Error" minOccurs="1" maxOccurs="1"/>
+ </xs:choice>
+ <xs:attribute name="status" type="sub:ResponseStatus" use="required"/>
+ <xs:attribute name="version" type="sub:Version" use="required"/>
+ </xs:complexType>
+
+ <xs:simpleType name="ResponseStatus">
+ <xs:restriction base="xs:string">
+ <xs:enumeration value="ok"/>
+ <xs:enumeration value="failed"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="Version">
+ <xs:restriction base="xs:string">
+ <xs:pattern value="\d+\.\d+\.\d+"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:complexType name="MusicFolders">
+ <xs:sequence>
+ <xs:element name="musicFolder" type="sub:MusicFolder" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="MusicFolder">
+ <xs:attribute name="id" type="xs:int" use="required"/>
+ <xs:attribute name="name" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="Indexes">
+ <xs:sequence>
+ <xs:element name="shortcut" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="index" type="sub:Index" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/> <!-- Added in 1.7.0 -->
+ </xs:sequence>
+ <xs:attribute name="lastModified" type="xs:long" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="Index">
+ <xs:sequence>
+ <xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="Artist">
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="ArtistsID3">
+ <xs:sequence>
+ <xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="ArtistID3">
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="coverArt" type="xs:string" use="optional"/>
+ <xs:attribute name="albumCount" type="xs:int" use="required"/>
+ <xs:attribute name="starred" type="xs:dateTime" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="ArtistWithAlbumsID3">
+ <xs:complexContent>
+ <xs:extension base="sub:ArtistID3">
+ <xs:sequence>
+ <xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
+ <xs:complexType name="AlbumID3">
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="artist" type="xs:string" use="optional"/>
+ <xs:attribute name="artistId" type="xs:string" use="optional"/>
+ <xs:attribute name="coverArt" type="xs:string" use="optional"/>
+ <xs:attribute name="songCount" type="xs:int" use="required"/>
+ <xs:attribute name="duration" type="xs:int" use="required"/>
+ <xs:attribute name="created" type="xs:dateTime" use="required"/>
+ <xs:attribute name="starred" type="xs:dateTime" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="AlbumWithSongsID3">
+ <xs:complexContent>
+ <xs:extension base="sub:AlbumID3">
+ <xs:sequence>
+ <xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
+ <xs:complexType name="Videos">
+ <xs:sequence>
+ <xs:element name="video" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="Directory">
+ <xs:sequence>
+ <xs:element name="child" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="parent" type="xs:string" use="optional"/>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="Child">
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="parent" type="xs:string" use="optional"/>
+ <xs:attribute name="isDir" type="xs:boolean" use="required"/>
+ <xs:attribute name="title" type="xs:string" use="required"/>
+ <xs:attribute name="album" type="xs:string" use="optional"/>
+ <xs:attribute name="artist" type="xs:string" use="optional"/>
+ <xs:attribute name="track" type="xs:int" use="optional"/>
+ <xs:attribute name="year" type="xs:int" use="optional"/>
+ <xs:attribute name="genre" type="xs:string" use="optional"/>
+ <xs:attribute name="coverArt" type="xs:string" use="optional"/>
+ <xs:attribute name="size" type="xs:long" use="optional"/>
+ <xs:attribute name="contentType" type="xs:string" use="optional"/>
+ <xs:attribute name="suffix" type="xs:string" use="optional"/>
+ <xs:attribute name="transcodedContentType" type="xs:string" use="optional"/>
+ <xs:attribute name="transcodedSuffix" type="xs:string" use="optional"/>
+ <xs:attribute name="duration" type="xs:int" use="optional"/>
+ <xs:attribute name="bitRate" type="xs:int" use="optional"/>
+ <xs:attribute name="path" type="xs:string" use="optional"/>
+ <xs:attribute name="isVideo" type="xs:boolean" use="optional"/> <!-- Added in 1.4.1 -->
+ <xs:attribute name="userRating" type="sub:UserRating" use="optional"/> <!-- Added in 1.6.0 -->
+ <xs:attribute name="averageRating" type="sub:AverageRating" use="optional"/> <!-- Added in 1.6.0 -->
+ <xs:attribute name="discNumber" type="xs:int" use="optional"/> <!-- Added in 1.8.0 -->
+ <xs:attribute name="created" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
+ <xs:attribute name="starred" type="xs:dateTime" use="optional"/> <!-- Added in 1.8.0 -->
+ <xs:attribute name="albumId" type="xs:string" use="optional"/> <!-- Added in 1.8.0 -->
+ <xs:attribute name="artistId" type="xs:string" use="optional"/> <!-- Added in 1.8.0 -->
+ <xs:attribute name="type" type="sub:MediaType" use="optional"/> <!-- Added in 1.8.0 -->
+ </xs:complexType>
+
+ <xs:simpleType name="MediaType">
+ <xs:restriction base="xs:string">
+ <xs:enumeration value="music"/>
+ <xs:enumeration value="podcast"/>
+ <xs:enumeration value="audiobook"/>
+ <xs:enumeration value="video"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="UserRating">
+ <xs:restriction base="xs:int">
+ <xs:minInclusive value="1"/>
+ <xs:maxInclusive value="5"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="AverageRating">
+ <xs:restriction base="xs:double">
+ <xs:minInclusive value="1.0"/>
+ <xs:maxInclusive value="5.0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:complexType name="NowPlaying">
+ <xs:sequence>
+ <xs:element name="entry" type="sub:NowPlayingEntry" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="NowPlayingEntry">
+ <xs:complexContent>
+ <xs:extension base="sub:Child">
+ <xs:attribute name="username" type="xs:string" use="required"/>
+ <xs:attribute name="minutesAgo" type="xs:int" use="required"/>
+ <xs:attribute name="playerId" type="xs:int" use="required"/>
+ <xs:attribute name="playerName" type="xs:string" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
+ <!--Deprecated-->
+ <xs:complexType name="SearchResult">
+ <xs:sequence>
+ <xs:element name="match" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="offset" type="xs:int" use="required"/>
+ <xs:attribute name="totalHits" type="xs:int" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="SearchResult2">
+ <xs:sequence>
+ <xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="SearchResult3">
+ <xs:sequence>
+ <xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="Playlists">
+ <xs:sequence>
+ <xs:element name="playlist" type="sub:Playlist" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="Playlist">
+ <xs:sequence>
+ <xs:element name="allowedUser" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> <!--Added in 1.8.0-->
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="name" type="xs:string" use="required"/>
+ <xs:attribute name="comment" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
+ <xs:attribute name="owner" type="xs:string" use="optional"/> <!--Added in 1.8.0-->
+ <xs:attribute name="public" type="xs:boolean" use="optional"/> <!--Added in 1.8.0-->
+ <xs:attribute name="songCount" type="xs:int" use="required"/> <!--Added in 1.8.0-->
+ <xs:attribute name="duration" type="xs:int" use="required"/> <!--Added in 1.8.0-->
+ <xs:attribute name="created" type="xs:dateTime" use="required"/> <!--Added in 1.8.0-->
+ </xs:complexType>
+
+ <xs:complexType name="PlaylistWithSongs">
+ <xs:complexContent>
+ <xs:extension base="sub:Playlist">
+ <xs:sequence>
+ <xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
+ <xs:complexType name="JukeboxStatus">
+ <xs:attribute name="currentIndex" type="xs:int" use="required"/>
+ <xs:attribute name="playing" type="xs:boolean" use="required"/>
+ <xs:attribute name="gain" type="xs:float" use="required"/>
+ <xs:attribute name="position" type="xs:int" use="optional"/> <!--Added in 1.7.0-->
+ </xs:complexType>
+
+ <xs:complexType name="JukeboxPlaylist">
+ <xs:complexContent>
+ <xs:extension base="sub:JukeboxStatus">
+ <xs:sequence>
+ <xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
+ <xs:complexType name="ChatMessages">
+ <xs:sequence>
+ <xs:element name="chatMessage" type="sub:ChatMessage" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="ChatMessage">
+ <xs:attribute name="username" type="xs:string" use="required"/>
+ <xs:attribute name="time" type="xs:long" use="required"/>
+ <xs:attribute name="message" type="xs:string" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="AlbumList">
+ <xs:sequence>
+ <xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="AlbumList2">
+ <xs:sequence>
+ <xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="RandomSongs">
+ <xs:sequence>
+ <xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="Lyrics" mixed="true">
+ <xs:attribute name="artist" type="xs:string" use="optional"/>
+ <xs:attribute name="title" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="Podcasts">
+ <xs:sequence>
+ <xs:element name="channel" type="sub:PodcastChannel" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="PodcastChannel">
+ <xs:sequence>
+ <xs:element name="episode" type="sub:PodcastEpisode" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="url" type="xs:string" use="required"/>
+ <xs:attribute name="title" type="xs:string" use="optional"/>
+ <xs:attribute name="description" type="xs:string" use="optional"/>
+ <xs:attribute name="status" type="sub:PodcastStatus" use="required"/>
+ <xs:attribute name="errorMessage" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="PodcastEpisode">
+ <xs:complexContent>
+ <xs:extension base="sub:Child">
+ <xs:attribute name="streamId" type="xs:string" use="optional"/> <!-- Use this ID for streaming the podcast. -->
+ <xs:attribute name="description" type="xs:string" use="optional"/>
+ <xs:attribute name="status" type="sub:PodcastStatus" use="required"/>
+ <xs:attribute name="publishDate" type="xs:dateTime" use="optional"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
+ <xs:simpleType name="PodcastStatus">
+ <xs:restriction base="xs:string">
+ <xs:enumeration value="new"/>
+ <xs:enumeration value="downloading"/>
+ <xs:enumeration value="completed"/>
+ <xs:enumeration value="error"/>
+ <xs:enumeration value="deleted"/>
+ <xs:enumeration value="skipped"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:complexType name="Shares">
+ <xs:sequence>
+ <xs:element name="share" type="sub:Share" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="Share">
+ <xs:sequence>
+ <xs:element name="entry" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="id" type="xs:string" use="required"/>
+ <xs:attribute name="url" type="xs:string" use="required"/>
+ <xs:attribute name="description" type="xs:string" use="optional"/>
+ <xs:attribute name="username" type="xs:string" use="required"/>
+ <xs:attribute name="created" type="xs:dateTime" use="required"/>
+ <xs:attribute name="expires" type="xs:dateTime" use="optional"/>
+ <xs:attribute name="lastVisited" type="xs:dateTime" use="optional"/>
+ <xs:attribute name="visitCount" type="xs:int" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="Starred">
+ <xs:sequence>
+ <xs:element name="artist" type="sub:Artist" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="album" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="Starred2">
+ <xs:sequence>
+ <xs:element name="artist" type="sub:ArtistID3" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="album" type="sub:AlbumID3" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="song" type="sub:Child" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+
+ <xs:complexType name="License">
+ <xs:attribute name="valid" type="xs:boolean" use="required"/>
+ <xs:attribute name="email" type="xs:string" use="optional"/>
+ <xs:attribute name="key" type="xs:string" use="optional"/>
+ <xs:attribute name="date" type="xs:dateTime" use="optional"/>
+ </xs:complexType>
+
+ <xs:complexType name="User">
+ <xs:attribute name="username" type="xs:string" use="required"/>
+ <xs:attribute name="email" type="xs:string" use="optional"/> <!-- Added in 1.6.0 -->
+ <xs:attribute name="scrobblingEnabled" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
+ <xs:attribute name="adminRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="settingsRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="downloadRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="uploadRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="playlistRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="coverArtRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="commentRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="podcastRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="streamRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="jukeboxRole" type="xs:boolean" use="required"/>
+ <xs:attribute name="shareRole" type="xs:boolean" use="required"/> <!-- Added in 1.7.0 -->
+ </xs:complexType>
+
+ <xs:complexType name="Error">
+ <xs:attribute name="code" type="xs:int" use="required"/>
+ <xs:attribute name="message" type="xs:string" use="optional"/>
+ </xs:complexType>
+
+</xs:schema> \ No newline at end of file
diff --git a/subsonic-main/src/main/webapp/xsd/user_example_1.xml b/subsonic-main/src/main/webapp/xsd/user_example_1.xml
new file mode 100644
index 00000000..9b96db67
--- /dev/null
+++ b/subsonic-main/src/main/webapp/xsd/user_example_1.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<subsonic-response xmlns="http://subsonic.org/restapi"
+ status="ok" version="1.7.0">
+
+ <user username="sindre" email="sindre@activeobjects.no" scrobblingEnabled="true" adminRole="false" settingsRole="true" downloadRole="true" uploadRole="false" playlistRole="true"
+ coverArtRole="true" commentRole="true" podcastRole="true" streamRole="true" jukeboxRole="true" shareRole="false"/>
+
+</subsonic-response>
+