@cascateer/sterio 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +0 -0
- package/.github/workflows/publish.yml +11 -0
- package/.vscode/settings.json +4 -0
- package/build/routes.ts +413 -0
- package/build/swagger.json +775 -0
- package/openapitools.json +14 -0
- package/package.json +56 -0
- package/src/Spotify.service.ts +202 -0
- package/src/Sterio.service.ts +208 -0
- package/src/Youtube.service.ts +112 -0
- package/src/YoutubeMusic.service.ts +52 -0
- package/src/api/.openapi-generator/FILES +29 -0
- package/src/api/.openapi-generator/VERSION +1 -0
- package/src/api/.openapi-generator-ignore +23 -0
- package/src/api/apis/DefaultApi.ts +154 -0
- package/src/api/apis/index.ts +1 -0
- package/src/api/index.ts +4 -0
- package/src/api/models/GetYoutubeMusicAlbums200ResponseInner.ts +29 -0
- package/src/api/models/PartialSterioAlbumResourcesFull.ts +39 -0
- package/src/api/models/SpotifyApiAlbumObjectSimplified.ts +152 -0
- package/src/api/models/SpotifyApiArtistObjectSimplified.ts +68 -0
- package/src/api/models/SpotifyApiExternalUrlObject.ts +25 -0
- package/src/api/models/SpotifyApiImageObject.ts +38 -0
- package/src/api/models/SpotifyApiRestrictionsObject.ts +24 -0
- package/src/api/models/SterioAlbum.ts +39 -0
- package/src/api/models/SterioAlbumResource.ts +29 -0
- package/src/api/models/SterioAlbumResourcesTable.ts +34 -0
- package/src/api/models/SterioAlbumSongs.ts +38 -0
- package/src/api/models/SterioAlbumSongsVideoIdsInner.ts +34 -0
- package/src/api/models/YoutubePlaylist.ts +38 -0
- package/src/api/models/YoutubeV3Schema36PlaylistItem.ts +59 -0
- package/src/api/models/YoutubeV3Schema36PlaylistItemContentDetails.ts +49 -0
- package/src/api/models/YoutubeV3Schema36PlaylistItemSnippet.ts +89 -0
- package/src/api/models/YoutubeV3Schema36PlaylistItemStatus.ts +26 -0
- package/src/api/models/YoutubeV3Schema36ResourceId.ts +44 -0
- package/src/api/models/YoutubeV3Schema36Thumbnail.ts +38 -0
- package/src/api/models/YoutubeV3Schema36ThumbnailDetails.ts +49 -0
- package/src/api/models/index.ts +20 -0
- package/src/api/runtime.ts +193 -0
- package/src/api/servers.ts +45 -0
- package/src/api/tsconfig.json +22 -0
- package/src/app.ts +58 -0
- package/src/config.ts +6 -0
- package/src/controller.ts +148 -0
- package/src/lib.ts +112 -0
- package/src/main.ts +225 -0
- package/src/tables.ts +178 -0
- package/src/types.ts +113 -0
- package/src/views/spotify/auth-callback.ejs +22 -0
- package/tables/document-files/82745273-7eda-4169-8b4a-998d2de00ac2.json +5208 -0
- package/tables/spotify-albums/5bff8eba-bd2b-4485-8136-1f89da5db6d7.json +1 -0
- package/tables/sterio-albums/81d5b327-68b1-4fef-8e40-491c792e8951.json +1 -0
- package/tables/stream-files/f6697f37-7320-41df-aa50-b70264fa1bbf.json +3788 -0
- package/tables/youtube-music-albums/55fd9bb8-46ba-4d0e-9376-f2f2cbb1aa38.json +1 -0
- package/tables/youtube-playlists/e3153ab8-8e1f-491e-995e-3142d07f4ecc.json +1 -0
- package/tables/youtube-videos/9067172e-4803-48f1-8b8e-4b942f3ca228.json +1 -0
- package/tsconfig.json +16 -0
- package/tsoa.json +14 -0
package/src/tables.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFileTable,
|
|
3
|
+
createTable,
|
|
4
|
+
defaults,
|
|
5
|
+
File,
|
|
6
|
+
} from "@cascateer/database";
|
|
7
|
+
import { nonNullable, nonNullableAsync } from "@cascateer/lib";
|
|
8
|
+
import { LazyPromise } from "@cascateer/lib/promise";
|
|
9
|
+
import { writeFile } from "fs/promises";
|
|
10
|
+
import { compact, truncate, uniq } from "lodash";
|
|
11
|
+
import { extension } from "mime-types";
|
|
12
|
+
import objectHash from "object-hash";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
import { helpers, YtDlp } from "ytdlp-nodejs";
|
|
15
|
+
import { tapPromise } from "./lib";
|
|
16
|
+
import { SpotifyService } from "./Spotify.service";
|
|
17
|
+
import {
|
|
18
|
+
SpotifyAlbum,
|
|
19
|
+
SterioAlbum,
|
|
20
|
+
YoutubeMusicAlbum,
|
|
21
|
+
YoutubePlaylist,
|
|
22
|
+
YoutubeVideo,
|
|
23
|
+
} from "./types";
|
|
24
|
+
import { YoutubeService } from "./Youtube.service";
|
|
25
|
+
import { YoutubeMusicService } from "./YoutubeMusic.service";
|
|
26
|
+
|
|
27
|
+
defaults.FILE_BASE_URL = resolve(__dirname, "../../..", "files");
|
|
28
|
+
|
|
29
|
+
const SpotifyAlbumTable = createTable<SpotifyAlbum, "id">(
|
|
30
|
+
"spotify-albums",
|
|
31
|
+
"id",
|
|
32
|
+
(ids) => new SpotifyService().getAlbums(ids),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const SterioAlbumTable = createTable<SterioAlbum, "id">(
|
|
36
|
+
"sterio-albums",
|
|
37
|
+
"id",
|
|
38
|
+
(ids) => ids.map((id) => ({ id })),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const YoutubePlaylistTable = createTable<YoutubePlaylist, "id">(
|
|
42
|
+
"youtube-playlists",
|
|
43
|
+
"id",
|
|
44
|
+
(ids) =>
|
|
45
|
+
LazyPromise.concatAll(
|
|
46
|
+
ids.map(
|
|
47
|
+
(id) => new LazyPromise(() => new YoutubeService().getPlaylist(id)),
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const YoutubeVideoTable = createTable<YoutubeVideo, "id">(
|
|
53
|
+
"youtube-videos",
|
|
54
|
+
"id",
|
|
55
|
+
(ids) => new YoutubeService().getVideos(compact(ids)),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const YoutubeMusicAlbumTable = createTable<YoutubeMusicAlbum, "albumId">(
|
|
59
|
+
"youtube-music-albums",
|
|
60
|
+
"albumId",
|
|
61
|
+
(albumIds) =>
|
|
62
|
+
Promise.all(
|
|
63
|
+
uniq(albumIds).map((albumId) =>
|
|
64
|
+
new YoutubeMusicService().getAlbum(albumId),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const DocumentFileTable = createFileTable("document-files", (urls, spinner) =>
|
|
70
|
+
LazyPromise.concatAll(
|
|
71
|
+
uniq(urls).map(
|
|
72
|
+
(url) =>
|
|
73
|
+
new LazyPromise(() =>
|
|
74
|
+
tapPromise(
|
|
75
|
+
fetch(url).then((response) =>
|
|
76
|
+
response.arrayBuffer().then((buffer) =>
|
|
77
|
+
Promise.resolve(
|
|
78
|
+
new File(
|
|
79
|
+
[
|
|
80
|
+
objectHash(url),
|
|
81
|
+
extension(
|
|
82
|
+
nonNullable(response.headers.get("content-type")),
|
|
83
|
+
),
|
|
84
|
+
].join("."),
|
|
85
|
+
),
|
|
86
|
+
).then((file) =>
|
|
87
|
+
writeFile(
|
|
88
|
+
file.path,
|
|
89
|
+
Buffer.from(buffer).toString("base64"),
|
|
90
|
+
"base64",
|
|
91
|
+
)
|
|
92
|
+
.then(() => file.hash())
|
|
93
|
+
.then((checksum) => ({
|
|
94
|
+
url,
|
|
95
|
+
name: file.name,
|
|
96
|
+
checksum,
|
|
97
|
+
})),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
{
|
|
102
|
+
start: () => {
|
|
103
|
+
if (spinner != null) {
|
|
104
|
+
spinner.text = truncate(`Loading document from ${url}.`, {
|
|
105
|
+
length: 50,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
finish: ({ name }) => {
|
|
110
|
+
if (spinner != null) {
|
|
111
|
+
spinner.text = `Document written to ${new File(name)}.`;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const StreamFileTable = createFileTable("stream-files", (urls, spinner) =>
|
|
122
|
+
LazyPromise.concatAll(
|
|
123
|
+
uniq(urls).map(
|
|
124
|
+
(url) =>
|
|
125
|
+
new LazyPromise(() =>
|
|
126
|
+
tapPromise(
|
|
127
|
+
helpers.downloadFFmpeg().then(() =>
|
|
128
|
+
new YtDlp()
|
|
129
|
+
.download(url, {
|
|
130
|
+
format: {
|
|
131
|
+
filter: "audioonly",
|
|
132
|
+
type: "mp3",
|
|
133
|
+
},
|
|
134
|
+
output: resolve(File.BASE_URL, "%(id)s.%(ext)s"),
|
|
135
|
+
})
|
|
136
|
+
.on("progress", ({ percentage }) => {
|
|
137
|
+
if (spinner != null && percentage != null) {
|
|
138
|
+
spinner.text = `Streaming from ${url} (${percentage.toFixed()}% completed).`;
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.then(({ filePaths: [path] }) =>
|
|
142
|
+
nonNullableAsync(path).then(File.fromPath),
|
|
143
|
+
)
|
|
144
|
+
.then((file) =>
|
|
145
|
+
file.hash().then((checksum) => ({
|
|
146
|
+
url,
|
|
147
|
+
name: file.name,
|
|
148
|
+
checksum,
|
|
149
|
+
})),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
{
|
|
153
|
+
start: () => {
|
|
154
|
+
if (spinner != null) {
|
|
155
|
+
spinner.text = `Streaming from ${url}.`;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
finish: ({ name }) => {
|
|
159
|
+
if (spinner != null) {
|
|
160
|
+
spinner.text = `Stream written to ${new File(name)}.`;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
export {
|
|
171
|
+
DocumentFileTable,
|
|
172
|
+
SpotifyAlbumTable,
|
|
173
|
+
SterioAlbumTable,
|
|
174
|
+
StreamFileTable,
|
|
175
|
+
YoutubeMusicAlbumTable,
|
|
176
|
+
YoutubePlaylistTable,
|
|
177
|
+
YoutubeVideoTable,
|
|
178
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { youtube_v3 } from "googleapis";
|
|
2
|
+
import { Tags } from "node-id3";
|
|
3
|
+
import SpotifyWebApi from "spotify-web-api-node";
|
|
4
|
+
import { AlbumFull } from "ytmusic-api";
|
|
5
|
+
|
|
6
|
+
export type SpotifyGrant = Partial<
|
|
7
|
+
Awaited<ReturnType<SpotifyWebApi["authorizationCodeGrant"]>>["body"]
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export type SpotifyAlbum = SpotifyApi.AlbumObjectFull;
|
|
11
|
+
|
|
12
|
+
export class SterioDuration {
|
|
13
|
+
constructor(
|
|
14
|
+
public minutes: number,
|
|
15
|
+
public seconds: number,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
get totalSeconds(): number {
|
|
19
|
+
return this.minutes * 60 + this.seconds;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get totalMilliseconds(): number {
|
|
23
|
+
return this.totalSeconds * 1e3;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toISOString() {
|
|
27
|
+
return [this.minutes, this.seconds]
|
|
28
|
+
.map((t) => t.toString().padStart(2, "0"))
|
|
29
|
+
.join(":");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static fromPeriodOfTime(duration?: string | null) {
|
|
33
|
+
const { minutes, seconds } =
|
|
34
|
+
duration?.match(/^PT(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?$/)
|
|
35
|
+
?.groups ?? {};
|
|
36
|
+
|
|
37
|
+
if (minutes != null || seconds != null) {
|
|
38
|
+
return new SterioDuration(+(minutes ?? 0), +(seconds ?? 0));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SterioAlbumResource {
|
|
44
|
+
id: string;
|
|
45
|
+
iteratee?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SterioAlbumResourcesFull extends Record<
|
|
49
|
+
"youtubeMusic" | "youtube" | "spotify",
|
|
50
|
+
SterioAlbumResource
|
|
51
|
+
> {}
|
|
52
|
+
|
|
53
|
+
export type SterioAlbumResources = Partial<SterioAlbumResourcesFull>;
|
|
54
|
+
|
|
55
|
+
export interface SterioAlbumResourcesTable extends Record<
|
|
56
|
+
keyof SterioAlbumResourcesFull,
|
|
57
|
+
string[]
|
|
58
|
+
> {}
|
|
59
|
+
|
|
60
|
+
export interface SterioAlbum {
|
|
61
|
+
id: string;
|
|
62
|
+
resources?: SterioAlbumResources;
|
|
63
|
+
songs?: {
|
|
64
|
+
videoIds?: {
|
|
65
|
+
index: number;
|
|
66
|
+
expectedValue: string;
|
|
67
|
+
newValue: string;
|
|
68
|
+
}[];
|
|
69
|
+
include?: number[];
|
|
70
|
+
exclude?: number[];
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SterioAlbumFull
|
|
75
|
+
extends
|
|
76
|
+
Omit<SterioAlbum, "resources" | "songs">,
|
|
77
|
+
Omit<YoutubeMusicAlbum, "albumId" | "songs"> {
|
|
78
|
+
songs: SterioSongFull[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SterioSongFull {
|
|
82
|
+
videoId: string;
|
|
83
|
+
patched?: boolean;
|
|
84
|
+
excluded?: boolean;
|
|
85
|
+
channel?: {
|
|
86
|
+
id?: string;
|
|
87
|
+
title?: string;
|
|
88
|
+
};
|
|
89
|
+
artwork: Partial<
|
|
90
|
+
Record<
|
|
91
|
+
"album" | "song",
|
|
92
|
+
{
|
|
93
|
+
path: string;
|
|
94
|
+
checksum: string;
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
>;
|
|
98
|
+
duration?: SterioDuration;
|
|
99
|
+
tags: Tags;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type YoutubeMusicAlbum = Pick<
|
|
103
|
+
AlbumFull,
|
|
104
|
+
"albumId" | "artist" | "name" | "songs" | "thumbnails" | "year"
|
|
105
|
+
>;
|
|
106
|
+
|
|
107
|
+
export interface YoutubePlaylist {
|
|
108
|
+
id: string;
|
|
109
|
+
title?: string;
|
|
110
|
+
items: youtube_v3.Schema$PlaylistItem[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type YoutubeVideo = youtube_v3.Schema$Video;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Page Title</title>
|
|
8
|
+
<link rel="stylesheet" href="main.css" />
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<% if (code) { %>
|
|
13
|
+
Received grant code
|
|
14
|
+
<pre><%= code %></pre>
|
|
15
|
+
<% } else { %>
|
|
16
|
+
Could not obtain grant code.
|
|
17
|
+
<pre>:-(</pre>
|
|
18
|
+
<% } %>
|
|
19
|
+
You can now close this page.
|
|
20
|
+
</body>
|
|
21
|
+
|
|
22
|
+
</html>
|