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 <ibmibmibm.tw@gmail.com>
Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
master
Shen-Ta Hsieh 5 years ago committed by Andrew DeMaria
parent e3b3bc9d2b
commit 84df4e3b94
No known key found for this signature in database
GPG Key ID: 0A3F5E91F8364EDF
  1. 18
      airsonic-main/src/main/java/org/airsonic/player/command/PersonalSettingsCommand.java
  2. 4
      airsonic-main/src/main/java/org/airsonic/player/controller/PersonalSettingsController.java
  3. 12
      airsonic-main/src/main/java/org/airsonic/player/dao/MediaFileDao.java
  4. 7
      airsonic-main/src/main/java/org/airsonic/player/dao/UserDao.java
  5. 12
      airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java
  6. 18
      airsonic-main/src/main/java/org/airsonic/player/domain/UserSettings.java
  7. 9
      airsonic-main/src/main/java/org/airsonic/player/service/AudioScrobblerService.java
  8. 1
      airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java
  9. 2
      airsonic-main/src/main/java/org/airsonic/player/service/SettingsService.java
  10. 1
      airsonic-main/src/main/java/org/airsonic/player/service/metadata/JaudiotaggerParser.java
  11. 9
      airsonic-main/src/main/java/org/airsonic/player/service/metadata/MetaData.java
  12. 230
      airsonic-main/src/main/java/org/airsonic/player/service/scrobbler/ListenBrainzScrobbler.java
  13. 17
      airsonic-main/src/main/resources/liquibase/10.6/add-media-file-mb-recording-id.xml
  14. 7
      airsonic-main/src/main/resources/liquibase/10.6/changelog.xml
  15. 18
      airsonic-main/src/main/resources/liquibase/10.6/support-listenbrainz.xml
  16. 1
      airsonic-main/src/main/resources/liquibase/db-changelog.xml
  17. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_bg.properties
  18. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ca.properties
  19. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_cs.properties
  20. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_da.properties
  21. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_de.properties
  22. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_el.properties
  23. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties
  24. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en_GB.properties
  25. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_es.properties
  26. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_et.properties
  27. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fi.properties
  28. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties
  29. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_is.properties
  30. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_it.properties
  31. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ja_JP.properties
  32. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ko.properties
  33. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_mk.properties
  34. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nl.properties
  35. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_nn.properties
  36. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_no.properties
  37. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pl.properties
  38. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt.properties
  39. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_BR.properties
  40. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_pt_PT.properties
  41. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_ru.properties
  42. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sl.properties
  43. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_sv.properties
  44. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_uk.properties
  45. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_CN.properties
  46. 2
      airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_zh_TW.properties
  47. 18
      airsonic-main/src/main/webapp/WEB-INF/jsp/personalSettings.jsp
  48. 8
      airsonic-main/src/test/java/org/airsonic/player/dao/UserDaoTestCase.java
  49. 3
      airsonic-main/src/test/java/org/airsonic/player/service/MediaScannerServiceTestCase.java
  50. BIN
      airsonic-main/src/test/resources/MEDIAS/Music3/TestAlbum/01 - Aria.flac

@ -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;
}

@ -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());

@ -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));
}
}

@ -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++));

@ -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.
*/

@ -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;
}

@ -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) {

@ -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);

@ -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);

@ -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));

@ -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;
}
}

@ -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 <http://www.gnu.org/licenses/>.
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.
* <br/>
* 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<RegistrationData> queue = new LinkedBlockingQueue<RegistrationData>();
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<String, Object> additional_info = new HashMap<String, Object>();
additional_info.computeIfAbsent("release_mbid", k -> registrationData.musicBrainzReleaseId);
additional_info.computeIfAbsent("recording_mbid", k -> registrationData.musicBrainzRecordingId);
additional_info.computeIfAbsent("tracknumber", k -> registrationData.trackNumber);
Map<String, Object> track_metadata = new HashMap<String, Object>();
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<String, Object> payload = new HashMap<String, Object>();
if (track_metadata.size() > 0) {
payload.put("track_metadata", track_metadata);
}
Map<String, Object> content = new HashMap<String, Object>();
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<Map<String, Object>> payloads = new ArrayList<Map<String, Object>>();
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;
}
}

@ -0,0 +1,17 @@
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="add-media-file-mb-recording-id" author="BestSteve">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="media_file" columnName="mb_recording_id" />
</not>
</preConditions>
<addColumn tableName="media_file">
<column name="mb_recording_id" type="${varchar_type}">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

@ -0,0 +1,7 @@
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="add-media-file-mb-recording-id.xml" relativeToChangelogFile="true"/>
<include file="support-listenbrainz.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

@ -0,0 +1,18 @@
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="support-listenbrainz" author="BestSteve">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="user_settings" columnName="listenbrainz_enabled" />
</not>
</preConditions>
<addColumn tableName="user_settings">
<column name="listenbrainz_enabled" type="boolean" defaultValueBoolean="false">
<constraints nullable="false" />
</column>
<column name="listenbrainz_token" type="${varchar_type}" />
</addColumn>
</changeSet>
</databaseChangeLog>

