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. 6
      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 songNotificationEnabled;
private boolean queueFollowingSongs; private boolean queueFollowingSongs;
private boolean lastFmEnabled; private boolean lastFmEnabled;
private boolean listenBrainzEnabled;
private int paginationSize; private int paginationSize;
private String lastFmUsername; private String lastFmUsername;
private String lastFmPassword; private String lastFmPassword;
private String listenBrainzToken;
public User getUser() { public User getUser() {
return user; return user;
@ -233,6 +235,14 @@ public class PersonalSettingsCommand {
this.lastFmEnabled = lastFmEnabled; this.lastFmEnabled = lastFmEnabled;
} }
public boolean isListenBrainzEnabled() {
return listenBrainzEnabled;
}
public void setListenBrainzEnabled(boolean listenBrainzEnabled) {
this.listenBrainzEnabled = listenBrainzEnabled;
}
public String getLastFmUsername() { public String getLastFmUsername() {
return lastFmUsername; return lastFmUsername;
} }
@ -249,6 +259,14 @@ public class PersonalSettingsCommand {
this.lastFmPassword = lastFmPassword; this.lastFmPassword = lastFmPassword;
} }
public String getListenBrainzToken() {
return listenBrainzToken;
}
public void setListenBrainzToken(String listenBrainzToken) {
this.listenBrainzToken = listenBrainzToken;
}
public boolean isQueueFollowingSongs() { public boolean isQueueFollowingSongs() {
return queueFollowingSongs; return queueFollowingSongs;
} }

