gmg137 / netease-cloud-music-gtk

Linux 平台下基于 Rust + GTK 开发的网易云音乐播放器
GNU General Public License v3.0
1.5k stars 89 forks source link

通过 Mpris 接口 `Metadata::xesam:asText` 提供歌词 #268

Open tuberry opened 6 months ago

tuberry commented 6 months ago

这个 Feature Request 算是此评论的后续。

这样做可以每首歌少读一次文件,而且文件名格式更自由有利于解决 #264 这类问题,也可以加songId之类的精确匹配。

我不会 rust,弄的这个 patch 可能有问题,但可以工作:

lyrics.patch ```diff diff --git a/src/audio/mpris.rs b/src/audio/mpris.rs index b4cce1a..48e1097 100644 --- a/src/audio/mpris.rs +++ b/src/audio/mpris.rs @@ -55,8 +55,9 @@ impl MprisController { Ok(Self { mpris_player }) } - pub async fn update_metadata(&self, si: &SongInfo) -> Result<()> { + pub async fn update_metadata(&self, si: &SongInfo, lrc: String) -> Result<()> { let mut metadata = Metadata::new(); + metadata.set_lyrics(Some(lrc.clone())); metadata.set_artist(Some(vec![si.singer.clone()])); metadata.set_title(Some(si.name.clone())); metadata.set_album(Some(si.album.clone())); diff --git a/src/gui/player_controls.rs b/src/gui/player_controls.rs index ba12410..741717b 100644 --- a/src/gui/player_controls.rs +++ b/src/gui/player_controls.rs @@ -19,7 +19,7 @@ use once_cell::sync::*; use crate::{application::Action, audio::*, model::ImageDownloadImpl, path::CACHE}; use std::{ - cell::Cell, + cell::{Cell, RefCell}, fs, path, rc::Rc, sync::{Arc, Mutex}, @@ -177,11 +177,12 @@ impl PlayerControls { artist_label.set_label(&song_info.singer); let volume = self.property("volume"); + let lyrics: String = self.property("lyrics"); if let Some(mpris) = imp.mpris.get() { crate::MAINCONTEXT.spawn_local_with_priority( Priority::LOW, clone!(@weak mpris => async move { - if let Err(err) = mpris.update_metadata(&song_info).await { + if let Err(err) = mpris.update_metadata(&song_info, lyrics).await { warn!("设置 MPRIS metadata 失败: {err:?}"); } if let Err(err) = mpris.set_playback_status(PlaybackStatus::Playing).await { @@ -324,13 +325,29 @@ impl PlayerControls { self.set_property("duration", sec); + let lyrics: String = self.property("lyrics"); if let Some(mpris) = imp.mpris.get() { if let Some(mut si) = self.get_current_song() { si.duration = msec / 1000; crate::MAINCONTEXT.spawn_local_with_priority( Priority::LOW, clone!(@weak mpris => async move { - mpris.update_metadata(&si).await.ok(); + mpris.update_metadata(&si, lyrics).await.ok(); + }), + ); + } + } + } + + pub fn metadata_lyrics_update(&self, lrc: String) { + self.set_property("lyrics", lrc.clone()); + let imp = self.imp(); + if let Some(mpris) = imp.mpris.get() { + if let Some(mut si) = self.get_current_song() { + crate::MAINCONTEXT.spawn_local_with_priority( + Priority::LOW, + clone!(@weak mpris => async move { + mpris.update_metadata(&si, lrc).await.ok(); }), ); } @@ -654,7 +671,7 @@ impl PlayerControls { mod imp { - use gst::glib::Propagation; + use gst::glib::{ParamSpecString, Propagation}; use super::*; @@ -713,6 +730,7 @@ mod imp { // 播放条拖动结束时的值 scale_value: Cell, + lyrics: RefCell, } #[glib::object_subclass] @@ -934,6 +952,7 @@ mod imp { ParamSpecUInt64::builder("duration").build(), ParamSpecBoolean::builder("like").readwrite().build(), ParamSpecDouble::builder("scale-value").readwrite().build(), + ParamSpecString::builder("lyrics").build(), ] }); PROPERTIES.as_ref() @@ -965,6 +984,10 @@ mod imp { let scale_value = value.get().expect("The value needs to be of type `bool`."); self.scale_value.replace(scale_value); } + "lyrics" => { + let val = value.get().unwrap(); + self.lyrics.replace(val); + } n => unimplemented!("{}", n), } } @@ -977,6 +1000,7 @@ mod imp { "duration" => self.duration.get().to_value(), "like" => self.like.get().to_value(), "scale-value" => self.scale_value.get().to_value(), + "lyrics" => self.lyrics.borrow().to_value(), n => unimplemented!("{}", n), } } diff --git a/src/ncmapi.rs b/src/ncmapi.rs index 16b9506..fcccaac 100644 --- a/src/ncmapi.rs +++ b/src/ncmapi.rs @@ -169,19 +169,16 @@ impl NcmClient { pub async fn get_lyrics(&self, si: SongInfo) -> Result { let mut path = LYRICS.clone(); path.push(format!("{}-{}-{}.lrc", si.name, si.singer, si.album)); - let re = regex::Regex::new(r"\[\d+:\d+.\d+\]").unwrap(); if !path.exists() { if let Ok(lyr) = self.client.song_lyric(si.id).await { let lrc = lyr.into_iter().collect::>().join("\n"); fs::write(&path, &lrc)?; - let lrc = re.replace_all(&lrc, "").to_string(); Ok(lrc) } else { Ok(gettextrs::gettext("No lyrics found!".to_owned())) } } else { let lrc = fs::read_to_string(&path)?; - let lrc = re.replace_all(&lrc, "").to_string(); Ok(lrc) } } diff --git a/src/window.rs b/src/window.rs index 58dcd6e..c348c42 100644 --- a/src/window.rs +++ b/src/window.rs @@ -674,9 +674,12 @@ impl NeteaseCloudMusicGtk4Window { pub fn update_lyrics(&self, lrc: String) { let imp = self.imp(); + let re = regex::Regex::new(r"\[\d+:\d+.\d+\]").unwrap(); + let txt = re.replace_all(&lrc, "").to_string(); let page = imp.playlist_lyrics_page.get().unwrap(); + imp.player_controls.get().metadata_lyrics_update(lrc); if self.page_cur_playlist_lyrics_page() { - page.update_lyrics(lrc); + page.update_lyrics(txt); } } ```