@ -12,4 +12,5 @@
<include file="6.3/changelog.xml" relativeToChangelogFile="true"/>
<include file="6.4/changelog.xml" relativeToChangelogFile="true"/>
<include file="10.2/changelog.xml" relativeToChangelogFile="true"/>
<include file="10.6/changelog.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

@ -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 <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.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 <a href="http://listenbrainz.org/" target="_blank">ListenBrainz</a>
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

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registrar el que estic reproduint a <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Imatge personal
personalsettings.avatar.none=Sense imatge
personalsettings.avatar.custom=Imatge personalitzada

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Informovat <a href="http://last.fm/" target="_blank">Last.fm</a> o tom, co p\u0159ehr\u00E1v\u00E1m
personalsettings.lastfmusername=U\u017Eivatel Last.fm
personalsettings.lastfmpassword=Heslo Last.fm
personalsettings.listenbrainzenabled=Informovat <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a> 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

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registrer hvad jeg spiller p\u00E5 <a href="https://listenbrainz.org/" target="_blank"> ListenBrainz </a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personligt image
personalsettings.avatar.none=Intet image
personalsettings.avatar.custom=Tilpassede image

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registriere was ich h\u00F6re bei <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Pers\u00F6nliches Bild
personalsettings.avatar.none=Kein Bild
personalsettings.avatar.custom=Eigenes Bild

@ -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 <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.listenbrainzenabled=\u039A\u03B1\u03C4\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE \u03C4\u03BF \u03C4\u03AF \u03B1\u03BA\u03BF\u03CD\u03C9 \u03C3\u03C4\u03BF <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
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

@ -366,6 +366,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password
personalsettings.listenbrainzenabled=Register what I'm playing at <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personal image
personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom image

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password
personalsettings.listenbrainzenabled=Register what I'm playing at <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personal image
personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom image

@ -365,6 +365,8 @@ personalsettings.paginationsize=N\u00FAmero de \u00E1lbumes/carpetas relacionado
personalsettings.lastfmenabled=Registrar lo que estoy reproduciendo en <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Nombre de usuario de Last.fm
personalsettings.lastfmpassword=Contrase\u00F1a de Last.fm
personalsettings.listenbrainzenabled=Registrar lo que estoy reproduciendo en <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Imagen personal
personalsettings.avatar.none=Sin imagen
personalsettings.avatar.custom=Imagen personalizada

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registreeri minu kuulatavate lugude loend portaali <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Isiklik pilt
personalsettings.avatar.none=Pilt puudub
personalsettings.avatar.custom=Kohandatud pilt

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Rekister\u00F6i mit\u00E4 kuuntelen <a href="http://last.fm/" target="_blank">Last.fm-palveluun</a>
personalsettings.lastfmusername=Last.fm k\u00E4ytt\u00E4j\u00E4tunnus
personalsettings.lastfmpassword=Last.fm salasana
personalsettings.listenbrainzenabled=Rekister\u00F6i mit\u00E4 kuuntelen <a href="https://listenbrainz.org/" target="_blank">ListenBrainz-palveluun</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Henkil\u00F6kohtainen kuva
personalsettings.avatar.none=Ei kuvaa
personalsettings.avatar.custom=Valinnainen kuva

@ -366,6 +366,8 @@ personalsettings.paginationsize=Nombre d'albums/r\u00e9pertoires associ\u00e9s \
personalsettings.lastfmenabled=Enregistrer ce que j'\u00e9coute sur <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Identifiant Last.fm
personalsettings.lastfmpassword=Mot de passe Last.fm
personalsettings.listenbrainzenabled=Enregistrer ce que j'\u00e9coute sur <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Image personnelle
personalsettings.avatar.none=Aucune image
personalsettings.avatar.custom=Image personnalis\u00e9e

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Skr\u00E1 \u00C9g Er A\u00F0 Spila \u00C1 <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm Notandanafn
personalsettings.lastfmpassword=Last.fm Lykilor\u00F0
personalsettings.listenbrainzenabled=Skr\u00E1 \u00C9g Er A\u00F0 Spila \u00C1 <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Einka Mynd
personalsettings.avatar.none=Engin Mynd
personalsettings.avatar.custom=Valin Mynd

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registra quello che sto ascoltando su <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Immagine personale
personalsettings.avatar.none=Nessuna immagine
personalsettings.avatar.custom=Immagine personalizzata

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=\u518D\u751F\u4E2D\u306E\u66F2\u3092 <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a> \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

@ -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 <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.listenbrainzenabled=\uB0B4\uAC00 \uAC10\uC0C1\uD558\uB294 \uB178\uB798\uB97C <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a> \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

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password
personalsettings.listenbrainzenabled=Register what I'm playing at <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personal image
personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom image

