ani-web 2.0.7

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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -0
  3. package/client/dist/assets/AnimeInfo-B88ZA3gl.js +1 -0
  4. package/client/dist/assets/AnimeInfo-R63luGTP.css +1 -0
  5. package/client/dist/assets/AnimeInfoPage-xGVarrXG.js +2 -0
  6. package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
  7. package/client/dist/assets/Button-lkEUHIDS.js +1 -0
  8. package/client/dist/assets/ErrorMessage-BVWNgHMx.css +1 -0
  9. package/client/dist/assets/ErrorMessage-BXKDLzn8.js +1 -0
  10. package/client/dist/assets/Home-O1FbN8t_.js +1 -0
  11. package/client/dist/assets/Home-r0eYbWNc.css +1 -0
  12. package/client/dist/assets/Insights-CF4K-oO5.css +1 -0
  13. package/client/dist/assets/Insights-opcB-aTo.js +1 -0
  14. package/client/dist/assets/MAL-BGM33N5c.js +1 -0
  15. package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
  16. package/client/dist/assets/Player-BarbgKjI.css +1 -0
  17. package/client/dist/assets/Player-CSyGax33.js +9 -0
  18. package/client/dist/assets/PlayerSettings-BaCVQyw6.js +1 -0
  19. package/client/dist/assets/PlayerSettings-l6aLKrHh.css +1 -0
  20. package/client/dist/assets/QueueRail-Cxn5U8kE.js +1 -0
  21. package/client/dist/assets/QueueRail-DOM_pWkE.css +1 -0
  22. package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
  23. package/client/dist/assets/RemoveConfirmationModal-DatCZQKq.js +1 -0
  24. package/client/dist/assets/Search-BzO-aRP7.css +1 -0
  25. package/client/dist/assets/Search-DJxo3BYH.js +1 -0
  26. package/client/dist/assets/SearchableSelect-BkCrf6E8.css +1 -0
  27. package/client/dist/assets/SearchableSelect-BzYsMz8B.js +1 -0
  28. package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
  29. package/client/dist/assets/Settings-C5adinOe.js +1 -0
  30. package/client/dist/assets/SynopsisText-C3AK-aRc.js +1 -0
  31. package/client/dist/assets/SynopsisText-DsI3mW5v.css +1 -0
  32. package/client/dist/assets/ToggleSwitch-BIlQxIjg.css +1 -0
  33. package/client/dist/assets/ToggleSwitch-CrXim14A.js +1 -0
  34. package/client/dist/assets/Watchlist-CXw0vbNx.js +1 -0
  35. package/client/dist/assets/Watchlist-a2RHQogs.css +1 -0
  36. package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
  37. package/client/dist/assets/index-BzX_xmnf.css +1 -0
  38. package/client/dist/assets/index-Ciivz6fh.js +178 -0
  39. package/client/dist/assets/useAnimeInfoData-Dqthchpa.js +1 -0
  40. package/client/dist/assets/useIsMobile-BviODivc.js +1 -0
  41. package/client/dist/assets/vendor-Bc4EraM_.js +3 -0
  42. package/client/dist/favicon.ico +0 -0
  43. package/client/dist/index.html +35 -0
  44. package/client/dist/logo.png +0 -0
  45. package/client/dist/placeholder.svg +4 -0
  46. package/client/dist/robots.txt +3 -0
  47. package/client/package.json +58 -0
  48. package/orchestrator.js +323 -0
  49. package/package.json +88 -0
  50. package/server/.env +1 -0
  51. package/server/dist/config.js +89 -0
  52. package/server/dist/constants.json +1359 -0
  53. package/server/dist/controllers/auth.controller.js +215 -0
  54. package/server/dist/controllers/data.controller.js +232 -0
  55. package/server/dist/controllers/insights.controller.js +200 -0
  56. package/server/dist/controllers/proxy.controller.js +353 -0
  57. package/server/dist/controllers/settings.controller.js +159 -0
  58. package/server/dist/controllers/watchlist.controller.js +749 -0
  59. package/server/dist/db.js +152 -0
  60. package/server/dist/discord-rpc.js +279 -0
  61. package/server/dist/github-sync.js +342 -0
  62. package/server/dist/google.js +310 -0
  63. package/server/dist/logger.js +21 -0
  64. package/server/dist/providers/123anime.provider.js +226 -0
  65. package/server/dist/providers/allanime.provider.js +736 -0
  66. package/server/dist/providers/animepahe.provider.js +457 -0
  67. package/server/dist/providers/animeya.provider.js +787 -0
  68. package/server/dist/providers/megaplay.provider.js +264 -0
  69. package/server/dist/providers/provider.interface.js +2 -0
  70. package/server/dist/rclone.js +126 -0
  71. package/server/dist/repositories/insights.repository.js +42 -0
  72. package/server/dist/repositories/notifications.repository.js +30 -0
  73. package/server/dist/repositories/queue.repository.js +38 -0
  74. package/server/dist/repositories/settings.repository.js +14 -0
  75. package/server/dist/repositories/shows-meta.repository.js +41 -0
  76. package/server/dist/repositories/watched-episodes.repository.js +67 -0
  77. package/server/dist/repositories/watchlist.repository.js +80 -0
  78. package/server/dist/routes/auth.routes.js +26 -0
  79. package/server/dist/routes/data.routes.js +42 -0
  80. package/server/dist/routes/insights.routes.js +12 -0
  81. package/server/dist/routes/proxy.routes.js +14 -0
  82. package/server/dist/routes/settings.routes.js +27 -0
  83. package/server/dist/routes/watchlist.routes.js +46 -0
  84. package/server/dist/server.js +229 -0
  85. package/server/dist/sync-config.js +28 -0
  86. package/server/dist/sync.js +427 -0
  87. package/server/dist/utils/db-utils.js +15 -0
  88. package/server/dist/utils/env.utils.js +79 -0
  89. package/server/dist/utils/machine-id.js +46 -0
  90. package/server/dist/utils/request-context.js +5 -0
  91. package/server/package.json +19 -0
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.githubSyncService = void 0;
40
+ const logger_1 = __importDefault(require("./logger"));
41
+ const db_utils_1 = require("./utils/db-utils");
42
+ const env_utils_1 = require("./utils/env.utils");
43
+ const config_1 = require("./config");
44
+ const log = logger_1.default.child({ module: 'GitHubSync' });
45
+ const REPO_NAME = 'aniweb-sync-data';
46
+ const DEFAULT_CLIENT_ID = 'Ov23liT1ZtPk7XtN9PZk';
47
+ const GITHUB_SCOPES = ['repo'];
48
+ const GITHUB_API_HEADERS = {
49
+ accept: 'application/vnd.github+json',
50
+ 'x-github-api-version': '2026-03-10',
51
+ };
52
+ const SYNC_TABLES = [
53
+ 'watchlist',
54
+ 'watched_episodes',
55
+ 'queue',
56
+ 'settings',
57
+ 'shows_meta',
58
+ 'sync_metadata',
59
+ 'dismissed_notifications',
60
+ 'discovered_notifications',
61
+ ];
62
+ const nativeImport = new Function('specifier', 'return import(specifier)');
63
+ function getGitHubClientId() {
64
+ return process.env.GITHUB_CLIENT_ID || DEFAULT_CLIENT_ID;
65
+ }
66
+ function getSyncFilename() {
67
+ return config_1.CONFIG.IS_DEV ? 'sync.dev.json' : 'sync.json';
68
+ }
69
+ async function loadOctokit(token) {
70
+ const { Octokit } = await nativeImport('@octokit/rest');
71
+ return new Octokit({
72
+ auth: token,
73
+ request: {
74
+ headers: GITHUB_API_HEADERS,
75
+ },
76
+ });
77
+ }
78
+ function getErrorStatus(error) {
79
+ if (typeof error === 'object' && error && 'status' in error) {
80
+ return Number(error.status);
81
+ }
82
+ return undefined;
83
+ }
84
+ function quoteIdentifier(identifier) {
85
+ return `"${identifier.replace(/"/g, '""')}"`;
86
+ }
87
+ function getRowsFromAll(db, sql) {
88
+ return (0, db_utils_1.dbAll)(db, sql);
89
+ }
90
+ function readVersion(payload) {
91
+ const versionRow = payload.tables.sync_metadata.find((row) => row.key === 'db_version');
92
+ const value = versionRow?.value;
93
+ return typeof value === 'number' ? value : Number(value || payload.version || 0);
94
+ }
95
+ function normalizePayload(input) {
96
+ if (!input || typeof input !== 'object' || !('tables' in input)) {
97
+ throw new Error('Invalid GitHub sync payload.');
98
+ }
99
+ const payload = input;
100
+ for (const table of SYNC_TABLES) {
101
+ if (!Array.isArray(payload.tables?.[table])) {
102
+ throw new Error(`Invalid GitHub sync payload: missing ${table}.`);
103
+ }
104
+ }
105
+ return payload;
106
+ }
107
+ class GitHubSyncService {
108
+ deviceState = { status: 'idle' };
109
+ devicePromise = null;
110
+ isAuthenticated() {
111
+ return !!process.env.GITHUB_TOKEN;
112
+ }
113
+ getDeviceState() {
114
+ return this.deviceState;
115
+ }
116
+ async startDeviceAuth(db, runSyncSequence) {
117
+ if (this.isAuthenticated()) {
118
+ const user = await this.getUserProfile();
119
+ if (user) {
120
+ this.deviceState = {
121
+ status: 'success',
122
+ user: user,
123
+ };
124
+ const { updateEnvFile } = await Promise.resolve().then(() => __importStar(require('./utils/env.utils')));
125
+ await updateEnvFile({ SYNC_PROVIDER: 'github' });
126
+ await runSyncSequence(db, 'github');
127
+ return this.deviceState;
128
+ }
129
+ else {
130
+ // Token exists but is invalid/expired, clear it
131
+ log.warn('Saved GitHub token is invalid or expired. Clearing for new auth.');
132
+ await this.logout();
133
+ }
134
+ }
135
+ if (this.deviceState.status === 'pending') {
136
+ return this.deviceState;
137
+ }
138
+ this.deviceState = { status: 'pending' };
139
+ let resolveVerification;
140
+ const verificationReady = new Promise((resolve) => {
141
+ resolveVerification = resolve;
142
+ });
143
+ this.devicePromise = this.runDeviceAuth(db, runSyncSequence, resolveVerification);
144
+ await verificationReady;
145
+ return this.deviceState;
146
+ }
147
+ async getUserProfile() {
148
+ if (!process.env.GITHUB_TOKEN)
149
+ return null;
150
+ try {
151
+ const octokit = await loadOctokit(process.env.GITHUB_TOKEN);
152
+ const { data } = await octokit.rest.users.getAuthenticated({
153
+ headers: GITHUB_API_HEADERS,
154
+ });
155
+ return {
156
+ login: data.login,
157
+ name: data.name,
158
+ avatarUrl: data.avatar_url,
159
+ };
160
+ }
161
+ catch (err) {
162
+ if (getErrorStatus(err) === 401) {
163
+ log.warn('GitHub token is invalid or expired. Logging out.');
164
+ await this.logout();
165
+ }
166
+ return null;
167
+ }
168
+ }
169
+ async logout() {
170
+ await (0, env_utils_1.updateEnvFile)({ GITHUB_TOKEN: '' });
171
+ delete process.env.GITHUB_TOKEN;
172
+ this.deviceState = { status: 'idle' };
173
+ }
174
+ async getRemoteVersion() {
175
+ const payload = await this.fetchSyncPayload();
176
+ return payload ? readVersion(payload) : 0;
177
+ }
178
+ async syncUp(db) {
179
+ const payload = await this.exportDatabase(db);
180
+ const octokit = await this.getOctokit();
181
+ const owner = await this.ensureRepo(octokit);
182
+ const existing = await this.getSyncFile(octokit, owner);
183
+ const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8').toString('base64');
184
+ await octokit.rest.repos.createOrUpdateFileContents({
185
+ owner,
186
+ repo: REPO_NAME,
187
+ path: getSyncFilename(),
188
+ message: `Sync ani-web data v${payload.version}`,
189
+ content,
190
+ sha: existing?.sha,
191
+ headers: GITHUB_API_HEADERS,
192
+ });
193
+ }
194
+ async syncDown(db) {
195
+ const payload = await this.fetchSyncPayload();
196
+ if (!payload) {
197
+ return 0;
198
+ }
199
+ this.importDatabase(db, payload);
200
+ return readVersion(payload);
201
+ }
202
+ async runDeviceAuth(db, runSyncSequence, resolveVerification) {
203
+ try {
204
+ const { createOAuthDeviceAuth } = await nativeImport('@octokit/auth-oauth-device');
205
+ const auth = createOAuthDeviceAuth({
206
+ clientId: getGitHubClientId(),
207
+ scopes: GITHUB_SCOPES,
208
+ onVerification: (verification) => {
209
+ this.deviceState = {
210
+ status: 'pending',
211
+ verification: {
212
+ user_code: verification.user_code,
213
+ verification_uri: verification.verification_uri,
214
+ expires_in: verification.expires_in,
215
+ interval: verification.interval,
216
+ },
217
+ };
218
+ resolveVerification();
219
+ },
220
+ });
221
+ const authentication = await auth({ type: 'oauth' });
222
+ await (0, env_utils_1.updateEnvFile)({ GITHUB_TOKEN: authentication.token, SYNC_PROVIDER: 'github' });
223
+ process.env.GITHUB_TOKEN = authentication.token;
224
+ const user = await this.getUserProfile();
225
+ this.deviceState = {
226
+ status: 'success',
227
+ user: user || undefined,
228
+ };
229
+ try {
230
+ await runSyncSequence(db, 'github');
231
+ }
232
+ catch (err) {
233
+ log.error({ err }, 'Post-GitHub-login sync failed');
234
+ }
235
+ }
236
+ catch (err) {
237
+ this.deviceState = {
238
+ status: 'error',
239
+ error: err instanceof Error ? err.message : 'GitHub device authentication failed.',
240
+ };
241
+ resolveVerification();
242
+ log.error({ err }, 'GitHub device authentication failed');
243
+ }
244
+ finally {
245
+ this.devicePromise = null;
246
+ }
247
+ }
248
+ async getOctokit() {
249
+ if (!process.env.GITHUB_TOKEN) {
250
+ throw new Error('GitHub token is not configured.');
251
+ }
252
+ return loadOctokit(process.env.GITHUB_TOKEN);
253
+ }
254
+ async ensureRepo(octokit) {
255
+ const { data: user } = await octokit.rest.users.getAuthenticated({
256
+ headers: GITHUB_API_HEADERS,
257
+ });
258
+ try {
259
+ await octokit.rest.repos.get({
260
+ owner: user.login,
261
+ repo: REPO_NAME,
262
+ headers: GITHUB_API_HEADERS,
263
+ });
264
+ }
265
+ catch (err) {
266
+ if (getErrorStatus(err) !== 404) {
267
+ throw err;
268
+ }
269
+ await octokit.rest.repos.createForAuthenticatedUser({
270
+ name: REPO_NAME,
271
+ private: true,
272
+ auto_init: true,
273
+ description: 'Private ani-web synchronization data.',
274
+ headers: GITHUB_API_HEADERS,
275
+ });
276
+ }
277
+ return user.login;
278
+ }
279
+ async getSyncFile(octokit, owner) {
280
+ try {
281
+ const response = await octokit.rest.repos.getContent({
282
+ owner,
283
+ repo: REPO_NAME,
284
+ path: getSyncFilename(),
285
+ headers: GITHUB_API_HEADERS,
286
+ });
287
+ const data = response.data;
288
+ if (data.type !== 'file' || !data.content || !data.sha) {
289
+ return null;
290
+ }
291
+ return {
292
+ content: Buffer.from(data.content, 'base64').toString('utf8'),
293
+ sha: data.sha,
294
+ };
295
+ }
296
+ catch (err) {
297
+ if (getErrorStatus(err) === 404) {
298
+ return null;
299
+ }
300
+ throw err;
301
+ }
302
+ }
303
+ async fetchSyncPayload() {
304
+ const octokit = await this.getOctokit();
305
+ const owner = await this.ensureRepo(octokit);
306
+ const file = await this.getSyncFile(octokit, owner);
307
+ if (!file) {
308
+ return null;
309
+ }
310
+ return normalizePayload(JSON.parse(file.content));
311
+ }
312
+ async exportDatabase(db) {
313
+ const tables = {};
314
+ for (const table of SYNC_TABLES) {
315
+ tables[table] = await getRowsFromAll(db, `SELECT * FROM ${quoteIdentifier(table)}`);
316
+ }
317
+ return {
318
+ version: readVersion({ version: 0, exportedAt: '', tables }),
319
+ exportedAt: new Date().toISOString(),
320
+ tables,
321
+ };
322
+ }
323
+ importDatabase(db, payload) {
324
+ db.serialize(() => {
325
+ for (const table of SYNC_TABLES) {
326
+ db.run(`DELETE FROM ${quoteIdentifier(table)}`);
327
+ }
328
+ for (const table of SYNC_TABLES) {
329
+ for (const row of payload.tables[table]) {
330
+ const columns = Object.keys(row);
331
+ if (columns.length === 0)
332
+ continue;
333
+ const columnSql = columns.map(quoteIdentifier).join(', ');
334
+ const placeholders = columns.map(() => '?').join(', ');
335
+ const values = columns.map((column) => row[column]);
336
+ db.run(`INSERT INTO ${quoteIdentifier(table)} (${columnSql}) VALUES (${placeholders})`, values);
337
+ }
338
+ }
339
+ });
340
+ }
341
+ }
342
+ exports.githubSyncService = new GitHubSyncService();
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.googleDriveService = exports.GoogleDriveService = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const https_1 = __importDefault(require("https"));
10
+ const axios_1 = __importDefault(require("axios"));
11
+ const promises_1 = require("stream/promises");
12
+ const logger_1 = __importDefault(require("./logger"));
13
+ const config_1 = require("./config");
14
+ const httpAgent = new http_1.default.Agent({ keepAlive: false });
15
+ const httpsAgent = new https_1.default.Agent({ keepAlive: false });
16
+ httpsAgent.setMaxListeners(100);
17
+ httpAgent.setMaxListeners(100);
18
+ const googleAxios = axios_1.default.create({
19
+ httpAgent,
20
+ httpsAgent,
21
+ timeout: 30000,
22
+ });
23
+ class GoogleDriveService {
24
+ tokens = {};
25
+ folderIdCache = new Map();
26
+ constructor() {
27
+ if (!config_1.CONFIG.GOOGLE_CLIENT_ID) {
28
+ logger_1.default.error('GOOGLE_CLIENT_ID is missing from .env!');
29
+ }
30
+ this.loadTokens();
31
+ }
32
+ loadTokens() {
33
+ if (fs_1.default.existsSync(config_1.CONFIG.TOKEN_PATH)) {
34
+ try {
35
+ this.tokens = JSON.parse(fs_1.default.readFileSync(config_1.CONFIG.TOKEN_PATH, 'utf-8'));
36
+ }
37
+ catch (error) {
38
+ logger_1.default.error({ err: error }, 'Failed to load Google tokens');
39
+ }
40
+ }
41
+ }
42
+ saveTokens(tokens) {
43
+ const merged = { ...this.tokens, ...tokens };
44
+ if (merged.expires_in && !merged.expiry_date) {
45
+ merged.expiry_date = Date.now() + merged.expires_in * 1000;
46
+ }
47
+ this.tokens = merged;
48
+ try {
49
+ fs_1.default.writeFileSync(config_1.CONFIG.TOKEN_PATH, JSON.stringify(this.tokens));
50
+ }
51
+ catch (error) {
52
+ logger_1.default.error({ err: error }, 'Failed to save refreshed tokens');
53
+ }
54
+ }
55
+ getGoogleClientConfig() {
56
+ if (!config_1.CONFIG.GOOGLE_CLIENT_ID || !config_1.CONFIG.GOOGLE_CLIENT_SECRET) {
57
+ throw new Error('Google OAuth credentials are not configured');
58
+ }
59
+ return {
60
+ clientId: config_1.CONFIG.GOOGLE_CLIENT_ID,
61
+ clientSecret: config_1.CONFIG.GOOGLE_CLIENT_SECRET,
62
+ };
63
+ }
64
+ async refreshAccessToken() {
65
+ if (!this.tokens.refresh_token) {
66
+ throw new Error('Missing refresh token');
67
+ }
68
+ const { clientId, clientSecret } = this.getGoogleClientConfig();
69
+ const params = new URLSearchParams({
70
+ client_id: clientId,
71
+ client_secret: clientSecret,
72
+ refresh_token: this.tokens.refresh_token,
73
+ grant_type: 'refresh_token',
74
+ });
75
+ try {
76
+ const { data } = await googleAxios.post('https://oauth2.googleapis.com/token', params.toString(), {
77
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
78
+ });
79
+ this.saveTokens(data);
80
+ }
81
+ catch (error) {
82
+ if (axios_1.default.isAxiosError(error) &&
83
+ (error.response?.status === 400 || error.response?.status === 401)) {
84
+ logger_1.default.warn('Failed to refresh Google access token. Token may be revoked. Logging out.');
85
+ await this.logout();
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ async ensureAccessToken() {
91
+ const expiresSoon = !this.tokens.expiry_date || Date.now() >= this.tokens.expiry_date - 60_000;
92
+ if (!this.tokens.access_token || expiresSoon) {
93
+ await this.refreshAccessToken();
94
+ }
95
+ }
96
+ async googleRequest(config) {
97
+ await this.ensureAccessToken();
98
+ try {
99
+ return await googleAxios.request({
100
+ ...config,
101
+ headers: {
102
+ Authorization: `Bearer ${this.tokens.access_token}`,
103
+ ...(config.headers ?? {}),
104
+ },
105
+ });
106
+ }
107
+ catch (error) {
108
+ if (axios_1.default.isAxiosError(error) &&
109
+ (error.response?.status === 401 || error.response?.status === 403)) {
110
+ logger_1.default.warn('Google API request failed with auth error. Logging out.');
111
+ await this.logout();
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+ isAuthenticated() {
117
+ return !!this.tokens.refresh_token;
118
+ }
119
+ getAuthUrl() {
120
+ const { clientId } = this.getGoogleClientConfig();
121
+ const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
122
+ url.searchParams.set('client_id', clientId);
123
+ url.searchParams.set('redirect_uri', config_1.CONFIG.GOOGLE_REDIRECT_URI);
124
+ url.searchParams.set('response_type', 'code');
125
+ url.searchParams.set('access_type', 'offline');
126
+ url.searchParams.set('prompt', 'consent');
127
+ url.searchParams.set('scope', config_1.CONFIG.GOOGLE_SCOPES.join(' '));
128
+ return url.toString();
129
+ }
130
+ async handleCallback(code) {
131
+ const { clientId, clientSecret } = this.getGoogleClientConfig();
132
+ const params = new URLSearchParams({
133
+ code,
134
+ client_id: clientId,
135
+ client_secret: clientSecret,
136
+ redirect_uri: config_1.CONFIG.GOOGLE_REDIRECT_URI,
137
+ grant_type: 'authorization_code',
138
+ });
139
+ const { data } = await googleAxios.post('https://oauth2.googleapis.com/token', params.toString(), {
140
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
141
+ });
142
+ this.saveTokens(data);
143
+ return this.tokens;
144
+ }
145
+ async getUserProfile() {
146
+ if (!this.isAuthenticated())
147
+ return null;
148
+ try {
149
+ const res = await this.googleRequest({
150
+ method: 'GET',
151
+ url: 'https://www.googleapis.com/oauth2/v2/userinfo',
152
+ });
153
+ return res.data;
154
+ }
155
+ catch (error) {
156
+ logger_1.default.error({ err: error }, 'Failed to fetch user profile');
157
+ if (axios_1.default.isAxiosError(error) &&
158
+ (error.response?.status === 401 || error.response?.status === 403)) {
159
+ logger_1.default.warn('Google authentication token is invalid or expired. Logging out.');
160
+ await this.logout();
161
+ }
162
+ return null;
163
+ }
164
+ }
165
+ async logout() {
166
+ if (fs_1.default.existsSync(config_1.CONFIG.TOKEN_PATH)) {
167
+ fs_1.default.unlinkSync(config_1.CONFIG.TOKEN_PATH);
168
+ }
169
+ this.tokens = {};
170
+ this.folderIdCache.clear();
171
+ }
172
+ async ensureFolder(folderName) {
173
+ if (!this.isAuthenticated())
174
+ throw new Error('Not authenticated');
175
+ const cachedId = this.folderIdCache.get(folderName);
176
+ if (cachedId)
177
+ return cachedId;
178
+ const existing = await this.findFile(folderName, undefined, 'application/vnd.google-apps.folder');
179
+ if (existing) {
180
+ this.folderIdCache.set(folderName, existing.id);
181
+ return existing.id;
182
+ }
183
+ const fileMetadata = {
184
+ name: folderName,
185
+ mimeType: 'application/vnd.google-apps.folder',
186
+ };
187
+ try {
188
+ const res = await this.googleRequest({
189
+ method: 'POST',
190
+ url: 'https://www.googleapis.com/drive/v3/files',
191
+ params: { fields: 'id' },
192
+ data: fileMetadata,
193
+ headers: { 'Content-Type': 'application/json' },
194
+ });
195
+ const id = res.data.id;
196
+ this.folderIdCache.set(folderName, id);
197
+ return id;
198
+ }
199
+ catch (error) {
200
+ logger_1.default.error({ err: error }, `Failed to create folder ${folderName}`);
201
+ throw error;
202
+ }
203
+ }
204
+ async findFile(filename, parentId, mimeType) {
205
+ if (!this.isAuthenticated())
206
+ return null;
207
+ const safeName = filename.replace(/'/g, "\\'");
208
+ let query = `name = '${safeName}' and trashed = false`;
209
+ if (parentId) {
210
+ const safeParentId = parentId.replace(/'/g, "\\'");
211
+ query += ` and '${safeParentId}' in parents`;
212
+ }
213
+ if (mimeType) {
214
+ const safeMimeType = mimeType.replace(/'/g, "\\'");
215
+ query += ` and mimeType = '${safeMimeType}'`;
216
+ }
217
+ try {
218
+ const res = await this.googleRequest({
219
+ method: 'GET',
220
+ url: 'https://www.googleapis.com/drive/v3/files',
221
+ params: {
222
+ q: query,
223
+ fields: 'files(id, name)',
224
+ spaces: 'drive',
225
+ orderBy: 'createdTime desc',
226
+ },
227
+ });
228
+ if (res.data.files && res.data.files.length > 0) {
229
+ if (res.data.files.length > 1) {
230
+ logger_1.default.warn(`Multiple files found for ${filename}, using the most recent one.`);
231
+ }
232
+ return { id: res.data.files[0].id, name: res.data.files[0].name };
233
+ }
234
+ return null;
235
+ }
236
+ catch (error) {
237
+ logger_1.default.error({ err: error }, `Error while searching for file ${filename}`);
238
+ throw error;
239
+ }
240
+ }
241
+ async downloadFile(fileId, destPath) {
242
+ if (!this.isAuthenticated())
243
+ throw new Error('Not authenticated');
244
+ const dest = fs_1.default.createWriteStream(destPath);
245
+ try {
246
+ const res = await this.googleRequest({
247
+ method: 'GET',
248
+ url: `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}`,
249
+ params: { alt: 'media' },
250
+ responseType: 'stream',
251
+ });
252
+ await (0, promises_1.pipeline)(res.data, dest);
253
+ }
254
+ catch (error) {
255
+ dest.destroy();
256
+ throw error;
257
+ }
258
+ }
259
+ async uploadFile(filePath, filename, mimeType = 'application/octet-stream', parentId, existingFileId) {
260
+ if (!this.isAuthenticated())
261
+ throw new Error('Not authenticated');
262
+ let targetId = existingFileId;
263
+ if (!targetId) {
264
+ const existing = await this.findFile(filename, parentId, mimeType);
265
+ if (existing)
266
+ targetId = existing.id;
267
+ }
268
+ const media = {
269
+ mimeType,
270
+ body: fs_1.default.createReadStream(filePath),
271
+ };
272
+ try {
273
+ if (targetId) {
274
+ await this.googleRequest({
275
+ method: 'PATCH',
276
+ url: `https://www.googleapis.com/upload/drive/v3/files/${encodeURIComponent(targetId)}`,
277
+ params: { uploadType: 'media' },
278
+ data: media.body,
279
+ headers: { 'Content-Type': media.mimeType },
280
+ });
281
+ }
282
+ else {
283
+ const resource = { name: filename };
284
+ if (parentId) {
285
+ resource.parents = [parentId];
286
+ }
287
+ const created = await this.googleRequest({
288
+ method: 'POST',
289
+ url: 'https://www.googleapis.com/drive/v3/files',
290
+ params: { fields: 'id' },
291
+ data: resource,
292
+ headers: { 'Content-Type': 'application/json' },
293
+ });
294
+ await this.googleRequest({
295
+ method: 'PATCH',
296
+ url: `https://www.googleapis.com/upload/drive/v3/files/${encodeURIComponent(created.data.id)}`,
297
+ params: { uploadType: 'media' },
298
+ data: media.body,
299
+ headers: { 'Content-Type': media.mimeType },
300
+ });
301
+ }
302
+ }
303
+ catch (error) {
304
+ logger_1.default.error({ err: error }, `Failed to upload file ${filename}`);
305
+ throw error;
306
+ }
307
+ }
308
+ }
309
+ exports.GoogleDriveService = GoogleDriveService;
310
+ exports.googleDriveService = new GoogleDriveService();
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const pino_1 = __importDefault(require("pino"));
7
+ const isDevelopment = process.env.NODE_ENV !== 'production';
8
+ const logger = (0, pino_1.default)({
9
+ level: isDevelopment ? 'debug' : 'info',
10
+ transport: isDevelopment
11
+ ? {
12
+ target: 'pino-pretty',
13
+ options: {
14
+ colorize: true,
15
+ translateTime: 'SYS:standard',
16
+ ignore: 'pid,hostname',
17
+ },
18
+ }
19
+ : undefined,
20
+ });
21
+ exports.default = logger;