貌似 Github 不支持上传 patch 文件,那直接贴这儿了。

gmg137 commented 6 months ago

没有使用mpris的歌词接口是为了减少网络请求次数,将歌词缓存在本地,当再次需要时可以直接从本地读取。缓存lrc和使用asText功能上是重复的,如果只实现一个的话缓存lrc性价比会更高一些。

tuberry commented 6 months ago

上面的 patch 只是在更新 PlayListLyricsPage 的歌词时顺便更一下 Mpris,获取歌词的方式根本没变。更新 Mpris 也需要网络请求吗?

gmg137 commented 6 months ago

我的意思是,更新Mpris歌词与缓存歌词文件在目的上是重复的,如果二选一选择mpris的asText传送歌词,会造成每次都要请求网络。

另外如果只在打开播放列表页面时才触发更新mpris歌词,这样该操作是没有意义的。

tuberry commented 6 months ago

另外如果只在打开播放列表页面时才触发更新mpris歌词,这样该操作是没有意义的。

在打开判定之前更新的。

这里启用桌面歌词支持时每次都会下载/读取本地歌词,顺便更新一下 Mpris 不好吗?读了不能传,要传不能存(读),苦涩的选择。目前如果本地有文件,扩展那边要多读一次,否则就多一次注定失败的网络请求,也算重复吧。既然如此,你可能要按需读取。

谢谢回答。

tuberry commented 6 months ago

另外发现在这每250ms更新进度条时 Mpris 会发Seeked信号,同时更新Mpris::Position。这似乎都没有必要

建议在自动更新进度条时不要发Seeked,至于Position之前就建议参考Lollypop按需直接Gst.Player读取不用更新。

谢谢。

gmg137 commented 6 months ago

如果不更新 position 你的歌词插件该如何确定当前播放位置? 特别是反复前后拖拉进度条后,会不会有影响。 你说的按需直接从Gst.Player读取我没太明白,这里是谁来读取?

更新 seeked 是为了让别的 mpris 播放插件同步播放条进度。

我上次测试gnome-music 在反复前后拖拉进度条时好像会卡住,具体情况忘了,反正就是做了取舍才弄成现在这样。

最近在忙装修,可以把你的建议先说下,我等闲了再研究研究看怎么弄。

tuberry commented 6 months ago

如果不更新 position 你的歌词插件该如何确定当前播放位置? 特别是反复前后拖拉进度条后,会不会有影响。

客户端维护有 timer,获取当前 position 一般为初始化而非同步。拖动进度条结束发一次Seeked就行,客户端不关心怎么拖的。

你说的按需直接从Gst.Player读取我没太明白,这里是谁来读取?

服务端Position不像Volume之类的会同步到客户端,客户端通过 org.freedesktop.DBus.Properties.Get 接口来获取 Position,所以服务端在实现此接口时,对Position直接返回 Gst.Player 的位置即可,而非定期set_position缓存一个随时过期的值待用。

