Skip to content

Commit ad9af42

Browse files
committed
Mark 「RSpotify: 一个用爱发电五年的开源项目」 as highlighted post
1 parent d81e69f commit ad9af42

File tree

2 files changed

+66
-64
lines changed

2 files changed

+66
-64
lines changed

content/zh/post/2018/rspotify.md

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
+++
2-
title = "rspotify– 我的第一个Rust crate"
3-
date = 2018-02-28T20:44:00+08:00
4-
lastmod = 2022-02-24T22:16:22+08:00
2+
title = "RSpotify– 我的第一个Rust crate"
3+
date = 2018-02-28T20:44:00-08:00
4+
lastmod = 2024-12-30T22:28:21-08:00
55
tags = ["rust", "rspotify"]
66
categories = ["rust"]
77
draft = false
@@ -10,7 +10,9 @@ toc = true
1010

1111
开发第一个Rust crate 的感受和踩到的坑
1212

13-
最近写了人生第一个 Rust crate-- [rspotify](https://github.com/samrayleung/rspotify). 虽说并不是什么惊天地,泣鬼神的大作,但是也是我花费了近两个月实现的。
13+
最近写了人生第一个 Rust crate -- [RSpotify](https://github.com/ramsayleung/rspotify).
14+
15+
虽说并不是什么惊天地,泣鬼神的大作,但是也是我花费了近两个月实现的。
1416

1517
现在就来聊聊这个开发过程的感悟和踩到的坑
1618

@@ -27,18 +29,19 @@ toc = true
2729
```python
2830
def artist_top_tracks(self, artist_id, country='US'):
2931
""" Get Spotify catalog information about an artist's top 10 tracks
30-
by country.
32+
by country.
3133
32-
Parameters:
33-
- artist_id - the artist ID, URI or URL
34-
- country - limit the response to one particular country.
34+
Parameters:
35+
- artist_id - the artist ID, URI or URL
36+
- country - limit the response to one particular country.
3537
"""
3638

3739
trid = self._get_id('artist', artist_id)
3840
return self._get('artists/' + trid + '/top-tracks', country=country)
3941
```
4042

41-
但是用 Rust 来实现的时候,问题就来了,因为Rust 是没有缺省参数的。而Rust 处理缺省参数的策略一般是`Builder Pattern`:
43+
但是用 Rust 来实现的时候,问题就来了,因为Rust 是没有缺省参数的。而Rust 处理缺省
44+
参数的策略一般是=Builder Pattern=:
4245

4346
```rust
4447
struct Part1 {
@@ -49,24 +52,24 @@ struct Part1 {
4952

5053
impl Part1 {
5154
fn new() -> Part1 {
52-
Part1 { points: 30_u32, tf: 3_f64, dt: 0.1_f64 }
55+
Part1 { points: 30_u32, tf: 3_f64, dt: 0.1_f64 }
5356
}
5457

5558
fn tf(mut self, tf: f64) -> Self {
56-
self.tf = tf;
57-
self
59+
self.tf = tf;
60+
self
5861
}
5962
fn points(mut self, points: u32) -> Self {
60-
self.points = points;
61-
self
63+
self.points = points;
64+
self
6265
}
6366
fn dt(mut self, dt: f64) -> Self {
64-
self.dt = dt;
65-
self
67+
self.dt = dt;
68+
self
6669
}
6770
fn run(self) {
68-
// code here
69-
println!("{:?}", self);
71+
// code here
72+
println!("{:?}", self);
7073
}
7174
}
7275

@@ -78,7 +81,7 @@ Part1::new().tf(7_f64).dt(15_f64).run();
7881
具体情况具体分析,就 rspotify 而言, `Builder Pattern` 并不适用,因为 rspotify
7982
有很多函数都需要缺省参数,而不同函数的缺省值可能又不一样。
8083

81-
例如,有些函数的 `offset`参数是 0, 而另外一些函数的 `offset` 参数是1. 为此,我还在 [Reddit](https://www.reddit.com/r/rust/comments/7u1zjl/question_about_default_values_for_function/) 发贴询问意见,[PM_ME_WALLPAPER](https://www.reddit.com/user/PM_ME_WALLPAPER) 建议我用`Into<Option<T>>`:
84+
例如,有些函数的 `offset=参数是 0, 而另外一些函数的 =offset` 参数是1. 为此,我还在 [Reddit](https://www.reddit.com/r/rust/comments/7u1zjl/question_about_default_values_for_function/) 发贴询问意见,[PM_ME_WALLPAPER](https://www.reddit.com/user/PM_ME_WALLPAPER) 建议我用=Into&lt;Option&lt;T&gt;&gt;=:
8285

8386
```rust
8487
fn foo<T: Into<Option<usize>>>(limit: T) {
@@ -102,17 +105,17 @@ pub fn artist_top_tracks(
102105
url.push_str(&trid);
103106
url.push_str("/top-tracks");
104107
match self.get(&mut url, &mut params) {
105-
Some(result) => {
106-
// let mut albums: Albums = ;
107-
match serde_json::from_str::<FullTracks>(&result) {
108-
Ok(_tracks) => Some(_tracks),
109-
Err(why) => {
110-
eprintln!("convert albums from String to Albums failed {:?}", why);
111-
None
112-
}
113-
}
114-
}
115-
None => None,
108+
Some(result) => {
109+
// let mut albums: Albums = ;
110+
match serde_json::from_str::<FullTracks>(&result) {
111+
Ok(_tracks) => Some(_tracks),
112+
Err(why) => {
113+
eprintln!("convert albums from String to Albums failed {:?}", why);
114+
None
115+
}
116+
}
117+
}
118+
None => None,
116119
}
117120
}
118121
```
@@ -124,9 +127,10 @@ pub fn artist_top_tracks(
124127

125128
对于一个 library 而言,错误处理是设计的重要一环。
126129

127-
因为我之前只有开发应用的经验, 而开发应用的错误处理和开发类库的错误处理显然需要考虑的东西不一样,所以我还谨慎思考过这个问题。后来,我决定不处理调用Spotify API 或者其他操作导致的错误,将错误进行一次包装(wrap), 然后再返回给library 的调用者。
130+
因为我之前只有开发应用的经验, 而开发应用的错误处理和开发类库的错误处理显然需要考虑的东西不一样,所以我还谨慎思
131+
考过这个问题。后来,我决定不处理调用Spotify API 或者其他操作导致的错误,将错误进行一次包装(wrap), 然后再返回给library 的调用者。
128132

129-
最开始的时候,我是自己定义错误类型的,后来觉得过于累赘,就用上`error_chain`. 用上 error_chain 之后, `errors.rs`这个文件也非常简单:
133+
最开始的时候,我是自己定义错误类型的,后来觉得过于累赘,就用上=error_chain=. 用上 error_chain 之后, =errors.rs=这个文件也非常简单:
130134

131135
```rust
132136
///The kind of spotify error.
@@ -136,27 +140,28 @@ error_chain! {
136140
errors {}
137141

138142
foreign_links {
139-
Json(serde_json::Error) #[doc = "An error happened while serializing JSON"];
143+
Json(serde_json::Error) #[doc = "An error happened while serializing JSON"];
140144
}
141145
}
142146
```
143147

144-
而刚刚我提到了只对错误作简单的包装,得益于 `error_chain`的设计,这个特性也很容易实现:
148+
而刚刚我提到了只对错误作简单的包装,得益于 =error_chain=的设计,这个特性也很容易实现:
145149

146150
```rust
147151
pub fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> Result<T> {
148152
let result = serde_json::from_str::<T>(input)
149-
.chain_err(|| format!("convert result failed, content {:?}",input))?;
153+
.chain_err(|| format!("convert result failed, content {:?}",input))?;
150154
Ok(result)
151155
}
152156
```
153157

154-
这个函数是将 Spotify 的响应体映射成对应的 object(例如 playlist, album 等). 如果转换过程出错了,那么就返回`convert result failed, content {:?}`错误信息之后,返回 `serde_json` 转换时出现的错误信息。
158+
这个函数是将 Spotify 的响应体映射成对应的 object(例如 playlist, album 等). 如果转换过程出错了,那么就返回=convert result failed, content {:?}=错误信息之后,返回 `serde_json` 转换时出现的错误信息。
155159

156160

157161
### <span class="section-num">1.3</span> Reddit+clippy {#reddit-plus-clippy}
158162

159-
剩下的是在纠结定义一个函数传参的时候是传值,参数是 mutable 还是 immutable, 以及其他类似的考虑。
163+
剩下的是在纠结定义一个函数传参的时候是传值,参数是 mutable 还是 immutable, 以及
164+
其他类似的考虑。
160165

161166
或许 Effective Rust 和 More Effective Rust 出现之后,我读完就知道什么样的设计才是 best practice. 因为有诸多设计的不确定,所以在完成rspotify 90% 的代码量之后,我在 [Reddit](https://www.reddit.com/r/rust/comments/7xn9mh/my_first_crate_rspotify_spotify_api_wrapper/)
162167
上发贴,邀请社区的同学来 review code 以帮我完善代码。
@@ -175,7 +180,7 @@ pub fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> Result<T
175180

176181
虽说 Rust 也有Debugger-- gdb-rust. gdb 我以前写c 的时候用过,gdb 熟悉程度虽然谈 不上精通,但是也能熟练使用。但是用gdb-rust 调试并不是非常便利,比如在使用 [Rocket](https://rocket.rs/) 这个Web框架的时候,就很难使用gdb来调试Web程序。
177182

178-
虽说 [intellij-rust](https://intellij-rust.github.io/) 这个 Intellij Idea 的插件也支 持Debugger, 但是只有配合[Clion](https://www.jetbrains.com/clion/)才能使用。因为 只有 Clion 才能调用 gdb, 无奈。所以在开发 rspotify 的时候,我用得都是 `println!()`调试大法。
183+
虽说 [intellij-rust](https://intellij-rust.github.io/) 这个 Intellij Idea 的插件也支 持Debugger, 但是只有配合[Clion](https://www.jetbrains.com/clion/)才能使用。因为 只有 Clion 才能调用 gdb, 无奈。所以在开发 rspotify 的时候,我用得都是 =println!()=调试大法。
179184

180185

181186
### <span class="section-num">2.2</span> 编译器Bug {#编译器bug}

content/zh/post/2023/rspotify_一个用爱发电五年的开源项目.md

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
+++
22
title = "RSpotify: 一个用爱发电五年的开源项目"
3-
date = 2023-02-07T15:40:00+08:00
4-
lastmod = 2023-02-10T10:37:29+08:00
3+
date = 2023-02-07T15:40:00-08:00
4+
lastmod = 2024-12-30T22:28:48-08:00
55
tags = ["rspotify", "rust"]
66
categories = ["rspotify"]
77
draft = false
88
toc = true
9+
highlighted = true
910
+++
1011

1112
## <span class="section-num">1</span> 前言 {#前言}
@@ -44,13 +45,14 @@ toc = true
4445

4546
当时看到个网易云音乐命令行版本的播放器 [musicbox](https://github.com/darknessomi/musicbox), 当时我在用的是Spotify,就希望可以为Spotify写个类似的播放器。
4647

47-
{{< figure src="/ox-hugo/musicbox.gif" link="/ox-hugo/musicbox.gif" >}}
48-
4948
虽说Spotify API是对外开放,但直接使用HttpClient来请求HTTP API有点太祼,所以就希望使用先封装个library,方便后续的Rust应用直接调用,就不需要自己操心Http请求了。
5049

5150
这就是RSpotify这个库的来源。
5251

53-
这次,我就只在 [Reddit](https://www.reddit.com/r/rust/comments/7xn9mh/my_first_crate_rspotify_spotify_api_wrapper/)[博客](https://ramsayleung.github.io/post/2018/rspotify/) 上分享使用Rust来写library 的经历了。
52+
这次,我就只在 Reddit和博客 上分享使用Rust来写library 的经历了。
53+
54+
- Reddit: [My first crate-- rspotify, Spotify API wrapper implemented in Rust](https://www.reddit.com/r/rust/comments/7xn9mh/my_first_crate_rspotify_spotify_api_wrapper/)
55+
- 博客: [RSpotify– 我的第一个Rust crate](https://ramsayleung.github.io/zh/post/2018/rspotify/)
5456

5557

5658
## <span class="section-num">3</span> 演进 {#演进}
@@ -87,13 +89,13 @@ toc = true
8789

8890
自此之后,Rust社区在做的事情,就是把已有Rust代码疯狂升级到async await,RSpotify虽迟,但也赶上了这波潮流。
8991

90-
当时RSpotify 请求Spotify的API使用的HTTP库是 `reqwest`,在 `reqwest` 支持异步模式之后,开发者 [Alexander](https://github.com/Rigellute)就提了一个超大的[PR](https://github.com/ramsayleung/rspotify/pull/81),把所有已有的api全部修改成`async`, 我就乐见其成,就把这个PR合并了。
92+
当时RSpotify 请求Spotify的API使用的HTTP库是 `reqwest=,在 =reqwest` 支持异步模式之后,开发者 [Alexander](https://github.com/Rigellute)就提了一个超大的[PR](https://github.com/ramsayleung/rspotify/pull/81),把所有已有的api全部修改成=async=, 我就乐见其成,就把这个PR合并了。
9193

9294
{{< figure src="/ox-hugo/add_async_await_1.png" link="/ox-hugo/add_async_await_1.png" >}}
9395

9496
有社区的同学抱怨说异步模式的代码不好使用,他对性能没有什么要求,能否保留同步模式的接口调用。
9597

96-
后来为了兼顾同步模式和异步模式这两种调用方式,Alexander 又提了一个超大超大的[PR](https://github.com/ramsayleung/rspotify/pull/82/files),把现有的异步模式代码复制一份,然后把`async` 关键字去掉。
98+
后来为了兼顾同步模式和异步模式这两种调用方式,Alexander 又提了一个超大超大的[PR](https://github.com/ramsayleung/rspotify/pull/82/files),把现有的异步模式代码复制一份,然后把=async= 关键字去掉。
9799

98100
{{< figure src="/ox-hugo/add_async_await_2.png" link="/ox-hugo/add_async_await_2.png" >}}
99101

@@ -115,24 +117,24 @@ toc = true
115117
// 异步代码
116118
async fn original() -> Result<String, reqwest::Error> {
117119
reqwest::get("https://www.rust-lang.org")
118-
.await?
119-
.text()
120-
.await
120+
.await?
121+
.text()
122+
.await
121123
}
122124

123125
lazy_static! {
124126
// Mutex to have mutable access and Arc so that it's thread-safe.
125127
static ref RT: Arc<Mutex<runtime::Runtime>> = Arc::new(Mutex::new(runtime::Builder::new()
126-
.basic_scheduler()
127-
.enable_all()
128-
.build()
129-
.unwrap()));
128+
.basic_scheduler()
129+
.enable_all()
130+
.build()
131+
.unwrap()));
130132
}
131133

132134
// 同步版本代码
133135
fn with_block_on() -> Result<String, reqwest::Error> {
134136
RT.lock().unwrap().block_on(async move {
135-
original().await
137+
original().await
136138
})
137139
}
138140
```
@@ -142,7 +144,7 @@ fn with_block_on() -> Result<String, reqwest::Error> {
142144
但实际上,发现编写[macro太复杂](https://github.com/ramsayleung/rspotify/pull/120),并且这种方案不够灵活,实现起来也相当复杂。
143145

144146
然后Mario 又调研出一种[新方案](https://github.com/ramsayleung/rspotify/pull/129),通过 [`maybe_async`](https://github.com/fMeow/maybe-async-rs) 这个库在同步和异步模式之间切换。
145-
默认是异步模式,但可以通过 `features = ["is_sync"]` 编译选项来切换到同步模式,`maybe_async` 就会把所有的`async/await` 关键字给去掉。
147+
默认是异步模式,但可以通过 `features = ["is_sync"]` 编译选项来切换到同步模式,=maybe_async= 就会把所有的=async/await= 关键字给去掉。
146148

147149
这个方案简单,可读性高,易于扩展,也不需要维护复杂的 macro 代码。
148150

@@ -158,7 +160,7 @@ fn with_block_on() -> Result<String, reqwest::Error> {
158160

159161
{{< figure src="/ox-hugo/meta-issue.png" link="/ox-hugo/meta-issue.png" >}}
160162

161-
这么多的优化建议,可以看出Kestrer真的花了很多时间来阅读和改善RSpotify的代码,盛情难却(可见原来的代码是~~多烂~~, 有非常多激发社区同学参与改进的空间).
163+
这么多的优化建议,可以看出Kestrer真的花了很多时间来阅读和改善RSpotify的代码,盛情难却(可见原来的代码是+多烂+, 有非常多激发社区同学参与改进的空间).
162164

163165
别人指出问题,就要好好优化。
164166

@@ -233,7 +235,7 @@ fn with_block_on() -> Result<String, reqwest::Error> {
233235
想到的点:
234236

235237
1. 使用Rust的macro来减少copy-paste的代码(但复杂的 macro,基本不具备可读性。)
236-
2. 使用serde 自定义序列化函数;
238+
2. 使用 serde 自定义序列化函数;
237239
3. 以workspace 模式管理多个crates;
238240
4. 编写 async/await 的异步代码;
239241
5. 使用标准库的Trait, 风格契合标准库;
@@ -269,8 +271,6 @@ fn with_block_on() -> Result<String, reqwest::Error> {
269271
2. 我们做了个好东西,我们要抢占市场。我们就开源,搞人海战术,让竞品淹没在人民群众的汪洋大海中,让我们的东西成为事实的标准。(Android,Chromium, Kubernetes, Vscode)
270272
3. 就想开源让你们见识下大佬是怎么样子的。
271273

272-
个人理解,****开源是「手段」,而非「目的」****
273-
274274
对于商业公司而言,如果没有收益,为什么要把花钱雇的人写的内部组件开源出来呢?总不成是为了在B站上博取小朋友的称赞吧。
275275

276276
而公司内部的组件,往往是与业务共生,高度适配的,藕断丝连,没有那么容易开源的。
@@ -288,7 +288,7 @@ fn with_block_on() -> Result<String, reqwest::Error> {
288288

289289
### <span class="section-num">4.4</span> 些许成果 {#些许成果}
290290

291-
在Github上,收获了495个star,被1108个仓库及18个package 所依赖,而其中Alexander的 [spotify-tui](https://github.com/Rigellute/spotify-tui) 就是我期望做的终端版本的Spotify。
291+
在Github上,被1108个仓库及18个package 所依赖,而其中Alexander的 [spotify-tui](https://github.com/Rigellute/spotify-tui) 就是我期望做的终端版本的Spotify。
292292

293293
开源的好处就是,在开发好基础设施之后,自然就会有其他有相同想法的同学,把应用开发出来。
294294

@@ -298,7 +298,7 @@ crates.io 的统计,总计被下载23w次,当然包括很多CI的重复下
298298

299299
{{< figure src="/ox-hugo/stats.png" link="/ox-hugo/stats.png" >}}
300300

301-
对于有Rust,Spotify,Library等诸多定语的RSpotify来说,目标受众本来就不多,能有现在这样的用户量已远超我最初了预期了
301+
对于有Rust,Spotify,Library等诸多定语的RSpotify来说,目标受众本来就不多,能有现在这样的用户量也算是个挺不错的成果了
302302

303303

304304
## <span class="section-num">5</span> 总结 {#总结}
@@ -307,13 +307,10 @@ crates.io 的统计,总计被下载23w次,当然包括很多CI的重复下
307307

308308
天上的云,飘来又飘走;开源的项目,挖坑又弃坑。
309309

310-
视线望不到下一个五年,唯有且行且看。
310+
视线望不穿下一个五年,唯有且行且看。
311311

312312

313313
## <span class="section-num">6</span> 参考 {#参考}
314314

315315
- [Spotify is first music streaming service to surpass 200M paid subscribers](https://www.theverge.com/2023/1/31/23577499/spotify-q4-2022-earnings-release-subscriber-growth-layoffs)
316316
- [RSpotify](https://github.com/ramsayleung/rspotify)
317-
- [The lesson learned from refactoring rspotify](https://ramsayleung.github.io/post/2020/serde_lesson/)
318-
- [Let's make everything iterable](https://ramsayleung.github.io/post/2021/iterate_through_pagination_api/)
319-
- [spotify-tui](https://github.com/Rigellute/spotify-tui)

0 commit comments

Comments
 (0)