1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
use crate::common::Thumbnail; //XXX: Move this to parse?
use crate::common::{AlbumType, Explicit};
use crate::crawler::{JsonCrawler, JsonCrawlerBorrowed};
use crate::nav_consts::*;
use crate::query::*;
use crate::{Error, Result};
use const_format::concatcp;
use super::{parse_playlist_items, ProcessedResult, SongResult};
#[derive(Debug)]
pub enum AlbumLikeStatus {
Like,
Indifferent,
}
#[derive(Debug)]
pub struct AlbumParamsOtherVersion {
pub title: String,
pub year: String,
pub browse_id: String,
pub thumbnails: Vec<Thumbnail>,
pub is_explicit: Explicit,
}
// Is this similar to another struct?
// XXX: Consider correct privacy
#[derive(Debug)]
pub struct AlbumParams {
pub title: String,
pub category: AlbumType,
pub thumbnails: Vec<Thumbnail>,
pub description: Option<String>,
pub artists: Option<String>, // Should be super::ParsedSongArtist<'a>, // Basic Artists
pub year: String,
pub track_count: Option<u64>,
pub duration: String,
pub audio_playlist_id: Option<String>,
// TODO: better interface
pub tracks: Vec<SongResult>,
//consider moving this struct up to super.
pub other_versions: Option<Vec<AlbumParamsOtherVersion>>,
pub like_status: Option<AlbumLikeStatus>,
}
pub(crate) struct MusicShelfContents<'a> {
pub json: JsonCrawlerBorrowed<'a>,
}
impl<'a, 'b> MusicShelfContents<'a> {
pub fn from_crawler(crawler: JsonCrawlerBorrowed<'a>) -> Self {
Self { json: crawler }
}
}
fn take_music_shelf_contents(nav: &mut JsonCrawler) -> Result<MusicShelfContents> {
let json = nav.borrow_pointer(concatcp!(
SINGLE_COLUMN_TAB,
SECTION_LIST_ITEM,
MUSIC_SHELF,
"/contents"
))?;
Ok(MusicShelfContents { json })
}
impl<'a> ProcessedResult<GetAlbumQuery<'a>> {
pub fn parse(self) -> Result<AlbumParams> {
// Due to limitation of the borrow checker, we can't simply pass a reference
// to ok_or_else. So instead, we'll keep a clone handy in case of error.
// The advantage of this approach is that the entire json
// for the function is stored.
// XXX: Consider adding code here so this only runs in debug mode.
// TODO: Implement pointer trace so that we can see exactly where error occurs.
// TODO: Allow error composition - so that an error in the parsing function
// also reports enter json_debug file.
let ProcessedResult {
mut json_crawler, ..
} = self;
// TODO parse_song_runs - returns id, views and a few others.
// Other verisions = parse_content_list.
// Fill in Tracks album title and artist (not sure if needed).
let mut header = json_crawler.borrow_pointer(HEADER_DETAIL)?;
// If this fails, try TryInto.
// Type annotation is required because I use title before its used as a struct field.
let title: String = header.take_value_pointer(TITLE_TEXT)?;
// I am not sure why the error here is OK but I'll take it!
let category = AlbumType::try_from_str(
header
.take_value_pointer::<String, &str>(SUBTITLE)?
.as_str(),
)?;
let description = header.take_value_pointer("/description/runs/0/text").ok();
let thumbnails: Vec<Thumbnail> = header.take_value_pointer(THUMBNAIL_CROPPED)?;
// If NAVIGATION_WATCH_PLAYLIST ID, then return that, else try NAVIGATION_PLAYLIST_ID else
// None.
// Seems a bit of a hacky way to do this.
// XXX: This is an issue! Clone inserted to make compile.
// TODO: Remove allocation.
// If we clone in this way, we won't have the parent json or path.
let mut top_level = header.borrow_pointer(concatcp!(MENU, "/topLevelButtons"))?;
let audio_playlist_id = if let Ok(value) = top_level
.take_value_pointer(concatcp!("/0/buttonRenderer", NAVIGATION_WATCH_PLAYLIST_ID))
{
Some(value)
} else {
top_level
.take_value_pointer(concatcp!("/0/buttonRenderer", NAVIGATION_PLAYLIST_ID))
.ok()
};
// TODO: Error instead of panic
// TODO: Improve this
let like_status = top_level
.take_value_pointer("/1/buttonRenderer/defaultServiceEndpoint/likeEndpoint/status")
.map(|likestatus| match likestatus {
1 => AlbumLikeStatus::Like,
2 => AlbumLikeStatus::Indifferent,
_ => unreachable!("likestatus should only be 1 or 2"),
})
.ok();
// Original python code:
// if len(header['secondSubtitle']['runs']) > 1:
// album['trackCount'] = to_int(header['secondSubtitle']['runs'][0]['text'])
// album['duration'] = header['secondSubtitle']['runs'][2]['text']
// else:
// album['duration'] = header['secondSubtitle']['runs'][0]['text']
// Below avoid mutable variables but looks messy & appears to be inefficient.
// Do I actually need this, when it can be calculated?
// Should be a better way to do this - potentially if-let.
// XXX: May be able to remove additional OKs for these.
let track_count = header
.borrow_pointer("/secondSubtitle/runs")
.ok()
.and_then(|s| s.into_array_iter_mut().ok())
.and_then(|mut a| {
if a.len() > 1 {
a.nth(0)
.and_then(|mut v| v.take_value_pointer("/text").ok())
} else {
None
}
});
let duration = header
.borrow_pointer("/secondSubtitle/runs")
.ok()
.and_then(|s| s.into_array_iter_mut().ok())
.and_then(|mut a| {
if a.len() > 1 {
a.nth(2)
.and_then(|mut v| v.take_value_pointer("/text").ok())
} else {
a.nth(0)
.and_then(|mut v| v.take_value_pointer("/text").ok())
}
})
.ok_or_else(|| Error::other("Basic error on duration"))?;
let mut year = String::new();
// Pretty hacky way to handle this, as the runs are quite free text.
// TODO: Add a regex crate.
for mut a in header
.navigate_pointer("/subtitle/runs")
.and_then(|s| s.into_array_iter_mut())
.into_iter()
.flatten()
.skip(2)
.step_by(2)
{
let value: Result<String> = a.take_value_pointer("/text");
if let Ok(4) = value.as_ref().map(|v| v.len()) {
year = value.unwrap();
}
}
let _results_other_versions = json_crawler.borrow_pointer(concatcp!(
SINGLE_COLUMN_TAB,
SECTION_LIST,
"/0",
MUSIC_SHELF
)); //this can be none.
let music_shelf = take_music_shelf_contents(&mut json_crawler)?;
let tracks = parse_playlist_items(music_shelf)?;
//let mut tracks = super::artist::parse_playlist_items(results_tracks.take())?;
// Tracks themselves don't know who the album artist is. But it can be handy for other
// parts of the application to know the artist.
// This may not be the ideal approach due to the allocation requirement but nevertheless we
// are using it for now.
// TODO: Consider alternative approach in the app design.
//for track in tracks.iter_mut() {
// track.album = Some(super::ParsedSongAlbum {
// id: audio_playlist_id.clone(),
// name: Some(title.clone()),
// });
//}
// album = parse_album_header(response)
// results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM + MUSIC_SHELF)
// album['tracks'] = parse_playlist_items(results['contents'])
// results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST + [1] + CAROUSEL, True)
// if results is not None:
// album['other_versions'] = parse_content_list(results['contents'], parse_album)
// album['duration_seconds'] = sum_total_duration(album)
// for i, track in enumerate(album['tracks']):
// album['tracks'][i]['album'] = album['title']
// album['tracks'][i]['artists'] = album['artists']
//
// return album
Ok(AlbumParams {
like_status,
title,
description,
thumbnails,
duration,
category,
track_count,
audio_playlist_id,
other_versions: None,
year,
tracks,
artists: None,
})
}
}