我上次测试gnome-music 在反复前后拖拉进度条时好像会卡住,具体情况忘了,反正就是做了取舍才弄成现在这样。

gnome-music 的实现有问题,表现为拖动过程中 PlaybackStatusPlayingPaused 间反复横跳,与客户端无关。

更新 seeked 是为了让别的 mpris 播放插件同步播放条进度。

这听起来有点像某软件每秒截30次屏来实现屏幕共享。尤其结合上文,为某进度条每250ms发一次信号,却不愿为歌词更一次元数据,就显得很有意思:) 总之,这信号据说 indicates that the track position has changed in a way that is inconsistant with the current playing state,用于同步不失为另辟蹊径吧。

最近在忙装修,可以把你的建议先说下,我等闲了再研究研究看怎么弄。

祝好。建议先前有一些了,刚打开播放列表没找到移除歌曲的办法,不确定这是 Feature 还是 Bug。

就这样吧。

gmg137 commented 3 months ago

你好,我在尝试使用 asText 提供歌词时,desktop-lyrics 没有正常工作,问题如下:

1、播放歌曲时,在使用asText推送歌词的情况下,desktop-lyrics 依然会自动下载歌词,测试应该是没有使用我用asText提供的歌词;

2、当播放下面的歌曲时,没有任何反应,也没有自动下载歌词。

https://music.163.com/song?id=2021437775 歌曲:My Stupid Heart (Kids Version) 歌手:Walk off the Earth

歌词如下(已测试去掉换行符问题依旧):

"[00:00.000] 作曲 : Gianni Luminati Nicassio/Jake Torrey/Lostboy/Michael Matosic/Sarah Blackwood/Tokyo Speirs\n[00:00.112]Hey, everybody\n[00:00.514]My mom 'n dad made this new song called \"My Stupid Heart\"\n[00:03.547]I'm gonna play it with my brothers\n[00:05.187]Let's gooooooo!!\n[00:07.074]\n[00:07.337]My stupid heart\n[00:08.881]Don't know\n[00:09.983]I've tried to let you go\n[00:11.850]So many times before\n[00:13.938]Then wound up at your door\n[00:15.765]My stupid\n[00:16.149]\n[00:16.524]Can't believe that I haven't figured out by now\n[00:19.634]Every time I call you up\n[00:21.842]All you do is let me down (Let's Go!)\n[00:24.487]\n[00:24.833]Shoulda known there was nothing about us I could change\n[00:28.105]Everytime we try to be friends\n[00:30.112]It always ends the same\n[00:32.207]\n[00:32.368]But when I try to remember\n[00:36.376]All the pain that we've been through\n[00:40.596]Something in me says whatever\n[00:44.781]And it brings me back to you\n[00:48.172]\n[00:48.869]My stupid heart\n[00:50.638]Don't know\n[00:51.631]I've tried to let you go\n[00:53.583]So many times before\n[00:55.604]Then wound up at your door\n[00:57.482]My stupid heart\n[00:58.854]Too late\n[00:59.796]Already on my way\n[01:01.920]If we go down in flames\n[01:04.123]Again then you can blame my stupid heart\n[01:09.832](Okay!)\n[01:11.917](Okay!)\n[01:13.068]You can blame my stupid heart\n[01:18.386](Okay!)\n[01:20.658](Okay!)\n[01:21.279]You can blame my stupid heart\n[01:23.518]\n[01:23.737]Every now and then I get inside my head\n[01:26.555]Try to leave your text unread\n[01:28.357]But I wind up here instead\n[01:30.968]\n[01:31.676]I shoulda bit my tongue while we were still ahead\n[01:34.787]Yeah always had to be right\n[01:36.800]Til we had nothing left\n[01:39.079]\n[01:39.238]But when I try to remember\n[01:43.129]All the pain that we've been through\n[01:47.364]Something in me says whatever\n[01:51.615]And it brings me back to you\n[01:55.052]\n[01:55.613]My stupid heart\n[01:57.328]Don't know\n[01:58.440]I've tried to let you go\n[02:00.248]So many times before\n[02:02.418]Then wound up at your door\n[02:04.002]My stupid heart\n[02:05.665]Too late\n[02:06.822]Already on my way\n[02:08.590]If we go down in flames\n[02:10.830]Again then you can blame my stupid heart (Let's Go!)\n[02:16.703](Okay!)\n[02:18.945](Okay!)\n[02:19.437]\n[02:19.607]You can blame my stupid heart\n[02:23.868]I've tried to let you go\n[02:25.389]So many times before\n[02:29.064]You can blame my stupid heart\n[02:30.832]Too late\n[02:32.142]Already on my way\n[02:33.964]If we go down in flames\n[02:35.902]Again then you can blame my stupid heart\n[02:39.253]Too late\n[02:40.317]Already on my way\n[02:42.139]If we go down in flames\n[02:44.276]Again then you can blame my stupid heart"