@ -365,6 +365,8 @@ personalsettings.paginationsize=Aantal te tonen gerelateerde albums/mappen (0=al
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.listenbrainzenabled=Registreer wat ik afspeel op <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Persoonlijke afbeelding
personalsettings.avatar.none=Geen afbeelding
personalsettings.avatar.custom=Aangepaste afbeelding

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Registrer kva eg spelar hos <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Brukarnamn for Last.fm
personalsettings.lastfmpassword=Passord for Last.fm
personalsettings.listenbrainzenabled=Registrer kva eg spelar hos <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personleg bilete
personalsettings.avatar.none=Ikkje bilete
personalsettings.avatar.custom=Eigendefinert bilete

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Registrer hva jeg spiller hos <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Brukernavn for Last.fm
personalsettings.lastfmpassword=Passord for Last.fm
personalsettings.listenbrainzenabled=Registrer hva jeg spiller hos <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personlig bilde
personalsettings.avatar.none=Ingen bilde
personalsettings.avatar.custom=Egendefinert bilde

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Rejestuj co odtwarzam na <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=M\u00F3j obrazek
personalsettings.avatar.none=Bez obrazka
personalsettings.avatar.custom=W\u0142asny obrazek

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registar o que estou a ouvir no <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Imagem pessoal
personalsettings.avatar.none=Sem imagem
personalsettings.avatar.custom=Imagem personalizada

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password
personalsettings.listenbrainzenabled=Register what I'm playing at <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personal image
personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom image

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password
personalsettings.listenbrainzenabled=Register what I'm playing at <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personal image
personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom image

@ -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 <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.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 <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
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

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Zabele\u017Ei, kaj poslu\u0161am, na <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Osebna slika
personalsettings.avatar.none=Brez slike
personalsettings.avatar.custom=Slika po meri

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
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.listenbrainzenabled=Registrera vad jag spelar hos <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personlig bild
personalsettings.avatar.none=Ingen bild
personalsettings.avatar.custom=Egen bild

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Ім'я користувача Last.fm
personalsettings.lastfmpassword=Last.fm пароль
personalsettings.listenbrainzenabled=Register what I'm playing at <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>
personalsettings.listenbrainztoken=ListenBrainz User Token
personalsettings.avatar.title=Personal image
personalsettings.avatar.none=Немає зображення
personalsettings.avatar.custom=Custom image

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of related albums/directories to show ini
personalsettings.lastfmenabled=\u767B\u5F55 <a href="http://last.fm/" target="_blank">Last.fm</a>\u7684\u5E10\u53F7
personalsettings.lastfmusername=Last.fm \u5E10\u53F7
personalsettings.lastfmpassword=Last.fm \u5BC6\u7801
personalsettings.listenbrainzenabled=\u767B\u5F55 <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a>\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

@ -365,6 +365,8 @@ personalsettings.paginationsize=Number of initial related albums/directories to
personalsettings.lastfmenabled=\u5728 <a href="http://last.fm/" target="_blank">Last.fm</a> \u767B\u9304\u6211\u7684\u64AD\u653E\u52D5\u614B
personalsettings.lastfmusername=Last.fm \u5E33\u865F
personalsettings.lastfmpassword=Last.fm \u5BC6\u78BC
personalsettings.listenbrainzenabled=\u5728 <a href="https://listenbrainz.org/" target="_blank">ListenBrainz</a> \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

@ -7,13 +7,14 @@
<script type="text/javascript" src="<c:url value='/script/utils.js'/>"></script>
<script type="text/javascript" language="javascript">
function enableLastFmFields() {
function enableFields() {
$("#lastFm").is(":checked") ? $("#lastFmTable").show() : $("#lastFmTable").hide();
$("#listenBrainz").is(":checked") ? $("#listenBrainzTable").show() : $("#listenBrainzTable").hide();
}
</script>
</head>
<body class="mainframe bgcolor1" onload="enableLastFmFields()">
<body class="mainframe bgcolor1" onload="enableFields()">
<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>
@ -172,9 +173,13 @@
<table class="indent">
<tr>
<td><form:checkbox path="lastFmEnabled" id="lastFm" cssClass="checkbox" onclick="enableLastFmFields()"/></td>
<td><form:checkbox path="lastFmEnabled" id="lastFm" cssClass="checkbox" onclick="enableFields()"/></td>
<td><label for="lastFm"><fmt:message key="personalsettings.lastfmenabled"/></label></td>
</tr>
<tr>
<td><form:checkbox path="listenBrainzEnabled" id="listenBrainz" cssClass="checkbox" onclick="enableFields()"/></td>
<td><label for="listenBrainz"><fmt:message key="personalsettings.listenbrainzenabled"/></label></td>
</tr>
</table>
<table class="indent">
@ -202,6 +207,13 @@
</tr>
</table>
<table id="listenBrainzTable" style="padding-left:2em">
<tr>
<td><fmt:message key="personalsettings.listenbrainztoken"/></td>
<td><form:input path="listenBrainzToken" size="36"/></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"/>
<a href='nowPlaying.view'><input type="button" value="<fmt:message key='common.cancel'/>"></a>

@ -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());
}
}
}

@ -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<MediaFile> 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());
}
}

Loading…
Cancel
Save