M app/controllers/listenbrainz_listen_controller.rb => app/controllers/listenbrainz_listen_controller.rb +15 -2
@@ 11,8 11,17 @@ class ListenbrainzListenController < ApplicationController
return
end
- result = lb_api.get("/user/#{listenbrainz_username}/playing-now")
- listen = first_listen result
+ listen = nil
+ if show_pinned_recording?
+ result = lb_api.get("/#{listenbrainz_username}/pins/current")
+ listen = result&.dig('pinned_recording')
+ end
+
+ if !listen
+ result = lb_api.get("/user/#{listenbrainz_username}/playing-now")
+ listen = first_listen result
+ end
+
if !listen
result = lb_api.get("/user/#{listenbrainz_username}/listens", {
:count => 1,
@@ 30,6 39,10 @@ class ListenbrainzListenController < ApplicationController
private
+ def show_pinned_recording?
+ user&.custom_fields&.[]('listenbrainz_show_pinned_recording') || false
+ end
+
def first_listen(result)
result.dig('payload', 'listens')&.first
end
D app/controllers/listenbrainz_pinned_recording_controller.rb => app/controllers/listenbrainz_pinned_recording_controller.rb +0 -19
@@ 1,19 0,0 @@
-require 'cgi'
-require_relative './listenbrainz_mixin'
-require_relative '../../lib/helpers/listen_helper'
-
-class ListenbrainzPinnedRecordingController < ApplicationController
- include ListenBrainzMixin
-
- def current_pin
- if !listenbrainz_enabled
- head :forbidden
- return
- end
-
- result = lb_api.get("/#{listenbrainz_username}/pins/current")
- listen = result&.dig('pinned_recording')
- ListenBrainz::ListenHelper::enhance_metadata(listen) if listen
- render :json => listen
- end
-end
M assets/javascripts/discourse/components/listenbrainz-listening.js => assets/javascripts/discourse/components/listenbrainz-listening.js +37 -1
@@ 15,6 15,14 @@ export default class ListeningComponent extends Component {
@tracked loading = true
@tracked listen = null
+ get isPlayingNow() {
+ return !!this.listen?.playing_now
+ }
+
+ get isPin() {
+ return !!this.listen?.pinned_until
+ }
+
get artist() {
return this.artistCredits.reduce((result, c) => {
result.push(c.artist_credit_name)
@@ 67,7 75,7 @@ export default class ListeningComponent extends Component {
}
get listenDateTime() {
- const timestamp = this.listen?.listened_at
+ const timestamp = this.listen?.listened_at || this.listen?.created
return timestamp ? new Date(timestamp * 1000) : null
}
@@ 106,6 114,34 @@ export default class ListeningComponent extends Component {
return !this.show && this.user.custom_fields.listenbrainz_display === 'listen_profile_fallback'
}
+ get hasIcon() {
+ return this.isPlayingNow || this.isPin
+ }
+
+ get icon() {
+ if (this.isPlayingNow) {
+ return 'music'
+ } else if (this.isPin) {
+ return 'thumbtack'
+ } else {
+ return null
+ }
+ }
+
+ get iconTooltip() {
+ if (this.isPlayingNow) {
+ return I18n.t('listenbrainz.listening.playing_now_tooltip')
+ } else if (this.isPin) {
+ return I18n.t('listenbrainz.listening.pin_tooltip')
+ } else {
+ return null
+ }
+ }
+
+ get blurb() {
+ return this.listen?.blurb_content
+ }
+
async loadNowPlaying() {
this.loading = true
try {
D assets/javascripts/discourse/components/listenbrainz-pinned-recording.js => assets/javascripts/discourse/components/listenbrainz-pinned-recording.js +0 -126
@@ 1,126 0,0 @@
-import Component from '@glimmer/component'
-import { tracked } from '@glimmer/tracking'
-import { ajax } from 'discourse/lib/ajax'
-import { buildMbUrl } from '../lib/listenbrainz-utils'
-
-export default class ListeningComponent extends Component {
-
- constructor(owner, args) {
- super(owner, args)
- this.user = args.user
- this.loadCurrentPinPlaying()
- }
-
- @tracked show = true
- @tracked loading = true
- @tracked listen = null
-
- get artist() {
- return this.artistCredits.reduce((result, c) => {
- result.push(c.artist_credit_name)
- if (c.join_phrase) {
- result.push(c.join_phrase)
- }
- return result
- }, []).join('')
- }
-
- get title() {
- return this.listen?.track_metadata?.track_name
- }
-
- get album() {
- return this.listen?.track_metadata?.release_name
- }
-
- get blurb() {
- return this.listen?.blurb_content
- }
-
- get artistCredits() {
- const metadata = this.listen?.track_metadata
- let artists = metadata?.mbid_mapping?.artists
- if (!artists || artists.length === 0) {
- // No detailed artist credits, build a single credit from artist name
- const artistMbids = metadata?.additional_info?.artist_mbids ||
- metadata?.mbid_mapping?.artist_mbids
- artists = [{
- // Link only if there is exactly one artist
- artist_mbid: artistMbids?.length === 1 ? artistMbids[0] : null,
- artist_credit_name: metadata?.mbid_mapping?.artist_credit_name ||
- metadata?.artist_name,
- join_phrase: '',
- }]
- }
- return artists
- }
-
- get duration() {
- const additional_info = this.listen?.track_metadata?.additional_info
- if (additional_info) {
- const duration = additional_info.duration_ms ?
- additional_info.duration_ms / 1000 :
- additional_info.duration
- if (duration) {
- const minutes = parseInt(duration / 60, 10)
- const seconds = String(parseInt(duration % 60, 10)).padStart(2, '0')
- return `${minutes}:${seconds}`
- }
- }
- return ''
- }
-
- get pinDateTime() {
- const timestamp = this.listen?.created
- return timestamp ? new Date(timestamp * 1000) : null
- }
-
- get recordingMbid() {
- const metadata = this.listen?.track_metadata
- return metadata?.additional_info?.recording_mbid ||
- metadata?.mbid_mapping?.recording_mbid
- }
-
- get releaseMbid() {
- const metadata = this.listen?.track_metadata
- return metadata?.additional_info?.release_mbid ||
- metadata?.mbid_mapping?.release_mbid
- }
-
- get recordingUrl() {
- return buildMbUrl('recording', this.recordingMbid)
- }
-
- get releaseUrl() {
- return buildMbUrl('release', this.releaseMbid)
- }
-
- get playUrl() {
- const recordingMbid = this.recordingMbid
- if (recordingMbid) {
- return `https://listenbrainz.org/player?recording_mbids=${encodeURIComponent(recordingMbid)}`
- }
- }
-
- get coverUrl() {
- return this.listen?.coverart_url
- }
-
- get enableProfileFallback() {
- return !this.show && this.user.custom_fields.listenbrainz_display === 'listen_profile_fallback'
- }
-
- async loadCurrentPinPlaying() {
- this.loading = true
- try {
- const listen = await ajax(`/listenbrainz/pin/${this.user.id}`)
- this.show = !!listen
- this.listen = listen
- } catch(err) {
- this.show = false
- console.error(err)
- } finally {
- this.loading = false
- }
- }
-}
M assets/javascripts/discourse/components/listenbrainz-preferences.js => assets/javascripts/discourse/components/listenbrainz-preferences.js +0 -1
@@ 7,7 7,6 @@ import I18n from "I18n"
const displayOptions = [
'profile',
'listen',
- 'pinned_recording',
'listen_profile_fallback',
]
M assets/javascripts/discourse/templates/components/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/components/listenbrainz-listening.hbs +8 -4
@@ 16,8 16,8 @@
{{else}}
<div class="listenbrainz-card-primary">
<div class="listenbrainz-card-title" title="{{title}}">
- {{#if listen.playing_now}}
- <span title="{{i18n "listenbrainz.listening.playing_now_tooltip"}}">🎶</span>
+ {{#if hasIcon}}
+ <span title="{{iconTooltip}}">{{d-icon icon}}</span>
{{/if}}
{{#if recordingMbid}}
<a href="{{recordingUrl}}" target="_blank">{{title}}</a>
@@ 56,8 56,12 @@
</div>
</div>
</div>
+ {{#if blurb}}
+ <div class="listenbrainz-card-additional-content">
+ <blockquote>{{blurb}}</blockquote>
+ </div>
+ {{/if}}
</div>
-{{/if}}
-{{#if enableProfileFallback}}
+{{else if enableProfileFallback}}
{{listenbrainz-profile user=user}}
{{/if}}
D assets/javascripts/discourse/templates/components/listenbrainz-pinned-recording.hbs => assets/javascripts/discourse/templates/components/listenbrainz-pinned-recording.hbs +0 -65
@@ 1,65 0,0 @@
-{{#if show }}
-<div class="listenbrainz-card listenbrainz-listening">
- <div class="listenbrainz-card-main-content">
- <div class="listenbrainz-card-thumbnail">
- {{#if releaseMbid}}
- <a href="{{releaseUrl}}" title="{{album}}" target="_blank">
- {{listenbrainz-cover src=coverUrl}}
- </a>
- {{else}}
- {{listenbrainz-cover src=coverUrl title=album}}
- {{/if}}
- </div>
- <div class="listenbrainz-card-details">
- {{#if loading }}
- {{i18n "listenbrainz.listening.loading"}}
- {{else}}
- <div class="listenbrainz-card-primary">
- <div class="listenbrainz-card-title" title="{{title}}">
- <span title="{{i18n "listenbrainz.listening.pin_tooltip"}}">❤️</span>
- {{#if recordingMbid}}
- <a href="{{recordingUrl}}" target="_blank">{{title}}</a>
- {{else}}
- {{title}}
- {{/if}}
- </div>
- <div class="listenbrainz-card-duration" title="{{i18n "listenbrainz.listening.duration_tooltip"}}">
- {{duration}}
- </div>
- </div>
- <div class="listenbrainz-card-secondary" title="{{artist}}">
- {{listenbrainz-artist-credits artists=artistCredits}}
- </div>
- {{/if}}
- </div>
- <div class="listenbrainz-card-right-section">
- <div class="listenbrainz-card-timestamp">
- <span class="listenbrainz-card-listen-time" title="{{pinDateTime}}">
- {{format-date pinDateTime leaveAgo="true" format="medium"}}
- </span>
- </div>
- <div class="listenbrainz-card-controls">
- {{#if recordingMbid}}
- <a class="btn-flat listenbrainz-play-button"
- title="{{i18n "listenbrainz.listening.play_on_listenbrainz"}}"
- href="{{playUrl}}"
- target="_blank">
- <svg viewBox="0 0 512 512">
- <path fill="currentColor"
- d="M371.7 238l-176-107c-15.8-8.8-35.7 2.5-35.7 21v208c0 18.4 19.8 29.8 35.7 21l176-101c16.4-9.1 16.4-32.8 0-42zM504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256z">
- </path>
- </svg>
- </a>
- {{/if}}
- </div>
- </div>
- </div>
- {{#if blurb}}
- <div class="listenbrainz-card-additional-content">
- <blockquote>{{blurb}}</blockquote>
- </div>
- {{/if}}
-</div>
-{{else if enableProfileFallback}}
-{{listenbrainz-profile user=user}}
-{{/if}}
M assets/javascripts/discourse/templates/components/listenbrainz-preferences.hbs => assets/javascripts/discourse/templates/components/listenbrainz-preferences.hbs +4 -0
@@ 49,6 49,10 @@
{{/if}}
{{#if showListenOptions}}
+ {{preference-checkbox
+ labelKey="listenbrainz.preferences.show_pinned_recording"
+ checked=user.custom_fields.listenbrainz_show_pinned_recording}}
+
<div class="controls">
<label>
{{i18n "listenbrainz.preferences.listen_max_age_label"}}
M assets/javascripts/discourse/templates/connectors/user-card-after-metadata/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/connectors/user-card-after-metadata/listenbrainz-listening.hbs +0 -2
@@ 1,8 1,6 @@
{{#if user.custom_fields.listenbrainz_enable}}
{{#if (eq user.custom_fields.listenbrainz_display 'profile')}}
{{listenbrainz-profile user=user}}
- {{else if (eq user.custom_fields.listenbrainz_display 'pinned_recording')}}
- {{listenbrainz-pinned-recording user=user}}
{{else}}
{{listenbrainz-listening user=user}}
{{/if}}
M assets/javascripts/discourse/templates/connectors/user-profile-primary/listenbrainz-listening.hbs => assets/javascripts/discourse/templates/connectors/user-profile-primary/listenbrainz-listening.hbs +0 -2
@@ 1,8 1,6 @@
{{#if model.custom_fields.listenbrainz_enable}}
{{#if (eq model.custom_fields.listenbrainz_display 'profile')}}
{{listenbrainz-profile user=model}}
- {{else if (eq model.custom_fields.listenbrainz_display 'pinned_recording')}}
- {{listenbrainz-pinned-recording user=model}}
{{else}}
{{listenbrainz-listening user=model}}
{{/if}}
M assets/stylesheets/listenbrainz.scss => assets/stylesheets/listenbrainz.scss +4 -0
@@ 91,6 91,10 @@ aside.onebox.listenbrainzprofile {
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
+
+ .d-icon {
+ color: var(--tertiary);
+ }
}
.listenbrainz-card-duration {
M config/locales/client.de.yml => config/locales/client.de.yml +2 -1
@@ 4,6 4,7 @@ de:
profile_link_tooltip: '{{username}}s Profil auf ListenBrainz'
listening:
loading: Lade Listen…
+ pin_tooltip: Angeheftete Aufnahme
playing_now_tooltip: Hört gerade
play_on_listenbrainz: Spiele auf ListenBrainz
duration_tooltip: "Spieldauer"
@@ 25,7 26,6 @@ de:
display_options:
listen: Zuletzt gehört
profile: Benutzerprofil-Statistiken
- pinned_recording: Angeheftete Aufnahme
listen_profile_fallback: Zuletzt gehört, fällt zurück auf Benutzerprofil-Statistiken
range_label: Zeitraum
range_options:
@@ 33,6 33,7 @@ de:
this_month: Diesen Monat
this_year: Dieses Jahr
all_time: Insgesamt
+ show_pinned_recording: Angeheftete Aufnahme statt letztem Listen anzeigen, falls verfügbar
listen_max_age_label: Kürzliches Listen nur anzeigen, wenn nicht älter als
listen_max_age_options:
0: Nur "hört gerade"
M config/locales/client.en.yml => config/locales/client.en.yml +2 -1
@@ 4,6 4,7 @@ en:
profile_link_tooltip: "{{username}}’s profile on ListenBrainz"
listening:
loading: "Loading listen…"
+ pin_tooltip: "Pinned recording"
playing_now_tooltip: "Currently listening"
play_on_listenbrainz: "Play on ListenBrainz"
duration_tooltip: "Duration"
@@ 25,7 26,6 @@ en:
display_options:
listen: "Most recent listen"
profile: "User profile statistics"
- pinned_recording: "Pinned recording"
listen_profile_fallback: "Most recent listen with fallback to user profile statistics"
range_label: "Time range"
range_options:
@@ 34,6 34,7 @@ en:
this_year: "This year"
all_time: "All time"
range_fallback_label: "Fallback to larger time range if there are no listens in the selected range"
+ show_pinned_recording: "Show pinned recording instead of recent listen, if available"
listen_max_age_label: "Show recent listen not older than"
listen_max_age_options:
0: "Only now playing"
M plugin.rb => plugin.rb +8 -2
@@ 45,17 45,23 @@ after_initialize do
register_editable_user_custom_field :listenbrainz_profile_range_fallback
DiscoursePluginRegistry.serialized_current_user_fields << 'listenbrainz_profile_range_fallback'
+ User.register_custom_field_type 'listenbrainz_show_pinned_recording', :boolean
+ register_editable_user_custom_field :listenbrainz_show_pinned_recording
+ DiscoursePluginRegistry.serialized_current_user_fields << 'listenbrainz_show_pinned_recording'
+
User.register_custom_field_type 'listenbrainz_max_listen_age_hours', :int
register_editable_user_custom_field :listenbrainz_max_listen_age_hours
DiscoursePluginRegistry.serialized_current_user_fields << 'listenbrainz_max_listen_age_hours'
+ # Extra icon
+ register_svg_icon 'music'
+ register_svg_icon 'thumbtack'
+
# Register routes
require_relative "app/controllers/listenbrainz_listen_controller"
- require_relative "app/controllers/listenbrainz_pinned_recording_controller"
require_relative "app/controllers/listenbrainz_profile_controller"
Discourse::Application.routes.append do
get "listenbrainz/now-playing/:user_id" => "listenbrainz_listen#now_playing"
- get "listenbrainz/pin/:user_id" => "listenbrainz_pinned_recording#current_pin"
get "listenbrainz/profile/:user_id" => "listenbrainz_profile#profile"
end
end