tuberry commented 3 months ago

GNOME45及以后的版本才支持,如果版本没问题看看Metadata是否提供了歌词:

gdbus call --session --dest org.mpris.MediaPlayer2.NeteaseCloudMusicGtk4 --object-path /org/mpris/MediaPlayer2 --method org.freedesktop.DBus.Properties.Get org.mpris.MediaPlayer2.Player Metadata

另外无asText字段时会尝试下载,所以初始化暂无歌词时需设置asText为空(字符串),有歌词再更新即可。

gmg137 commented 3 months ago

GNOME 版本是 45.3, Metadata 中可以正常获取歌词: (<{'mpris:artUrl': <'file:///home/gmg/.cache/netease-cloud-music-gtk4/159843058-songlist.jpg'>, 'mpris:length': <int64 168112000>, 'mpris:trackid': <objectpath '/com/gitee/gmg137/NeteaseCloudMusicGtk4/2021437775'>, 'xesam:album': <'My Stupid Heart (Kids Version)'>, 'xesam:artist': <['Walk off the Earth']>, 'xesam:asText': <"[00:00.000] 作词 : Kim Taylor\n[00:00.000] 作曲 : Kim Taylor\n[00:00.000]La di da di da da\n[00:03.860]La di da di da da\n[00:08.110]La di da di da da\n[00:12.000]La da da\n[00:14.760]\n[00:16.510]I am tied by truth like an anchor\n[00:25.440]Anchored to a bottomless sea\n[00:32.340]I am floating freely in the heavens\n[00:41.440]Held in by your heart's gravity\n[00:46.160]\n[00:48.920]All because of love\n[00:53.040]All because of love\n[00:57.410]Even though sometimes\n[00:59.999]You don't know who I am\n[01:03.259]\n[01:05.239]I am you..\n[01:09.290]Everything you do\n[01:13.168]Anything you say,\n[01:17.319]You want me to be\n[01:21.328]You and me..\n[01:25.290]We're charms on a chain\n[01:29.790]Linked eternally one we can't undo\n[01:37.110]And I am you\n[01:40.790]\n[01:41.890]La di da di da da\n[01:45.840]La di da di da da\n[01:49.799]La da da\n[01:52.099]\n[01:53.920]All my senses awaken to the changes yeah\n[02:02.209]And I feel alive inside my own skin\n[02:10.159]All my reasons tell me just how strange it is\n[02:18.409]Coming home to a place I've always been\n[02:24.199]\n[02:26.439]And it's all for love\n[02:30.530]And it's all for love\n[02:34.739]Even though sometimes,\n[02:37.429]I don't know who I am\n[02:40.639]\n[02:42.629]I am you..\n[02:46.699]Everything you do\n[02:50.360]Anything you say,\n[02:54.549]You want me to be\n[02:58.810]You and me..\n[03:02.739]We're charms on a chain\n[03:07.109]Linked eternally one we can't undo, ohhh\n[03:14.829]I am you...\n[03:18.979]La di da di da da\n[03:22.869]La di da di da da (anything you say)\n[03:27.069]La da da (everything you do)\n[03:30.970]La di da di da da (I am you...)\n[03:35.129]La di da di da da\n[03:39.169]La di da di da da (anything you say)\n[03:43.160]La da da (everything you do)\n[03:47.209]I am you... (La di da di da da)\n[03:51.190]I am you... (La di da di da da)\n[03:55.119]I am you... (La di da di da da)\n[03:59.049]I am you... (La di da di da da)\n[04:01.639]\n[04:03.199]I am you... (La di da di da da)\n[04:07.139]I am you... (La di da di da da)\n[04:11.379]Everything you do....\n[04:18.069]\n[04:20.459]Fading away...\n[04:28.379]">, 'xesam:title': <'My Stupid Heart (Kids Version)'>}>,)

下面是我编译好的程序你可以测试下,如果不能运行我再把源代码打包了发你。 netease-cloud-music-gtk4.zip

tuberry commented 2 months ago

此提交(对应扩展网站上的版本19)在GNOME46上测试发现你编译的 grsource 文件路径硬编码为本地,不过我用软连接解决了。 随机播放歌曲均显示为你上方提供的歌词,扩展本身并未下载/保存任何文件:

lrc-ncm-gtk

gmg137 commented 2 months ago

已经可以正常显示了,不过osdlyrics 貌似不支持 xesam:asText 接口,这样的话还是要保留从歌词文件读取歌词的功能。