From 84df4e3b94cb60b7b710e8856c89d07a107047ec Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Sat, 23 Nov 2019 12:33:39 -0700 Subject: [PATCH] Add support for ListenBrainz Audio Scrobbler Service - Add textbox in user settings page for ListenBrainz token - Add changes to db - Add db colume to store MusicBrainz Recording ID - Add db colume to store ListenBrainz token - Add test for reading id - Add tag on testing file - Add localization entry Signed-off-by: Shen-Ta Hsieh Signed-off-by: Andrew DeMaria --- .../command/PersonalSettingsCommand.java | 18 ++ .../PersonalSettingsController.java | 4 + .../org/airsonic/player/dao/MediaFileDao.java | 12 +- .../java/org/airsonic/player/dao/UserDao.java | 7 +- .../org/airsonic/player/domain/MediaFile.java | 12 +- .../airsonic/player/domain/UserSettings.java | 18 ++ .../player/service/AudioScrobblerService.java | 9 + .../player/service/MediaFileService.java | 1 + .../player/service/SettingsService.java | 2 + .../service/metadata/JaudiotaggerParser.java | 1 + .../player/service/metadata/MetaData.java | 9 + .../scrobbler/ListenBrainzScrobbler.java | 230 ++++++++++++++++++ .../10.6/add-media-file-mb-recording-id.xml | 17 ++ .../resources/liquibase/10.6/changelog.xml | 7 + .../liquibase/10.6/support-listenbrainz.xml | 18 ++ .../main/resources/liquibase/db-changelog.xml | 1 + .../player/i18n/ResourceBundle_bg.properties | 2 + .../player/i18n/ResourceBundle_ca.properties | 2 + .../player/i18n/ResourceBundle_cs.properties | 2 + .../player/i18n/ResourceBundle_da.properties | 2 + .../player/i18n/ResourceBundle_de.properties | 2 + .../player/i18n/ResourceBundle_el.properties | 2 + .../player/i18n/ResourceBundle_en.properties | 2 + .../i18n/ResourceBundle_en_GB.properties | 2 + .../player/i18n/ResourceBundle_es.properties | 2 + .../player/i18n/ResourceBundle_et.properties | 2 + .../player/i18n/ResourceBundle_fi.properties | 2 + .../player/i18n/ResourceBundle_fr.properties | 2 + .../player/i18n/ResourceBundle_is.properties | 2 + .../player/i18n/ResourceBundle_it.properties | 2 + .../i18n/ResourceBundle_ja_JP.properties | 2 + .../player/i18n/ResourceBundle_ko.properties | 2 + .../player/i18n/ResourceBundle_mk.properties | 2 + .../player/i18n/ResourceBundle_nl.properties | 2 + .../player/i18n/ResourceBundle_nn.properties | 2 + .../player/i18n/ResourceBundle_no.properties | 2 + .../player/i18n/ResourceBundle_pl.properties | 2 + .../player/i18n/ResourceBundle_pt.properties | 2 + .../i18n/ResourceBundle_pt_BR.properties | 2 + .../i18n/ResourceBundle_pt_PT.properties | 2 + .../player/i18n/ResourceBundle_ru.properties | 2 + .../player/i18n/ResourceBundle_sl.properties | 2 + .../player/i18n/ResourceBundle_sv.properties | 2 + .../player/i18n/ResourceBundle_uk.properties | 2 + .../i18n/ResourceBundle_zh_CN.properties | 2 + .../i18n/ResourceBundle_zh_TW.properties | 2 + .../webapp/WEB-INF/jsp/personalSettings.jsp | 18 +- .../airsonic/player/dao/UserDaoTestCase.java | 8 +- .../service/MediaScannerServiceTestCase.java | 3 +- .../MEDIAS/Music3/TestAlbum/01 - Aria.flac | Bin 19328 -> 19328 bytes 50 files changed, 443 insertions(+), 12 deletions(-) create mode 100644 airsonic-main/src/main/java/org/airsonic/player/service/scrobbler/ListenBrainzScrobbler.java create mode 100644 airsonic-main/src/main/resources/liquibase/10.6/add-media-file-mb-recording-id.xml create mode 100644 airsonic-main/src/main/resources/liquibase/10.6/changelog.xml create mode 100644 airsonic-main/src/main/resources/liquibase/10.6/support-listenbrainz.xml diff --git a/airsonic-main/src/main/java/org/airsonic/player/command/PersonalSettingsCommand.java b/airsonic-main/src/main/java/org/airsonic/player/command/PersonalSettingsCommand.java index 57661132..9a2fe51f 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/command/PersonalSettingsCommand.java +++ b/airsonic-main/src/main/java/org/airsonic/player/command/PersonalSettingsCommand.java @@ -53,9 +53,11 @@ public class PersonalSettingsCommand { private boolean songNotificationEnabled; private boolean queueFollowingSongs; private boolean lastFmEnabled; + private boolean listenBrainzEnabled; private int paginationSize; private String lastFmUsername; private String lastFmPassword; + private String listenBrainzToken; public User getUser() { return user; @@ -233,6 +235,14 @@ public class PersonalSettingsCommand { this.lastFmEnabled = lastFmEnabled; } + public boolean isListenBrainzEnabled() { + return listenBrainzEnabled; + } + + public void setListenBrainzEnabled(boolean listenBrainzEnabled) { + this.listenBrainzEnabled = listenBrainzEnabled; + } + public String getLastFmUsername() { return lastFmUsername; } @@ -249,6 +259,14 @@ public class PersonalSettingsCommand { this.lastFmPassword = lastFmPassword; } + public String getListenBrainzToken() { + return listenBrainzToken; + } + + public void setListenBrainzToken(String listenBrainzToken) { + this.listenBrainzToken = listenBrainzToken; + } + public boolean isQueueFollowingSongs() { return queueFollowingSongs; } diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/PersonalSettingsController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/PersonalSettingsController.java index 02baa92f..382aa31f 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/PersonalSettingsController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/PersonalSettingsController.java @@ -82,6 +82,8 @@ public class PersonalSettingsController { command.setLastFmEnabled(userSettings.isLastFmEnabled()); command.setLastFmUsername(userSettings.getLastFmUsername()); command.setLastFmPassword(userSettings.getLastFmPassword()); + command.setListenBrainzEnabled(userSettings.isListenBrainzEnabled()); + command.setListenBrainzToken(userSettings.getListenBrainzToken()); command.setPaginationSize(userSettings.getPaginationSize()); Locale currentLocale = userSettings.getLocale(); @@ -148,6 +150,8 @@ public class PersonalSettingsController { settings.setKeyboardShortcutsEnabled(command.isKeyboardShortcutsEnabled()); settings.setLastFmEnabled(command.isLastFmEnabled()); settings.setLastFmUsername(command.getLastFmUsername()); + settings.setListenBrainzEnabled(command.isListenBrainzEnabled()); + settings.setListenBrainzToken(command.getListenBrainzToken()); settings.setSystemAvatarId(getSystemAvatarId(command)); settings.setAvatarScheme(getAvatarScheme(command)); settings.setPaginationSize(command.getPaginationSize()); diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/MediaFileDao.java b/airsonic-main/src/main/java/org/airsonic/player/dao/MediaFileDao.java index ef2a6c6a..4803a828 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/dao/MediaFileDao.java +++ b/airsonic-main/src/main/java/org/airsonic/player/dao/MediaFileDao.java @@ -46,7 +46,7 @@ public class MediaFileDao extends AbstractDao { private static final String INSERT_COLUMNS = "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, mb_release_id"; + "version, mb_release_id, mb_recording_id"; private static final String QUERY_COLUMNS = "id, " + INSERT_COLUMNS; private static final String GENRE_COLUMNS = "name, song_count, album_count"; @@ -162,7 +162,8 @@ public class MediaFileDao extends AbstractDao { "children_last_updated=?," + "present=?, " + "version=?, " + - "mb_release_id=? " + + "mb_release_id=?, " + + "mb_recording_id=? " + "where path=?"; LOG.trace("Updating media file {}", Util.debugObject(file)); @@ -173,7 +174,7 @@ public class MediaFileDao extends AbstractDao { 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.getMusicBrainzReleaseId(), file.getPath()); + file.getMusicBrainzReleaseId(), file.getMusicBrainzRecordingId(), file.getPath()); if (n == 0) { @@ -191,7 +192,7 @@ public class MediaFileDao extends AbstractDao { 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, file.getMusicBrainzReleaseId()); + file.getChildrenLastUpdated(), file.isPresent(), VERSION, file.getMusicBrainzReleaseId(), file.getMusicBrainzRecordingId()); } int id = queryForInt("select id from media_file where path=?", null, file.getPath()); @@ -721,7 +722,8 @@ public class MediaFileDao extends AbstractDao { rs.getTimestamp(28), rs.getBoolean(29), rs.getInt(30), - rs.getString(31)); + rs.getString(31), + rs.getString(32)); } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/UserDao.java b/airsonic-main/src/main/java/org/airsonic/player/dao/UserDao.java index eebafa4e..a7ca1b7b 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/dao/UserDao.java +++ b/airsonic-main/src/main/java/org/airsonic/player/dao/UserDao.java @@ -48,7 +48,8 @@ public class UserDao extends AbstractDao { "main_year, main_bit_rate, main_duration, main_format, main_file_size, " + "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, " + + "last_fm_enabled, last_fm_username, last_fm_password, listenbrainz_enabled, listenbrainz_token, " + + "transcode_scheme, show_now_playing, selected_music_folder_id, " + "party_mode_enabled, now_playing_allowed, avatar_scheme, system_avatar_id, changed, show_artist_info, auto_hide_play_queue, " + "view_as_list, default_album_list, queue_following_songs, show_side_bar, list_reload_delay, " + "keyboard_shortcuts_enabled, pagination_size"; @@ -219,6 +220,7 @@ public class UserDao extends AbstractDao { playlist.isGenreVisible(), playlist.isYearVisible(), playlist.isBitRateVisible(), playlist.isDurationVisible(), playlist.isFormatVisible(), playlist.isFileSizeVisible(), settings.isLastFmEnabled(), settings.getLastFmUsername(), encrypt(settings.getLastFmPassword()), + settings.isListenBrainzEnabled(), settings.getListenBrainzToken(), settings.getTranscodeScheme().name(), settings.isShowNowPlayingEnabled(), settings.getSelectedMusicFolderId(), settings.isPartyModeEnabled(), settings.isNowPlayingAllowed(), settings.getAvatarScheme().name(), settings.getSystemAvatarId(), settings.getChanged(), @@ -370,6 +372,9 @@ public class UserDao extends AbstractDao { settings.setLastFmUsername(rs.getString(col++)); settings.setLastFmPassword(decrypt(rs.getString(col++))); + settings.setListenBrainzEnabled(rs.getBoolean(col++)); + settings.setListenBrainzToken(rs.getString(col++)); + settings.setTranscodeScheme(TranscodeScheme.valueOf(rs.getString(col++))); settings.setShowNowPlayingEnabled(rs.getBoolean(col++)); settings.setSelectedMusicFolderId(rs.getInt(col++)); diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java b/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java index b8e29740..d54a0149 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java @@ -69,12 +69,13 @@ public class MediaFile { private boolean present; private int version; private String musicBrainzReleaseId; + private String musicBrainzRecordingId; 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, int version, String musicBrainzReleaseId) { + Date childrenLastUpdated, boolean present, int version, String musicBrainzReleaseId, String musicBrainzRecordingId) { this.id = id; this.path = path; this.folder = folder; @@ -106,6 +107,7 @@ public class MediaFile { this.present = present; this.version = version; this.musicBrainzReleaseId = musicBrainzReleaseId; + this.musicBrainzRecordingId = musicBrainzRecordingId; } public MediaFile() { @@ -393,6 +395,14 @@ public class MediaFile { this.musicBrainzReleaseId = musicBrainzReleaseId; } + public String getMusicBrainzRecordingId() { + return musicBrainzRecordingId; + } + + public void setMusicBrainzRecordingId(String musicBrainzRecordingId) { + this.musicBrainzRecordingId = musicBrainzRecordingId; + } + /** * Returns when the children was last updated in the database. */ diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/UserSettings.java b/airsonic-main/src/main/java/org/airsonic/player/domain/UserSettings.java index f85e2fb4..56048662 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/UserSettings.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/UserSettings.java @@ -46,8 +46,10 @@ public class UserSettings { private Visibility mainVisibility = new Visibility(); private Visibility playlistVisibility = new Visibility(); private boolean lastFmEnabled; + private boolean listenBrainzEnabled; private String lastFmUsername; private String lastFmPassword; + private String listenBrainzToken; private TranscodeScheme transcodeScheme = TranscodeScheme.OFF; private int selectedMusicFolderId = -1; private boolean partyModeEnabled; @@ -149,6 +151,14 @@ public class UserSettings { this.lastFmEnabled = lastFmEnabled; } + public boolean isListenBrainzEnabled() { + return listenBrainzEnabled; + } + + public void setListenBrainzEnabled(boolean listenBrainzEnabled) { + this.listenBrainzEnabled = listenBrainzEnabled; + } + public String getLastFmUsername() { return lastFmUsername; } @@ -165,6 +175,14 @@ public class UserSettings { this.lastFmPassword = lastFmPassword; } + public String getListenBrainzToken() { + return listenBrainzToken; + } + + public void setListenBrainzToken(String listenBrainzToken) { + this.listenBrainzToken = listenBrainzToken; + } + public TranscodeScheme getTranscodeScheme() { return transcodeScheme; } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/AudioScrobblerService.java b/airsonic-main/src/main/java/org/airsonic/player/service/AudioScrobblerService.java index d3311879..5103e5f7 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/AudioScrobblerService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/AudioScrobblerService.java @@ -21,6 +21,7 @@ package org.airsonic.player.service; import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.UserSettings; import org.airsonic.player.service.scrobbler.LastFMScrobbler; +import org.airsonic.player.service.scrobbler.ListenBrainzScrobbler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -34,6 +35,7 @@ import java.util.Date; public class AudioScrobblerService { private LastFMScrobbler lastFMScrobbler; + private ListenBrainzScrobbler listenBrainzScrobbler; @Autowired private SettingsService settingsService; @@ -58,6 +60,13 @@ public class AudioScrobblerService { } lastFMScrobbler.register(mediaFile, userSettings.getLastFmUsername(), userSettings.getLastFmPassword(), submission, time); } + + if (userSettings.isListenBrainzEnabled() && userSettings.getListenBrainzToken() != null) { + if (listenBrainzScrobbler == null) { + listenBrainzScrobbler = new ListenBrainzScrobbler(); + } + listenBrainzScrobbler.register(mediaFile, userSettings.getListenBrainzToken(), submission, time); + } } public void setSettingsService(SettingsService settingsService) { diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java b/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java index 8ba4fc78..d30c0312 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java @@ -508,6 +508,7 @@ public class MediaFileService { mediaFile.setHeight(metaData.getHeight()); mediaFile.setWidth(metaData.getWidth()); mediaFile.setMusicBrainzReleaseId(metaData.getMusicBrainzReleaseId()); + mediaFile.setMusicBrainzRecordingId(metaData.getMusicBrainzRecordingId()); } String format = StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(mediaFile.getPath()))); mediaFile.setFormat(format); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java index 02aa5f70..a8f000e9 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java @@ -1121,8 +1121,10 @@ public class SettingsService { settings.setQueueFollowingSongs(true); settings.setDefaultAlbumList(AlbumListType.RANDOM); settings.setLastFmEnabled(false); + settings.setListenBrainzEnabled(false); settings.setLastFmUsername(null); settings.setLastFmPassword(null); + settings.setListenBrainzToken(null); settings.setChanged(new Date()); settings.setPaginationSize(40); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/metadata/JaudiotaggerParser.java b/airsonic-main/src/main/java/org/airsonic/player/service/metadata/JaudiotaggerParser.java index f905d693..300bfb60 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/metadata/JaudiotaggerParser.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/metadata/JaudiotaggerParser.java @@ -97,6 +97,7 @@ public class JaudiotaggerParser extends MetaDataParser { metaData.setDiscNumber(parseInteger(getTagField(tag, FieldKey.DISC_NO))); metaData.setTrackNumber(parseTrackNumber(getTagField(tag, FieldKey.TRACK))); metaData.setMusicBrainzReleaseId(getTagField(tag, FieldKey.MUSICBRAINZ_RELEASEID)); + metaData.setMusicBrainzRecordingId(getTagField(tag, FieldKey.MUSICBRAINZ_TRACK_ID)); metaData.setArtist(getTagField(tag, FieldKey.ARTIST)); metaData.setAlbumArtist(getTagField(tag, FieldKey.ALBUM_ARTIST)); diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/metadata/MetaData.java b/airsonic-main/src/main/java/org/airsonic/player/service/metadata/MetaData.java index 1a2f615f..3de76e36 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/service/metadata/MetaData.java +++ b/airsonic-main/src/main/java/org/airsonic/player/service/metadata/MetaData.java @@ -39,6 +39,7 @@ public class MetaData { private Integer width; private Integer height; private String musicBrainzReleaseId; + private String musicBrainzRecordingId; public Integer getDiscNumber() { return discNumber; @@ -151,4 +152,12 @@ public class MetaData { public void setMusicBrainzReleaseId(String musicBrainzReleaseId) { this.musicBrainzReleaseId = musicBrainzReleaseId; } + + public String getMusicBrainzRecordingId() { + return musicBrainzRecordingId; + } + + public void setMusicBrainzRecordingId(String musicBrainzRecordingId) { + this.musicBrainzRecordingId = musicBrainzRecordingId; + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/scrobbler/ListenBrainzScrobbler.java b/airsonic-main/src/main/java/org/airsonic/player/service/scrobbler/ListenBrainzScrobbler.java new file mode 100644 index 00000000..ca96fd04 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/scrobbler/ListenBrainzScrobbler.java @@ -0,0 +1,230 @@ +/* + This file is part of Airsonic. + + Airsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Airsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Airsonic. If not, see . + + Copyright 2016 (C) Airsonic Authors + Based upon Subsonic, Copyright 2009 (C) Sindre Mehus + */ +package org.airsonic.player.service.scrobbler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.airsonic.player.domain.MediaFile; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Provides services for "audioscrobbling" at listenbrainz.org. + *
+ * See https://listenbrainz.readthedocs.io/ + */ +public class ListenBrainzScrobbler { + + private static final Logger LOG = LoggerFactory.getLogger(ListenBrainzScrobbler.class); + private static final int MAX_PENDING_REGISTRATION = 2000; + + private RegistrationThread thread; + private final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + private final RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(15000) + .setSocketTimeout(15000) + .build(); + + /** + * Registers the given media file at listenbrainz.org. This method returns immediately, the actual registration is done + * by a separate thread. + * + * @param mediaFile The media file to register. + * @param token The token to authentication user on ListenBrainz. + * @param submission Whether this is a submission or a now playing notification. + * @param time Event time, or {@code null} to use current time. + */ + public synchronized void register(MediaFile mediaFile, String token, boolean submission, Date time) { + if (thread == null) { + thread = new RegistrationThread(); + thread.start(); + } + + if (queue.size() >= MAX_PENDING_REGISTRATION) { + LOG.warn("ListenBrainz scrobbler queue is full. Ignoring '" + mediaFile.getTitle() + "'"); + return; + } + + RegistrationData registrationData = createRegistrationData(mediaFile, token, submission, time); + if (registrationData == null) { + return; + } + + try { + queue.put(registrationData); + } catch (InterruptedException x) { + LOG.warn("Interrupted while queuing ListenBrainz scrobble: " + x.toString()); + } + } + + private RegistrationData createRegistrationData(MediaFile mediaFile, String token, boolean submission, Date time) { + RegistrationData reg = new RegistrationData(); + reg.token = token; + reg.artist = mediaFile.getArtist(); + reg.album = mediaFile.getAlbumName(); + reg.title = mediaFile.getTitle(); + reg.musicBrainzReleaseId = mediaFile.getMusicBrainzReleaseId(); + reg.musicBrainzRecordingId = mediaFile.getMusicBrainzRecordingId(); + reg.trackNumber = mediaFile.getTrackNumber(); + reg.duration = mediaFile.getDurationSeconds() == null ? 0 : mediaFile.getDurationSeconds(); + reg.time = time == null ? new Date() : time; + reg.submission = submission; + + return reg; + } + + /** + * Scrobbles the given song data at listenbrainz.org, using the protocol defined at https://listenbrainz.readthedocs.io/en/latest/dev/api.html. + * + * @param registrationData Registration data for the song. + */ + private void scrobble(RegistrationData registrationData) throws ClientProtocolException, IOException { + if (registrationData == null || registrationData.token == null) { + return; + } + + if (!submit(registrationData)) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at ListenBrainz."); + } else { + LOG.info("Successfully registered " + + (registrationData.submission ? "submission" : "now playing") + + " for song '" + registrationData.title + "'" + + " at ListenBrainz: " + registrationData.time); + } + } + /** + * Returns if submission succeeds. + */ + private boolean submit(RegistrationData registrationData) throws ClientProtocolException, IOException { + Map additional_info = new HashMap(); + additional_info.computeIfAbsent("release_mbid", k -> registrationData.musicBrainzReleaseId); + additional_info.computeIfAbsent("recording_mbid", k -> registrationData.musicBrainzRecordingId); + additional_info.computeIfAbsent("tracknumber", k -> registrationData.trackNumber); + + Map track_metadata = new HashMap(); + if (additional_info.size() > 0) { + track_metadata.put("additional_info", additional_info); + } + track_metadata.computeIfAbsent("artist_name", k -> registrationData.artist); + track_metadata.computeIfAbsent("track_name", k -> registrationData.title); + track_metadata.computeIfAbsent("release_name", k -> registrationData.album); + + Map payload = new HashMap(); + if (track_metadata.size() > 0) { + payload.put("track_metadata", track_metadata); + } + + Map content = new HashMap(); + + if (registrationData.submission) { + payload.put("listened_at", Long.valueOf(registrationData.time.getTime() / 1000L)); + content.put("listen_type", "single"); + } else { + content.put("listen_type", "playing_now"); + } + + List> payloads = new ArrayList>(); + payloads.add(payload); + content.put("payload", payloads); + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(content); + + executeJsonPostRequest("https://api.listenbrainz.org/1/submit-listens", registrationData.token, json); + + return true; + } + + private boolean executeJsonPostRequest(String url, String token, String json) throws ClientProtocolException, IOException { + HttpPost request = new HttpPost(url); + request.setEntity(new StringEntity(json, "UTF-8")); + request.setHeader("Authorization", "token " + token); + request.setHeader("Content-type", "application/json; charset=utf-8"); + + executeRequest(request); + return true; + } + + private void executeRequest(HttpUriRequest request) throws ClientProtocolException, IOException { + CloseableHttpClient client = HttpClients.createDefault(); + client.execute(request); + } + + private class RegistrationThread extends Thread { + private RegistrationThread() { + super("ListenBrainzScrobbler Registration"); + } + + @Override + public void run() { + while (true) { + RegistrationData registrationData = null; + try { + registrationData = queue.take(); + scrobble(registrationData); + } catch (ClientProtocolException x) { + } catch (IOException x) { + handleNetworkError(registrationData, x.toString()); + } catch (Exception x) { + LOG.warn("Error in ListenBrainz registration: " + x.toString()); + } + } + } + + private void handleNetworkError(RegistrationData registrationData, String errorMessage) { + try { + queue.put(registrationData); + LOG.info("ListenBrainz registration for '" + registrationData.title + + "' encountered network error: " + errorMessage + ". Will try again later. In queue: " + queue.size()); + } catch (InterruptedException x) { + LOG.error("Failed to reschedule ListenBrainz registration for '" + registrationData.title + "': " + x.toString()); + } + try { + sleep(60L * 1000L); // Wait 60 seconds. + } catch (InterruptedException x) { + LOG.error("Failed to sleep after ListenBrainz registration failure for '" + registrationData.title + "': " + x.toString()); + } + } + } + + private static class RegistrationData { + private String token; + private String artist; + private String album; + private String title; + private String musicBrainzReleaseId; + private String musicBrainzRecordingId; + private Integer trackNumber; + private int duration; + private Date time; + public boolean submission; + } + +} diff --git a/airsonic-main/src/main/resources/liquibase/10.6/add-media-file-mb-recording-id.xml b/airsonic-main/src/main/resources/liquibase/10.6/add-media-file-mb-recording-id.xml new file mode 100644 index 00000000..08b9927d --- /dev/null +++ b/airsonic-main/src/main/resources/liquibase/10.6/add-media-file-mb-recording-id.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/airsonic-main/src/main/resources/liquibase/10.6/changelog.xml b/airsonic-main/src/main/resources/liquibase/10.6/changelog.xml new file mode 100644 index 00000000..042215bb --- /dev/null +++ b/airsonic-main/src/main/resources/liquibase/10.6/changelog.xml @@ -0,0 +1,7 @@ + + + + diff --git a/airsonic-main/src/main/resources/liquibase/10.6/support-listenbrainz.xml b/airsonic-main/src/main/resources/liquibase/10.6/support-listenbrainz.xml new file mode 100644 index 00000000..6d7fee9a --- /dev/null +++ b/airsonic-main/src/main/resources/liquibase/10.6/support-listenbrainz.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/airsonic-main/src/main/resources/liquibase/db-changelog.xml b/airsonic-main/src/main/resources/liquibase/db-changelog.xml index 247d9134..6004cb0f 100644 --- a/airsonic-main/src/main/resources/liquibase/db-changelog.xml +++ b/airsonic-main/src/main/resources/liquibase/db-changelog.xml @@ -12,4 +12,5 @@ + diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties index 9d2345dc..284e3c4e 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini 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 Last.fm 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.listenbrainzenabled=\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 ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token 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 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties index 7ad8f643..b4a8399b 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registrar el que estic reproduint a Last.fm personalsettings.lastfmusername=Nom d'usuari de Last.fm personalsettings.lastfmpassword=Contrasenya de Last.fm +personalsettings.listenbrainzenabled=Registrar el que estic reproduint a ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Imatge personal personalsettings.avatar.none=Sense imatge personalsettings.avatar.custom=Imatge personalitzada diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties index c3303f39..9f747aeb 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Informovat Last.fm o tom, co p\u0159ehr\u00E1v\u00E1m personalsettings.lastfmusername=U\u017Eivatel Last.fm personalsettings.lastfmpassword=Heslo Last.fm +personalsettings.listenbrainzenabled=Informovat ListenBrainz o tom, co p\u0159ehr\u00E1v\u00E1m +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Osobn\u00ED obr\u00E1zek personalsettings.avatar.none=Bez obr\u00E1zku personalsettings.avatar.custom=Vlastn\u00ED obr\u00E1zek diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties index b5a5c100..68823570 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registrer hvad jeg spiller p\u00E5 Last.fm personalsettings.lastfmusername=Last.fm brugernavn personalsettings.lastfmpassword=Last.fm adgangskode +personalsettings.listenbrainzenabled=Registrer hvad jeg spiller p\u00E5 ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personligt image personalsettings.avatar.none=Intet image personalsettings.avatar.custom=Tilpassede image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties index f700a3b7..bbfe67bf 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registriere was ich h\u00F6re bei Last.fm personalsettings.lastfmusername=Last.fm Benutzername personalsettings.lastfmpassword=Last.fm Passwort +personalsettings.listenbrainzenabled=Registriere was ich h\u00F6re bei ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Pers\u00F6nliches Bild personalsettings.avatar.none=Kein Bild personalsettings.avatar.custom=Eigenes Bild diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties index 7a226cf2..2c26c110 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=\u039A\u03B1\u03C4\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03C4\u03BF \u03C4\u03AF \u03B1\u03BA\u03BF\u03CD\u03C9 \u03C3\u03C4\u03BF Last.fm 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.listenbrainzenabled=\u039A\u03B1\u03C4\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03C4\u03BF \u03C4\u03AF \u03B1\u03BA\u03BF\u03CD\u03C9 \u03C3\u03C4\u03BF ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token 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 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties index 559cd129..3cf7c1d5 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties @@ -366,6 +366,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Register what I'm playing at Last.fm personalsettings.lastfmusername=Last.fm username personalsettings.lastfmpassword=Last.fm password +personalsettings.listenbrainzenabled=Register what I'm playing at ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personal image personalsettings.avatar.none=No image personalsettings.avatar.custom=Custom image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties index 0c98e831..f2829ac8 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Register what I'm playing at Last.fm personalsettings.lastfmusername=Last.fm username personalsettings.lastfmpassword=Last.fm password +personalsettings.listenbrainzenabled=Register what I'm playing at ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personal image personalsettings.avatar.none=No image personalsettings.avatar.custom=Custom image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties index 7eb0abc2..bd666e27 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=N\u00FAmero de \u00E1lbumes/carpetas relacionado personalsettings.lastfmenabled=Registrar lo que estoy reproduciendo en Last.fm personalsettings.lastfmusername=Nombre de usuario de Last.fm personalsettings.lastfmpassword=Contrase\u00F1a de Last.fm +personalsettings.listenbrainzenabled=Registrar lo que estoy reproduciendo en ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Imagen personal personalsettings.avatar.none=Sin imagen personalsettings.avatar.custom=Imagen personalizada diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties index 8f6712e0..0748b232 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registreeri minu kuulatavate lugude loend portaali Last.fm personalsettings.lastfmusername=Last.fm\u00b4i kasutajanimi personalsettings.lastfmpassword=Last.fm\u00b4i parool +personalsettings.listenbrainzenabled=Registreeri minu kuulatavate lugude loend portaali ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Isiklik pilt personalsettings.avatar.none=Pilt puudub personalsettings.avatar.custom=Kohandatud pilt diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties index 3a89f1d2..73abc469 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Rekister\u00F6i mit\u00E4 kuuntelen Last.fm-palveluun personalsettings.lastfmusername=Last.fm k\u00E4ytt\u00E4j\u00E4tunnus personalsettings.lastfmpassword=Last.fm salasana +personalsettings.listenbrainzenabled=Rekister\u00F6i mit\u00E4 kuuntelen ListenBrainz-palveluun +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Henkil\u00F6kohtainen kuva personalsettings.avatar.none=Ei kuvaa personalsettings.avatar.custom=Valinnainen kuva diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties index 4dc07fe3..2ec4dbb7 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties @@ -366,6 +366,8 @@ personalsettings.paginationsize=Nombre d'albums/r\u00e9pertoires associ\u00e9s \ personalsettings.lastfmenabled=Enregistrer ce que j'\u00e9coute sur Last.fm personalsettings.lastfmusername=Identifiant Last.fm personalsettings.lastfmpassword=Mot de passe Last.fm +personalsettings.listenbrainzenabled=Enregistrer ce que j'\u00e9coute sur ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Image personnelle personalsettings.avatar.none=Aucune image personalsettings.avatar.custom=Image personnalis\u00e9e diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties index a7d3d7e9..be236f7e 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Skr\u00E1 \u00C9g Er A\u00F0 Spila \u00C1 Last.fm personalsettings.lastfmusername=Last.fm Notandanafn personalsettings.lastfmpassword=Last.fm Lykilor\u00F0 +personalsettings.listenbrainzenabled=Skr\u00E1 \u00C9g Er A\u00F0 Spila \u00C1 ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Einka Mynd personalsettings.avatar.none=Engin Mynd personalsettings.avatar.custom=Valin Mynd diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties index ffaf2bfb..8b678cbb 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registra quello che sto ascoltando su Last.fm personalsettings.lastfmusername=Nome utente Last.fm personalsettings.lastfmpassword=Password Last.fm +personalsettings.listenbrainzenabled=Registra quello che sto ascoltando su ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Immagine personale personalsettings.avatar.none=Nessuna immagine personalsettings.avatar.custom=Immagine personalizzata diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties index d1179aa0..44c36110 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=\u518D\u751F\u4E2D\u306E\u66F2\u3092 Last.fm \u306B\u767B\u9332 personalsettings.lastfmusername=Last.fm \u30E6\u30FC\u30B6\u540D personalsettings.lastfmpassword=Last.fm \u30D1\u30B9\u30EF\u30FC\u30C9 +personalsettings.listenbrainzenabled=\u518D\u751F\u4E2D\u306E\u66F2\u3092 ListenBrainz \u306B\u767B\u9332 +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=\u30A2\u30A4\u30B3\u30F3 personalsettings.avatar.none=\u30A2\u30A4\u30B3\u30F3\u306A\u3057 personalsettings.avatar.custom=\u30AB\u30B9\u30BF\u30E0 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties index 66ca8f18..00797bd5 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=\uB0B4\uAC00 \uAC10\uC0C1\uD558\uB294 \uB178\uB798\uB97C Last.fm \uC5D0 \uB4F1\uB85D\uD558\uAE30 personalsettings.lastfmusername=Last.fm \uC0AC\uC6A9\uC790 \uC774\uB984 personalsettings.lastfmpassword=Last.fm \uC554\uD638 +personalsettings.listenbrainzenabled=\uB0B4\uAC00 \uAC10\uC0C1\uD558\uB294 \uB178\uB798\uB97C ListenBrainz \uC5D0 \uB4F1\uB85D\uD558\uAE30 +personalsettings.listenbrainztoken=ListenBrainz User Token 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 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties index 62ece33c..dfbd97c2 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Register what I'm playing at Last.fm personalsettings.lastfmusername=Last.fm username personalsettings.lastfmpassword=Last.fm password +personalsettings.listenbrainzenabled=Register what I'm playing at ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personal image personalsettings.avatar.none=No image personalsettings.avatar.custom=Custom image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties index 194f9d21..544b71ca 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Aantal te tonen gerelateerde albums/mappen (0=al personalsettings.lastfmenabled=Registreer wat ik afspeel op Last.fm personalsettings.lastfmusername=Last.fm-gebruikersnaam personalsettings.lastfmpassword=Last.fm-wachtwoord +personalsettings.listenbrainzenabled=Registreer wat ik afspeel op ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Persoonlijke afbeelding personalsettings.avatar.none=Geen afbeelding personalsettings.avatar.custom=Aangepaste afbeelding diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties index 4021fdfe..53d08880 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registrer kva eg spelar hos Last.fm personalsettings.lastfmusername=Brukarnamn for Last.fm personalsettings.lastfmpassword=Passord for Last.fm +personalsettings.listenbrainzenabled=Registrer kva eg spelar hos ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personleg bilete personalsettings.avatar.none=Ikkje bilete personalsettings.avatar.custom=Eigendefinert bilete diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties index e8e11d0e..4e6e60a5 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registrer hva jeg spiller hos Last.fm personalsettings.lastfmusername=Brukernavn for Last.fm personalsettings.lastfmpassword=Passord for Last.fm +personalsettings.listenbrainzenabled=Registrer hva jeg spiller hos ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personlig bilde personalsettings.avatar.none=Ingen bilde personalsettings.avatar.custom=Egendefinert bilde diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties index 084e69a1..d3daf14f 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Rejestuj co odtwarzam na Last.fm personalsettings.lastfmusername=U\u017Cytkownik Last.fm personalsettings.lastfmpassword=Has\u0142o Last.fm +personalsettings.listenbrainzenabled=Rejestuj co odtwarzam na ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=M\u00F3j obrazek personalsettings.avatar.none=Bez obrazka personalsettings.avatar.custom=W\u0142asny obrazek diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties index 5cad3b95..ed1b68a2 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registar o que estou a ouvir no Last.fm personalsettings.lastfmusername=Utilizador Last.fm personalsettings.lastfmpassword=Senha Last.fm +personalsettings.listenbrainzenabled=Registar o que estou a ouvir no ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Imagem pessoal personalsettings.avatar.none=Sem imagem personalsettings.avatar.custom=Imagem personalizada diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties index b98b047b..ef79467d 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Register what I'm playing at Last.fm personalsettings.lastfmusername=Last.fm username personalsettings.lastfmpassword=Last.fm password +personalsettings.listenbrainzenabled=Register what I'm playing at ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personal image personalsettings.avatar.none=No image personalsettings.avatar.custom=Custom image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties index b98b047b..ef79467d 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Register what I'm playing at Last.fm personalsettings.lastfmusername=Last.fm username personalsettings.lastfmpassword=Last.fm password +personalsettings.listenbrainzenabled=Register what I'm playing at ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personal image personalsettings.avatar.none=No image personalsettings.avatar.custom=Custom image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties index 06fa47ed..d566c326 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini 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 Last.fm personalsettings.lastfmusername=Last.fm \u043B\u043E\u0433\u0438\u043D personalsettings.lastfmpassword=Last.fm \u043F\u0430\u0440\u043E\u043B\u044C +personalsettings.listenbrainzenabled=\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 ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token 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 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties index 01d33c89..44ecfd52 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Zabele\u017Ei, kaj poslu\u0161am, na Last.fm personalsettings.lastfmusername=Last.fm uporabni\u0161ko ime personalsettings.lastfmpassword=Last.fm geslo +personalsettings.listenbrainzenabled=Zabele\u017Ei, kaj poslu\u0161am, na ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Osebna slika personalsettings.avatar.none=Brez slike personalsettings.avatar.custom=Slika po meri diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties index 6e80cd6f..13000434 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Registrera vad jag spelar hos Last.fm personalsettings.lastfmusername=Last.fm anv\u00E4ndare personalsettings.lastfmpassword=Last.fm l\u00F6senord +personalsettings.listenbrainzenabled=Registrera vad jag spelar hos ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personlig bild personalsettings.avatar.none=Ingen bild personalsettings.avatar.custom=Egen bild diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties index c20d421e..347c2f34 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=Register what I'm playing at Last.fm personalsettings.lastfmusername=Ім'я користувача Last.fm personalsettings.lastfmpassword=Last.fm пароль +personalsettings.listenbrainzenabled=Register what I'm playing at ListenBrainz +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=Personal image personalsettings.avatar.none=Немає зображення personalsettings.avatar.custom=Custom image diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties index 94dc1bfe..f782d6f5 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini personalsettings.lastfmenabled=\u767B\u5F55 Last.fm\u7684\u5E10\u53F7 personalsettings.lastfmusername=Last.fm \u5E10\u53F7 personalsettings.lastfmpassword=Last.fm \u5BC6\u7801 +personalsettings.listenbrainzenabled=\u767B\u5F55 ListenBrainz\u7684\u5E10\u53F7 +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=\u4E2A\u6027\u5316\u5934\u50CF personalsettings.avatar.none=\u65E0\u5934\u50CF personalsettings.avatar.custom=\u81EA\u5B9A\u4E49 diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties index 42b6f0a1..785e1a7f 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties @@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of initial related albums/directories to personalsettings.lastfmenabled=\u5728 Last.fm \u767B\u9304\u6211\u7684\u64AD\u653E\u52D5\u614B personalsettings.lastfmusername=Last.fm \u5E33\u865F personalsettings.lastfmpassword=Last.fm \u5BC6\u78BC +personalsettings.listenbrainzenabled=\u5728 ListenBrainz \u767B\u9304\u6211\u7684\u64AD\u653E\u52D5\u614B +personalsettings.listenbrainztoken=ListenBrainz User Token personalsettings.avatar.title=\u500B\u4EBA\u5716\u793A personalsettings.avatar.none=\u4E0D\u7528\u5716\u793A personalsettings.avatar.custom=\u81EA\u8A02\u5F71\u50CF diff --git a/airsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp b/airsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp index fc1b33a2..3a680041 100644 --- a/airsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp +++ b/airsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp @@ -7,13 +7,14 @@ - + @@ -172,9 +173,13 @@ - + + + + +
@@ -202,6 +207,13 @@
+ + + + + +
+

diff --git a/airsonic-main/src/test/java/org/airsonic/player/dao/UserDaoTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/dao/UserDaoTestCase.java index 775f2f3f..4c8d2a42 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/dao/UserDaoTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/dao/UserDaoTestCase.java @@ -187,6 +187,8 @@ public class UserDaoTestCase extends DaoTestCaseBean2 { assertFalse("Error in getUserSettings().", userSettings.isLastFmEnabled()); assertNull("Error in getUserSettings().", userSettings.getLastFmUsername()); assertNull("Error in getUserSettings().", userSettings.getLastFmPassword()); + assertFalse("Error in getUserSettings().", userSettings.isListenBrainzEnabled()); + assertNull("Error in getUserSettings().", userSettings.getListenBrainzToken()); assertSame("Error in getUserSettings().", TranscodeScheme.OFF, userSettings.getTranscodeScheme()); assertFalse("Error in getUserSettings().", userSettings.isShowNowPlayingEnabled()); assertEquals("Error in getUserSettings().", -1, userSettings.getSelectedMusicFolderId()); @@ -208,6 +210,8 @@ public class UserDaoTestCase extends DaoTestCaseBean2 { settings.setLastFmEnabled(true); settings.setLastFmUsername("last_user"); settings.setLastFmPassword("last_pass"); + settings.setListenBrainzEnabled(true); + settings.setListenBrainzToken("01234567-89ab-cdef-0123-456789abcdef"); settings.setTranscodeScheme(TranscodeScheme.MAX_192); settings.setShowNowPlayingEnabled(false); settings.setSelectedMusicFolderId(3); @@ -233,6 +237,8 @@ public class UserDaoTestCase extends DaoTestCaseBean2 { assertEquals("Error in getUserSettings().", true, userSettings.isLastFmEnabled()); assertEquals("Error in getUserSettings().", "last_user", userSettings.getLastFmUsername()); assertEquals("Error in getUserSettings().", "last_pass", userSettings.getLastFmPassword()); + assertEquals("Error in getUserSettings().", true, userSettings.isListenBrainzEnabled()); + assertEquals("Error in getUserSettings().", "01234567-89ab-cdef-0123-456789abcdef", userSettings.getListenBrainzToken()); assertSame("Error in getUserSettings().", TranscodeScheme.MAX_192, userSettings.getTranscodeScheme()); assertFalse("Error in getUserSettings().", userSettings.isShowNowPlayingEnabled()); assertEquals("Error in getUserSettings().", 3, userSettings.getSelectedMusicFolderId()); @@ -267,4 +273,4 @@ public class UserDaoTestCase extends DaoTestCaseBean2 { assertEquals("Wrong jukebox role.", expected.isJukeboxRole(), actual.isJukeboxRole()); assertEquals("Wrong settings role.", expected.isSettingsRole(), actual.isSettingsRole()); } -} \ No newline at end of file +} diff --git a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java index 5375780a..87415a2e 100644 --- a/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java +++ b/airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java @@ -218,7 +218,7 @@ public class MediaScannerServiceTestCase { Assert.assertEquals("0820752d-1043-4572-ab36-2df3b5cc15fa", album.getMusicBrainzReleaseId()); Assert.assertEquals(musicFolderFile.toPath().resolve("TestAlbum").toString(), album.getPath()); - // Test that the music file is correctly imported, along with its MusicBrainz release ID + // Test that the music file is correctly imported, along with its MusicBrainz release ID and recording ID List albumFiles = mediaFileDao.getChildrenOf(allAlbums.get(0).getPath()); Assert.assertEquals(1, albumFiles.size()); MediaFile file = albumFiles.get(0); @@ -232,5 +232,6 @@ public class MediaScannerServiceTestCase { Assert.assertEquals(album.getPath(), file.getParentPath()); Assert.assertEquals(new File(album.getPath()).toPath().resolve("01 - Aria.flac").toString(), file.getPath()); Assert.assertEquals("0820752d-1043-4572-ab36-2df3b5cc15fa", file.getMusicBrainzReleaseId()); + Assert.assertEquals("831586f4-56f9-4785-ac91-447ae20af633", file.getMusicBrainzRecordingId()); } } diff --git a/airsonic-main/src/test/resources/MEDIAS/Music3/TestAlbum/01 - Aria.flac b/airsonic-main/src/test/resources/MEDIAS/Music3/TestAlbum/01 - Aria.flac index 26d3346ba2d97db439e63fe8e53378efcfb0c349..4e5fd6ff8d0554ff73e3923c0cbaf3ba0bd8f39c 100644 GIT binary patch delta 94 zcmZpe&e$-Wae_1Bmx(SajO-ILV{I%L7#Ms*gFT&{f*d{lqT)k>9G$&AU2H9k4NWb~ y(oA$s&C)D&P0TG!brX{<4RuXS%o9_M3=-4KjEyHAvt%sY%*a@(uz926c})QP*Bd?n delta 38 ucmZpe&e$-Wae_1Bj)^WRjBFD#V<$5)S~70j?8sQ4FgbwHaq~vS^O^wlstmjU