@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.
Files changed (58) hide show
  1. package/.env +0 -0
  2. package/.github/workflows/publish.yml +11 -0
  3. package/.vscode/settings.json +4 -0
  4. package/build/routes.ts +413 -0
  5. package/build/swagger.json +775 -0
  6. package/openapitools.json +14 -0
  7. package/package.json +56 -0
  8. package/src/Spotify.service.ts +202 -0
  9. package/src/Sterio.service.ts +208 -0
  10. package/src/Youtube.service.ts +112 -0
  11. package/src/YoutubeMusic.service.ts +52 -0
  12. package/src/api/.openapi-generator/FILES +29 -0
  13. package/src/api/.openapi-generator/VERSION +1 -0
  14. package/src/api/.openapi-generator-ignore +23 -0
  15. package/src/api/apis/DefaultApi.ts +154 -0
  16. package/src/api/apis/index.ts +1 -0
  17. package/src/api/index.ts +4 -0
  18. package/src/api/models/GetYoutubeMusicAlbums200ResponseInner.ts +29 -0
  19. package/src/api/models/PartialSterioAlbumResourcesFull.ts +39 -0
  20. package/src/api/models/SpotifyApiAlbumObjectSimplified.ts +152 -0
  21. package/src/api/models/SpotifyApiArtistObjectSimplified.ts +68 -0
  22. package/src/api/models/SpotifyApiExternalUrlObject.ts +25 -0
  23. package/src/api/models/SpotifyApiImageObject.ts +38 -0
  24. package/src/api/models/SpotifyApiRestrictionsObject.ts +24 -0
  25. package/src/api/models/SterioAlbum.ts +39 -0
  26. package/src/api/models/SterioAlbumResource.ts +29 -0
  27. package/src/api/models/SterioAlbumResourcesTable.ts +34 -0
  28. package/src/api/models/SterioAlbumSongs.ts +38 -0
  29. package/src/api/models/SterioAlbumSongsVideoIdsInner.ts +34 -0
  30. package/src/api/models/YoutubePlaylist.ts +38 -0
  31. package/src/api/models/YoutubeV3Schema36PlaylistItem.ts +59 -0
  32. package/src/api/models/YoutubeV3Schema36PlaylistItemContentDetails.ts +49 -0
  33. package/src/api/models/YoutubeV3Schema36PlaylistItemSnippet.ts +89 -0
  34. package/src/api/models/YoutubeV3Schema36PlaylistItemStatus.ts +26 -0
  35. package/src/api/models/YoutubeV3Schema36ResourceId.ts +44 -0
  36. package/src/api/models/YoutubeV3Schema36Thumbnail.ts +38 -0
  37. package/src/api/models/YoutubeV3Schema36ThumbnailDetails.ts +49 -0
  38. package/src/api/models/index.ts +20 -0
  39. package/src/api/runtime.ts +193 -0
  40. package/src/api/servers.ts +45 -0
  41. package/src/api/tsconfig.json +22 -0
  42. package/src/app.ts +58 -0
  43. package/src/config.ts +6 -0
  44. package/src/controller.ts +148 -0
  45. package/src/lib.ts +112 -0
  46. package/src/main.ts +225 -0
  47. package/src/tables.ts +178 -0
  48. package/src/types.ts +113 -0
  49. package/src/views/spotify/auth-callback.ejs +22 -0
  50. package/tables/document-files/82745273-7eda-4169-8b4a-998d2de00ac2.json +5208 -0
  51. package/tables/spotify-albums/5bff8eba-bd2b-4485-8136-1f89da5db6d7.json +1 -0
  52. package/tables/sterio-albums/81d5b327-68b1-4fef-8e40-491c792e8951.json +1 -0
  53. package/tables/stream-files/f6697f37-7320-41df-aa50-b70264fa1bbf.json +3788 -0
  54. package/tables/youtube-music-albums/55fd9bb8-46ba-4d0e-9376-f2f2cbb1aa38.json +1 -0
  55. package/tables/youtube-playlists/e3153ab8-8e1f-491e-995e-3142d07f4ecc.json +1 -0
  56. package/tables/youtube-videos/9067172e-4803-48f1-8b8e-4b942f3ca228.json +1 -0
  57. package/tsconfig.json +16 -0
  58. 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>