@ -82,6 +82,8 @@ public class PersonalSettingsController {
command.setLastFmEnabled(userSettings.isLastFmEnabled()); command.setLastFmEnabled(userSettings.isLastFmEnabled());
command.setLastFmUsername(userSettings.getLastFmUsername()); command.setLastFmUsername(userSettings.getLastFmUsername());
command.setLastFmPassword(userSettings.getLastFmPassword()); command.setLastFmPassword(userSettings.getLastFmPassword());
command.setListenBrainzEnabled(userSettings.isListenBrainzEnabled());
command.setListenBrainzToken(userSettings.getListenBrainzToken());
command.setPaginationSize(userSettings.getPaginationSize()); command.setPaginationSize(userSettings.getPaginationSize());
Locale currentLocale = userSettings.getLocale(); Locale currentLocale = userSettings.getLocale();
@ -148,6 +150,8 @@ public class PersonalSettingsController {
settings.setKeyboardShortcutsEnabled(command.isKeyboardShortcutsEnabled()); settings.setKeyboardShortcutsEnabled(command.isKeyboardShortcutsEnabled());
settings.setLastFmEnabled(command.isLastFmEnabled()); settings.setLastFmEnabled(command.isLastFmEnabled());
settings.setLastFmUsername(command.getLastFmUsername()); settings.setLastFmUsername(command.getLastFmUsername());
settings.setListenBrainzEnabled(command.isListenBrainzEnabled());
settings.setListenBrainzToken(command.getListenBrainzToken());
settings.setSystemAvatarId(getSystemAvatarId(command)); settings.setSystemAvatarId(getSystemAvatarId(command));
settings.setAvatarScheme(getAvatarScheme(command)); settings.setAvatarScheme(getAvatarScheme(command));
settings.setPaginationSize(command.getPaginationSize()); 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, " + 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, " + "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, " + "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 QUERY_COLUMNS = "id, " + INSERT_COLUMNS;
private static final String GENRE_COLUMNS = "name, song_count, album_count"; private static final String GENRE_COLUMNS = "name, song_count, album_count";
@ -162,7 +162,8 @@ public class MediaFileDao extends AbstractDao {
"children_last_updated=?," + "children_last_updated=?," +
"present=?, " + "present=?, " +
"version=?, " + "version=?, " +
"mb_release_id=? " + "mb_release_id=?, " +
"mb_recording_id=? " +
"where path=?"; "where path=?";
LOG.trace("Updating media file {}", Util.debugObject(file)); 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.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(),
file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(), file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(),
file.getChanged(), file.getLastScanned(), file.getChildrenLastUpdated(), file.isPresent(), VERSION, file.getChanged(), file.getLastScanned(), file.getChildrenLastUpdated(), file.isPresent(), VERSION,
file.getMusicBrainzReleaseId(), file.getPath()); file.getMusicBrainzReleaseId(), file.getMusicBrainzRecordingId(), file.getPath());
if (n == 0) { if (n == 0) {
@ -191,7 +192,7 @@ public class MediaFileDao extends AbstractDao {
file.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(), file.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(),
file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(), file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(),
file.getCreated(), file.getChanged(), file.getLastScanned(), 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()); 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.getTimestamp(28),
rs.getBoolean(29), rs.getBoolean(29),
rs.getInt(30), 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, " + "main_year, main_bit_rate, main_duration, main_format, main_file_size, " +
"playlist_track_number, playlist_artist, playlist_album, playlist_genre, " + "playlist_track_number, playlist_artist, playlist_album, playlist_genre, " +
"playlist_year, playlist_bit_rate, playlist_duration, playlist_format, playlist_file_size, " + "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, " + "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, " + "view_as_list, default_album_list, queue_following_songs, show_side_bar, list_reload_delay, " +
"keyboard_shortcuts_enabled, pagination_size"; "keyboard_shortcuts_enabled, pagination_size";
@ -219,6 +220,7 @@ public class UserDao extends AbstractDao {
playlist.isGenreVisible(), playlist.isYearVisible(), playlist.isBitRateVisible(), playlist.isDurationVisible(), playlist.isGenreVisible(), playlist.isYearVisible(), playlist.isBitRateVisible(), playlist.isDurationVisible(),
playlist.isFormatVisible(), playlist.isFileSizeVisible(), playlist.isFormatVisible(), playlist.isFileSizeVisible(),
settings.isLastFmEnabled(), settings.getLastFmUsername(), encrypt(settings.getLastFmPassword()), settings.isLastFmEnabled(), settings.getLastFmUsername(), encrypt(settings.getLastFmPassword()),
settings.isListenBrainzEnabled(), settings.getListenBrainzToken(),
settings.getTranscodeScheme().name(), settings.isShowNowPlayingEnabled(), settings.getTranscodeScheme().name(), settings.isShowNowPlayingEnabled(),
settings.getSelectedMusicFolderId(), settings.isPartyModeEnabled(), settings.isNowPlayingAllowed(), settings.getSelectedMusicFolderId(), settings.isPartyModeEnabled(), settings.isNowPlayingAllowed(),
settings.getAvatarScheme().name(), settings.getSystemAvatarId(), settings.getChanged(), settings.getAvatarScheme().name(), settings.getSystemAvatarId(), settings.getChanged(),
@ -370,6 +372,9 @@ public class UserDao extends AbstractDao {
settings.setLastFmUsername(rs.getString(col++)); settings.setLastFmUsername(rs.getString(col++));
settings.setLastFmPassword(decrypt(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.setTranscodeScheme(TranscodeScheme.valueOf(rs.getString(col++)));
settings.setShowNowPlayingEnabled(rs.getBoolean(col++)); settings.setShowNowPlayingEnabled(rs.getBoolean(col++));
settings.setSelectedMusicFolderId(rs.getInt(col++)); settings.setSelectedMusicFolderId(rs.getInt(col++));

@ -69,12 +69,13 @@ public class MediaFile {
private boolean present; private boolean present;
private int version; private int version;
private String musicBrainzReleaseId; private String musicBrainzReleaseId;
private String musicBrainzRecordingId;
public MediaFile(int id, String path, String folder, MediaType mediaType, String format, String title, 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, 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, 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, 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.id = id;
this.path = path; this.path = path;
this.folder = folder; this.folder = folder;
@ -106,6 +107,7 @@ public class MediaFile {
this.present = present; this.present = present;
this.version = version; this.version = version;
this.musicBrainzReleaseId = musicBrainzReleaseId; this.musicBrainzReleaseId = musicBrainzReleaseId;
this.musicBrainzRecordingId = musicBrainzRecordingId;
} }
public MediaFile() { public MediaFile() {
@ -393,6 +395,14 @@ public class MediaFile {
this.musicBrainzReleaseId = musicBrainzReleaseId; 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. * Returns when the children was last updated in the database.
*/ */

@ -46,8 +46,10 @@ public class UserSettings {
private Visibility mainVisibility = new Visibility(); private Visibility mainVisibility = new Visibility();
private Visibility playlistVisibility = new Visibility(); private Visibility playlistVisibility = new Visibility();
private boolean lastFmEnabled; private boolean lastFmEnabled;
private boolean listenBrainzEnabled;
private String lastFmUsername; private String lastFmUsername;
private String lastFmPassword; private String lastFmPassword;
private String listenBrainzToken;
private TranscodeScheme transcodeScheme = TranscodeScheme.OFF; private TranscodeScheme transcodeScheme = TranscodeScheme.OFF;
private int selectedMusicFolderId = -1; private int selectedMusicFolderId = -1;
private boolean partyModeEnabled; private boolean partyModeEnabled;
@ -149,6 +151,14 @@ public class UserSettings {
this.lastFmEnabled = lastFmEnabled; this.lastFmEnabled = lastFmEnabled;
} }
public boolean isListenBrainzEnabled() {
return listenBrainzEnabled;
}
public void setListenBrainzEnabled(boolean listenBrainzEnabled) {
this.listenBrainzEnabled = listenBrainzEnabled;
}
public String getLastFmUsername() { public String getLastFmUsername() {
return lastFmUsername; return lastFmUsername;
} }
@ -165,6 +175,14 @@ public class UserSettings {
this.lastFmPassword = lastFmPassword; this.lastFmPassword = lastFmPassword;
} }
public String getListenBrainzToken() {
return listenBrainzToken;
}
public void setListenBrainzToken(String listenBrainzToken) {
this.listenBrainzToken = listenBrainzToken;
}
public TranscodeScheme getTranscodeScheme() { public TranscodeScheme getTranscodeScheme() {
return transcodeScheme; return transcodeScheme;
} }

@ -21,6 +21,7 @@ package org.airsonic.player.service;
import org.airsonic.player.domain.MediaFile; import org.airsonic.player.domain.MediaFile;
import org.airsonic.player.domain.UserSettings; import org.airsonic.player.domain.UserSettings;
import org.airsonic.player.service.scrobbler.LastFMScrobbler; import org.airsonic.player.service.scrobbler.LastFMScrobbler;
import org.airsonic.player.service.scrobbler.ListenBrainzScrobbler;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -34,6 +35,7 @@ import java.util.Date;
public class AudioScrobblerService { public class AudioScrobblerService {
private LastFMScrobbler lastFMScrobbler; private LastFMScrobbler lastFMScrobbler;
private ListenBrainzScrobbler listenBrainzScrobbler;
@Autowired @Autowired
private SettingsService settingsService; private SettingsService settingsService;
@ -58,6 +60,13 @@ public class AudioScrobblerService {
} }
lastFMScrobbler.register(mediaFile, userSettings.getLastFmUsername(), userSettings.getLastFmPassword(), submission, time); 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) { public void setSettingsService(SettingsService settingsService) {

@ -508,6 +508,7 @@ public class MediaFileService {
mediaFile.setHeight(metaData.getHeight()); mediaFile.setHeight(metaData.getHeight());
mediaFile.setWidth(metaData.getWidth()); mediaFile.setWidth(metaData.getWidth());
mediaFile.setMusicBrainzReleaseId(metaData.getMusicBrainzReleaseId()); mediaFile.setMusicBrainzReleaseId(metaData.getMusicBrainzReleaseId());
mediaFile.setMusicBrainzRecordingId(metaData.getMusicBrainzRecordingId());
} }
String format = StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(mediaFile.getPath()))); String format = StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(mediaFile.getPath())));
mediaFile.setFormat(format); mediaFile.setFormat(format);

@ -1121,8 +1121,10 @@ public class SettingsService {
settings.setQueueFollowingSongs(true); settings.setQueueFollowingSongs(true);
settings.setDefaultAlbumList(AlbumListType.RANDOM); settings.setDefaultAlbumList(AlbumListType.RANDOM);
settings.setLastFmEnabled(false); settings.setLastFmEnabled(false);
settings.setListenBrainzEnabled(false);
settings.setLastFmUsername(null); settings.setLastFmUsername(null);
settings.setLastFmPassword(null); settings.setLastFmPassword(null);
settings.setListenBrainzToken(null);
settings.setChanged(new Date()); settings.setChanged(new Date());
settings.setPaginationSize(40); settings.setPaginationSize(40);

@ -97,6 +97,7 @@ public class JaudiotaggerParser extends MetaDataParser {
metaData.setDiscNumber(parseInteger(getTagField(tag, FieldKey.DISC_NO))); metaData.setDiscNumber(parseInteger(getTagField(tag, FieldKey.DISC_NO)));
metaData.setTrackNumber(parseTrackNumber(getTagField(tag, FieldKey.TRACK))); metaData.setTrackNumber(parseTrackNumber(getTagField(tag, FieldKey.TRACK)));
metaData.setMusicBrainzReleaseId(getTagField(tag, FieldKey.MUSICBRAINZ_RELEASEID)); metaData.setMusicBrainzReleaseId(getTagField(tag, FieldKey.MUSICBRAINZ_RELEASEID));
metaData.setMusicBrainzRecordingId(getTagField(tag, FieldKey.MUSICBRAINZ_TRACK_ID));
metaData.setArtist(getTagField(tag, FieldKey.ARTIST)); metaData.setArtist(getTagField(tag, FieldKey.ARTIST));
metaData.setAlbumArtist(getTagField(tag, FieldKey.ALBUM_ARTIST)); metaData.setAlbumArtist(getTagField(tag, FieldKey.ALBUM_ARTIST));

@ -39,6 +39,7 @@ public class MetaData {
private Integer width; private Integer width;
private Integer height; private Integer height;
private String musicBrainzReleaseId; private String musicBrainzReleaseId;
private String musicBrainzRecordingId;
public Integer getDiscNumber() { public Integer getDiscNumber() {
return discNumber; return discNumber;
@ -151,4 +152,12 @@ public class MetaData {
public void setMusicBrainzReleaseId(String musicBrainzReleaseId) { public void setMusicBrainzReleaseId(String musicBrainzReleaseId) {
this.musicBrainzReleaseId = 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.3/changelog.xml" relativeToChangelogFile="true"/>
<include file="6.4/changelog.xml" relativeToChangelogFile="true"/> <include file="6.4/changelog.xml" relativeToChangelogFile="true"/>
<include file="10.2/changelog.xml" relativeToChangelogFile="true"/> <include file="10.2/changelog.xml" relativeToChangelogFile="true"/>
<include file="10.6/changelog.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </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.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.lastfmusername=Last.fm \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B
personalsettings.lastfmpassword=Last.fm \u043F\u0430\u0440\u043E\u043B\u0430 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.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.none=\u0411\u0435\u0437 \u0441\u043D\u0438\u043C\u043A\u0430
personalsettings.avatar.custom=\u041C\u043E\u044F \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.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.lastfmusername=Nom d'usuari de Last.fm
personalsettings.lastfmpassword=Contrasenya 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.title=Imatge personal
personalsettings.avatar.none=Sense imatge personalsettings.avatar.none=Sense imatge
personalsettings.avatar.custom=Imatge personalitzada 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.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.lastfmusername=U\u017Eivatel Last.fm
personalsettings.lastfmpassword=Heslo 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.title=Osobn\u00ED obr\u00E1zek
personalsettings.avatar.none=Bez obr\u00E1zku personalsettings.avatar.none=Bez obr\u00E1zku
personalsettings.avatar.custom=Vlastn\u00ED obr\u00E1zek 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.lastfmenabled=Registrer hvad jeg spiller p\u00E5 <a href="http://last.fm/" target="_blank"> Last.fm </a>
personalsettings.lastfmusername=Last.fm brugernavn personalsettings.lastfmusername=Last.fm brugernavn
personalsettings.lastfmpassword=Last.fm adgangskode 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.title=Personligt image
personalsettings.avatar.none=Intet image personalsettings.avatar.none=Intet image
personalsettings.avatar.custom=Tilpassede 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.lastfmenabled=Registriere was ich h\u00F6re bei <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm Benutzername personalsettings.lastfmusername=Last.fm Benutzername
personalsettings.lastfmpassword=Last.fm Passwort 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.title=Pers\u00F6nliches Bild
personalsettings.avatar.none=Kein Bild personalsettings.avatar.none=Kein Bild
personalsettings.avatar.custom=Eigenes 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.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.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.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.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.none=\u039A\u03B1\u03BD\u03AD\u03BD\u03B1 \u03B5\u03B9\u03BA\u03BF\u03BD\u03AF\u03B4\u03B9\u03BF
personalsettings.avatar.custom=\u03A0\u03C1\u03BF\u03C3\u03B1\u03C1\u03BC\u03BF\u03C3\u03BC\u03AD\u03BD\u03BF \u03B5\u03B9\u03BA\u03BF\u03BD\u03AF\u03B4\u03B9\u03BF personalsettings.avatar.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.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password 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.title=Personal image
personalsettings.avatar.none=No image personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom 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.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password 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.title=Personal image
personalsettings.avatar.none=No image personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom 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.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.lastfmusername=Nombre de usuario de Last.fm
personalsettings.lastfmpassword=Contrase\u00F1a 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.title=Imagen personal
personalsettings.avatar.none=Sin imagen personalsettings.avatar.none=Sin imagen
personalsettings.avatar.custom=Imagen personalizada 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.lastfmenabled=Registreeri minu kuulatavate lugude loend portaali <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm\u00b4i kasutajanimi personalsettings.lastfmusername=Last.fm\u00b4i kasutajanimi
personalsettings.lastfmpassword=Last.fm\u00b4i parool 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.title=Isiklik pilt
personalsettings.avatar.none=Pilt puudub personalsettings.avatar.none=Pilt puudub
personalsettings.avatar.custom=Kohandatud pilt 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.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.lastfmusername=Last.fm k\u00E4ytt\u00E4j\u00E4tunnus
personalsettings.lastfmpassword=Last.fm salasana 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.title=Henkil\u00F6kohtainen kuva
personalsettings.avatar.none=Ei kuvaa personalsettings.avatar.none=Ei kuvaa
personalsettings.avatar.custom=Valinnainen kuva 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.lastfmenabled=Enregistrer ce que j'\u00e9coute sur <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Identifiant Last.fm personalsettings.lastfmusername=Identifiant Last.fm
personalsettings.lastfmpassword=Mot de passe 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.title=Image personnelle
personalsettings.avatar.none=Aucune image personalsettings.avatar.none=Aucune image
personalsettings.avatar.custom=Image personnalis\u00e9e 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.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.lastfmusername=Last.fm Notandanafn
personalsettings.lastfmpassword=Last.fm Lykilor\u00F0 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.title=Einka Mynd
personalsettings.avatar.none=Engin Mynd personalsettings.avatar.none=Engin Mynd
personalsettings.avatar.custom=Valin 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.lastfmenabled=Registra quello che sto ascoltando su <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Nome utente Last.fm personalsettings.lastfmusername=Nome utente Last.fm
personalsettings.lastfmpassword=Password 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.title=Immagine personale
personalsettings.avatar.none=Nessuna immagine personalsettings.avatar.none=Nessuna immagine
personalsettings.avatar.custom=Immagine personalizzata 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.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.lastfmusername=Last.fm \u30E6\u30FC\u30B6\u540D
personalsettings.lastfmpassword=Last.fm \u30D1\u30B9\u30EF\u30FC\u30C9 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.title=\u30A2\u30A4\u30B3\u30F3
personalsettings.avatar.none=\u30A2\u30A4\u30B3\u30F3\u306A\u3057 personalsettings.avatar.none=\u30A2\u30A4\u30B3\u30F3\u306A\u3057
personalsettings.avatar.custom=\u30AB\u30B9\u30BF\u30E0 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.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.lastfmusername=Last.fm \uC0AC\uC6A9\uC790 \uC774\uB984
personalsettings.lastfmpassword=Last.fm \uC554\uD638 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.title=\uAC1C\uC778 \uC774\uBBF8\uC9C0
personalsettings.avatar.none=\uC774\uBBF8\uC9C0 \uC5C6\uC74C personalsettings.avatar.none=\uC774\uBBF8\uC9C0 \uC5C6\uC74C
personalsettings.avatar.custom=\uC0AC\uC6A9\uC790 \uC774\uBBF8\uC9C0 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.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password 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.title=Personal image
personalsettings.avatar.none=No image personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom 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.lastfmenabled=Registreer wat ik afspeel op <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm-gebruikersnaam personalsettings.lastfmusername=Last.fm-gebruikersnaam
personalsettings.lastfmpassword=Last.fm-wachtwoord 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.title=Persoonlijke afbeelding
personalsettings.avatar.none=Geen afbeelding personalsettings.avatar.none=Geen afbeelding
personalsettings.avatar.custom=Aangepaste 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.lastfmenabled=Registrer kva eg spelar hos <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Brukarnamn for Last.fm personalsettings.lastfmusername=Brukarnamn for Last.fm
personalsettings.lastfmpassword=Passord 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.title=Personleg bilete
personalsettings.avatar.none=Ikkje bilete personalsettings.avatar.none=Ikkje bilete
personalsettings.avatar.custom=Eigendefinert 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.lastfmenabled=Registrer hva jeg spiller hos <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Brukernavn for Last.fm personalsettings.lastfmusername=Brukernavn for Last.fm
personalsettings.lastfmpassword=Passord 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.title=Personlig bilde
personalsettings.avatar.none=Ingen bilde personalsettings.avatar.none=Ingen bilde
personalsettings.avatar.custom=Egendefinert 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.lastfmenabled=Rejestuj co odtwarzam na <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=U\u017Cytkownik Last.fm personalsettings.lastfmusername=U\u017Cytkownik Last.fm
personalsettings.lastfmpassword=Has\u0142o 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.title=M\u00F3j obrazek
personalsettings.avatar.none=Bez obrazka personalsettings.avatar.none=Bez obrazka
personalsettings.avatar.custom=W\u0142asny obrazek 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.lastfmenabled=Registar o que estou a ouvir no <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Utilizador Last.fm personalsettings.lastfmusername=Utilizador Last.fm
personalsettings.lastfmpassword=Senha 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.title=Imagem pessoal
personalsettings.avatar.none=Sem imagem personalsettings.avatar.none=Sem imagem
personalsettings.avatar.custom=Imagem personalizada 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.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password 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.title=Personal image
personalsettings.avatar.none=No image personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom 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.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm username personalsettings.lastfmusername=Last.fm username
personalsettings.lastfmpassword=Last.fm password 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.title=Personal image
personalsettings.avatar.none=No image personalsettings.avatar.none=No image
personalsettings.avatar.custom=Custom 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.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.lastfmusername=Last.fm \u043B\u043E\u0433\u0438\u043D
personalsettings.lastfmpassword=Last.fm \u043F\u0430\u0440\u043E\u043B\u044C 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.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.none=\u041D\u0435\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F
personalsettings.avatar.custom=\u0421\u0432\u043E\u0451 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 personalsettings.avatar.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.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.lastfmusername=Last.fm uporabni\u0161ko ime
personalsettings.lastfmpassword=Last.fm geslo 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.title=Osebna slika
personalsettings.avatar.none=Brez slike personalsettings.avatar.none=Brez slike
personalsettings.avatar.custom=Slika po meri 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.lastfmenabled=Registrera vad jag spelar hos <a href="http://last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Last.fm anv\u00E4ndare personalsettings.lastfmusername=Last.fm anv\u00E4ndare
personalsettings.lastfmpassword=Last.fm l\u00F6senord 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.title=Personlig bild
personalsettings.avatar.none=Ingen bild personalsettings.avatar.none=Ingen bild
personalsettings.avatar.custom=Egen 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.lastfmenabled=Register what I'm playing at <a href="https://www.last.fm/" target="_blank">Last.fm</a>
personalsettings.lastfmusername=Ім'я користувача Last.fm personalsettings.lastfmusername=Ім'я користувача Last.fm
personalsettings.lastfmpassword=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.title=Personal image
personalsettings.avatar.none=Немає зображення personalsettings.avatar.none=Немає зображення
personalsettings.avatar.custom=Custom image 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.lastfmenabled=\u767B\u5F55 <a href="http://last.fm/" target="_blank">Last.fm</a>\u7684\u5E10\u53F7
personalsettings.lastfmusername=Last.fm \u5E10\u53F7 personalsettings.lastfmusername=Last.fm \u5E10\u53F7
personalsettings.lastfmpassword=Last.fm \u5BC6\u7801 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.title=\u4E2A\u6027\u5316\u5934\u50CF
personalsettings.avatar.none=\u65E0\u5934\u50CF personalsettings.avatar.none=\u65E0\u5934\u50CF
personalsettings.avatar.custom=\u81EA\u5B9A\u4E49 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.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.lastfmusername=Last.fm \u5E33\u865F
personalsettings.lastfmpassword=Last.fm \u5BC6\u78BC 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.title=\u500B\u4EBA\u5716\u793A
personalsettings.avatar.none=\u4E0D\u7528\u5716\u793A personalsettings.avatar.none=\u4E0D\u7528\u5716\u793A
personalsettings.avatar.custom=\u81EA\u8A02\u5F71\u50CF 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" src="<c:url value='/script/utils.js'/>"></script>
<script type="text/javascript" language="javascript"> <script type="text/javascript" language="javascript">
function enableLastFmFields() { function enableFields() {
$("#lastFm").is(":checked") ? $("#lastFmTable").show() : $("#lastFmTable").hide(); $("#lastFm").is(":checked") ? $("#lastFmTable").show() : $("#lastFmTable").hide();
$("#listenBrainz").is(":checked") ? $("#listenBrainzTable").show() : $("#listenBrainzTable").hide();
} }
</script> </script>
</head> </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/wz_tooltip.js'/>"></script>
<script type="text/javascript" src="<c:url value='/script/tip_balloon.js'/>"></script> <script type="text/javascript" src="<c:url value='/script/tip_balloon.js'/>"></script>
@ -172,9 +173,13 @@
<table class="indent"> <table class="indent">
<tr> <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> <td><label for="lastFm"><fmt:message key="personalsettings.lastfmenabled"/></label></td>
</tr> </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>
<table class="indent"> <table class="indent">
@ -202,6 +207,13 @@
</tr> </tr>
</table> </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"> <p style="padding-top:1em;padding-bottom:1em">
<input type="submit" value="<fmt:message key='common.save'/>" style="margin-right:0.3em"/> <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> <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()); assertFalse("Error in getUserSettings().", userSettings.isLastFmEnabled());
assertNull("Error in getUserSettings().", userSettings.getLastFmUsername()); assertNull("Error in getUserSettings().", userSettings.getLastFmUsername());
assertNull("Error in getUserSettings().", userSettings.getLastFmPassword()); 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()); assertSame("Error in getUserSettings().", TranscodeScheme.OFF, userSettings.getTranscodeScheme());
assertFalse("Error in getUserSettings().", userSettings.isShowNowPlayingEnabled()); assertFalse("Error in getUserSettings().", userSettings.isShowNowPlayingEnabled());
assertEquals("Error in getUserSettings().", -1, userSettings.getSelectedMusicFolderId()); assertEquals("Error in getUserSettings().", -1, userSettings.getSelectedMusicFolderId());
@ -208,6 +210,8 @@ public class UserDaoTestCase extends DaoTestCaseBean2 {
settings.setLastFmEnabled(true); settings.setLastFmEnabled(true);
settings.setLastFmUsername("last_user"); settings.setLastFmUsername("last_user");
settings.setLastFmPassword("last_pass"); settings.setLastFmPassword("last_pass");
settings.setListenBrainzEnabled(true);
settings.setListenBrainzToken("01234567-89ab-cdef-0123-456789abcdef");
settings.setTranscodeScheme(TranscodeScheme.MAX_192); settings.setTranscodeScheme(TranscodeScheme.MAX_192);
settings.setShowNowPlayingEnabled(false); settings.setShowNowPlayingEnabled(false);
settings.setSelectedMusicFolderId(3); settings.setSelectedMusicFolderId(3);
@ -233,6 +237,8 @@ public class UserDaoTestCase extends DaoTestCaseBean2 {
assertEquals("Error in getUserSettings().", true, userSettings.isLastFmEnabled()); assertEquals("Error in getUserSettings().", true, userSettings.isLastFmEnabled());
assertEquals("Error in getUserSettings().", "last_user", userSettings.getLastFmUsername()); assertEquals("Error in getUserSettings().", "last_user", userSettings.getLastFmUsername());
assertEquals("Error in getUserSettings().", "last_pass", userSettings.getLastFmPassword()); 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()); assertSame("Error in getUserSettings().", TranscodeScheme.MAX_192, userSettings.getTranscodeScheme());
assertFalse("Error in getUserSettings().", userSettings.isShowNowPlayingEnabled()); assertFalse("Error in getUserSettings().", userSettings.isShowNowPlayingEnabled());
assertEquals("Error in getUserSettings().", 3, userSettings.getSelectedMusicFolderId()); assertEquals("Error in getUserSettings().", 3, userSettings.getSelectedMusicFolderId());

@ -218,7 +218,7 @@ public class MediaScannerServiceTestCase {
Assert.assertEquals("0820752d-1043-4572-ab36-2df3b5cc15fa", album.getMusicBrainzReleaseId()); Assert.assertEquals("0820752d-1043-4572-ab36-2df3b5cc15fa", album.getMusicBrainzReleaseId());
Assert.assertEquals(musicFolderFile.toPath().resolve("TestAlbum").toString(), album.getPath()); 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()); List<MediaFile> albumFiles = mediaFileDao.getChildrenOf(allAlbums.get(0).getPath());
Assert.assertEquals(1, albumFiles.size()); Assert.assertEquals(1, albumFiles.size());
MediaFile file = albumFiles.get(0); MediaFile file = albumFiles.get(0);
@ -232,5 +232,6 @@ public class MediaScannerServiceTestCase {
Assert.assertEquals(album.getPath(), file.getParentPath()); Assert.assertEquals(album.getPath(), file.getParentPath());
Assert.assertEquals(new File(album.getPath()).toPath().resolve("01 - Aria.flac").toString(), file.getPath()); 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("0820752d-1043-4572-ab36-2df3b5cc15fa", file.getMusicBrainzReleaseId());
Assert.assertEquals("831586f4-56f9-4785-ac91-447ae20af633", file.getMusicBrainzRecordingId());
} }
} }

Loading…
Cancel
Save