2021-08-18 00:01:11 +02:00
const express = require ( 'express' )
2021-09-22 03:57:33 +02:00
const Path = require ( 'path' )
2022-11-24 22:53:58 +01:00
const Logger = require ( '../Logger' )
2023-07-05 01:14:44 +02:00
const Database = require ( '../Database' )
2022-11-24 22:53:58 +01:00
const SocketAuthority = require ( '../SocketAuthority' )
2022-07-06 02:53:01 +02:00
const fs = require ( '../libs/fsExtra' )
2022-07-07 02:18:27 +02:00
const date = require ( '../libs/dateAndTime' )
2022-03-18 01:10:47 +01:00
2023-09-07 00:48:50 +02:00
const CacheManager = require ( '../managers/CacheManager' )
2022-03-18 01:10:47 +01:00
const LibraryController = require ( '../controllers/LibraryController' )
const UserController = require ( '../controllers/UserController' )
const CollectionController = require ( '../controllers/CollectionController' )
2022-11-26 22:14:45 +01:00
const PlaylistController = require ( '../controllers/PlaylistController' )
2022-03-18 01:10:47 +01:00
const MeController = require ( '../controllers/MeController' )
const BackupController = require ( '../controllers/BackupController' )
const LibraryItemController = require ( '../controllers/LibraryItemController' )
const SeriesController = require ( '../controllers/SeriesController' )
2022-11-19 20:28:06 +01:00
const FileSystemController = require ( '../controllers/FileSystemController' )
2022-03-18 01:10:47 +01:00
const AuthorController = require ( '../controllers/AuthorController' )
const SessionController = require ( '../controllers/SessionController' )
2022-03-19 16:13:10 +01:00
const PodcastController = require ( '../controllers/PodcastController' )
2022-09-22 01:01:10 +02:00
const NotificationController = require ( '../controllers/NotificationController' )
2023-05-30 00:38:38 +02:00
const EmailController = require ( '../controllers/EmailController' )
2022-11-19 20:28:06 +01:00
const SearchController = require ( '../controllers/SearchController' )
const CacheController = require ( '../controllers/CacheController' )
const ToolsController = require ( '../controllers/ToolsController' )
2022-12-26 23:58:36 +01:00
const RSSFeedController = require ( '../controllers/RSSFeedController' )
2022-03-18 17:51:55 +01:00
const MiscController = require ( '../controllers/MiscController' )
2022-03-18 01:10:47 +01:00
const Author = require ( '../objects/entities/Author' )
const Series = require ( '../objects/entities/Series' )
class ApiRouter {
2022-11-24 22:53:58 +01:00
constructor ( Server ) {
this . auth = Server . auth
this . playbackSessionManager = Server . playbackSessionManager
this . abMergeManager = Server . abMergeManager
this . backupManager = Server . backupManager
this . watcher = Server . watcher
this . podcastManager = Server . podcastManager
this . audioMetadataManager = Server . audioMetadataManager
this . rssFeedManager = Server . rssFeedManager
this . cronManager = Server . cronManager
this . notificationManager = Server . notificationManager
2023-05-30 00:38:38 +02:00
this . emailManager = Server . emailManager
2021-08-18 00:01:11 +02:00
this . router = express ( )
2023-02-01 21:34:01 +01:00
this . router . disable ( 'x-powered-by' )
2021-08-18 00:01:11 +02:00
this . init ( )
}
init ( ) {
2021-11-22 03:00:40 +01:00
//
// Library Routes
//
this . router . post ( '/libraries' , LibraryController . create . bind ( this ) )
this . router . get ( '/libraries' , LibraryController . findAll . bind ( this ) )
2023-09-04 22:26:07 +02:00
this . router . get ( '/libraries/:id' , LibraryController . middleware . bind ( this ) , LibraryController . findOne . bind ( this ) )
this . router . patch ( '/libraries/:id' , LibraryController . middleware . bind ( this ) , LibraryController . update . bind ( this ) )
this . router . delete ( '/libraries/:id' , LibraryController . middleware . bind ( this ) , LibraryController . delete . bind ( this ) )
2021-12-01 03:02:40 +01:00
2022-03-11 01:45:02 +01:00
this . router . get ( '/libraries/:id/items' , LibraryController . middleware . bind ( this ) , LibraryController . getLibraryItems . bind ( this ) )
2023-09-04 22:26:07 +02:00
this . router . delete ( '/libraries/:id/issues' , LibraryController . middleware . bind ( this ) , LibraryController . removeLibraryItemsWithIssues . bind ( this ) )
this . router . get ( '/libraries/:id/episode-downloads' , LibraryController . middleware . bind ( this ) , LibraryController . getEpisodeDownloadQueue . bind ( this ) )
this . router . get ( '/libraries/:id/series' , LibraryController . middleware . bind ( this ) , LibraryController . getAllSeriesForLibrary . bind ( this ) )
this . router . get ( '/libraries/:id/series/:seriesId' , LibraryController . middleware . bind ( this ) , LibraryController . getSeriesForLibrary . bind ( this ) )
this . router . get ( '/libraries/:id/collections' , LibraryController . middleware . bind ( this ) , LibraryController . getCollectionsForLibrary . bind ( this ) )
this . router . get ( '/libraries/:id/playlists' , LibraryController . middleware . bind ( this ) , LibraryController . getUserPlaylistsForLibrary . bind ( this ) )
this . router . get ( '/libraries/:id/personalized' , LibraryController . middleware . bind ( this ) , LibraryController . getUserPersonalizedShelves . bind ( this ) )
this . router . get ( '/libraries/:id/filterdata' , LibraryController . middleware . bind ( this ) , LibraryController . getLibraryFilterData . bind ( this ) )
this . router . get ( '/libraries/:id/search' , LibraryController . middleware . bind ( this ) , LibraryController . search . bind ( this ) )
this . router . get ( '/libraries/:id/stats' , LibraryController . middleware . bind ( this ) , LibraryController . stats . bind ( this ) )
this . router . get ( '/libraries/:id/authors' , LibraryController . middleware . bind ( this ) , LibraryController . getAuthors . bind ( this ) )
this . router . get ( '/libraries/:id/narrators' , LibraryController . middleware . bind ( this ) , LibraryController . getNarrators . bind ( this ) )
this . router . patch ( '/libraries/:id/narrators/:narratorId' , LibraryController . middleware . bind ( this ) , LibraryController . updateNarrator . bind ( this ) )
this . router . delete ( '/libraries/:id/narrators/:narratorId' , LibraryController . middleware . bind ( this ) , LibraryController . removeNarrator . bind ( this ) )
this . router . get ( '/libraries/:id/matchall' , LibraryController . middleware . bind ( this ) , LibraryController . matchAll . bind ( this ) )
this . router . post ( '/libraries/:id/scan' , LibraryController . middleware . bind ( this ) , LibraryController . scan . bind ( this ) )
this . router . get ( '/libraries/:id/recent-episodes' , LibraryController . middleware . bind ( this ) , LibraryController . getRecentEpisodes . bind ( this ) )
this . router . get ( '/libraries/:id/opml' , LibraryController . middleware . bind ( this ) , LibraryController . getOPMLFile . bind ( this ) )
2022-03-13 23:10:48 +01:00
this . router . post ( '/libraries/order' , LibraryController . reorder . bind ( this ) )
2023-10-18 00:46:43 +02:00
this . router . post ( '/libraries/:id/remove-metadata' , LibraryController . middleware . bind ( this ) , LibraryController . removeAllMetadataFiles . bind ( this ) )
2022-03-11 01:45:02 +01:00
//
// Item Routes
//
2023-05-27 21:51:03 +02:00
this . router . post ( '/items/batch/delete' , LibraryItemController . batchDelete . bind ( this ) )
this . router . post ( '/items/batch/update' , LibraryItemController . batchUpdate . bind ( this ) )
this . router . post ( '/items/batch/get' , LibraryItemController . batchGet . bind ( this ) )
this . router . post ( '/items/batch/quickmatch' , LibraryItemController . batchQuickMatch . bind ( this ) )
this . router . post ( '/items/batch/scan' , LibraryItemController . batchScan . bind ( this ) )
2022-03-14 01:34:31 +01:00
2022-03-11 01:45:02 +01:00
this . router . get ( '/items/:id' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . findOne . bind ( this ) )
2022-03-12 02:46:32 +01:00
this . router . patch ( '/items/:id' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . update . bind ( this ) )
2022-03-13 00:45:32 +01:00
this . router . delete ( '/items/:id' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . delete . bind ( this ) )
2023-04-10 00:05:35 +02:00
this . router . get ( '/items/:id/download' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . download . bind ( this ) )
2022-03-12 02:46:32 +01:00
this . router . patch ( '/items/:id/media' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . updateMedia . bind ( this ) )
2023-09-21 23:57:48 +02:00
this . router . get ( '/items/:id/cover' , LibraryItemController . getCover . bind ( this ) )
2022-03-13 00:45:32 +01:00
this . router . post ( '/items/:id/cover' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . uploadCover . bind ( this ) )
this . router . patch ( '/items/:id/cover' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . updateCover . bind ( this ) )
this . router . delete ( '/items/:id/cover' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . removeCover . bind ( this ) )
2022-03-14 01:34:31 +01:00
this . router . post ( '/items/:id/match' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . match . bind ( this ) )
2022-03-18 01:10:47 +01:00
this . router . post ( '/items/:id/play' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . startPlaybackSession . bind ( this ) )
2022-03-26 23:41:26 +01:00
this . router . post ( '/items/:id/play/:episodeId' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . startEpisodePlaybackSession . bind ( this ) )
2022-03-26 17:59:34 +01:00
this . router . patch ( '/items/:id/tracks' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . updateTracks . bind ( this ) )
2023-02-04 00:50:42 +01:00
this . router . post ( '/items/:id/scan' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . scan . bind ( this ) )
2022-09-25 22:56:06 +02:00
this . router . get ( '/items/:id/tone-object' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . getToneMetadataObject . bind ( this ) )
2022-05-11 00:03:41 +02:00
this . router . post ( '/items/:id/chapters' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . updateMediaChapters . bind ( this ) )
2023-06-25 23:16:11 +02:00
this . router . get ( '/items/:id/ffprobe/:fileid' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . getFFprobeData . bind ( this ) )
2023-05-28 19:34:22 +02:00
this . router . get ( '/items/:id/file/:fileid' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . getLibraryFile . bind ( this ) )
this . router . delete ( '/items/:id/file/:fileid' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . deleteLibraryFile . bind ( this ) )
this . router . get ( '/items/:id/file/:fileid/download' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . downloadLibraryFile . bind ( this ) )
2023-06-10 19:46:57 +02:00
this . router . get ( '/items/:id/ebook/:fileid?' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . getEBookFile . bind ( this ) )
this . router . patch ( '/items/:id/ebook/:fileid/status' , LibraryItemController . middleware . bind ( this ) , LibraryItemController . updateEbookFileStatus . bind ( this ) )
2022-03-11 01:45:02 +01:00
2021-11-22 03:00:40 +01:00
//
// User Routes
//
2022-09-26 00:11:39 +02:00
this . router . post ( '/users' , UserController . middleware . bind ( this ) , UserController . create . bind ( this ) )
this . router . get ( '/users' , UserController . middleware . bind ( this ) , UserController . findAll . bind ( this ) )
2022-11-11 00:42:20 +01:00
this . router . get ( '/users/online' , UserController . getOnlineUsers . bind ( this ) )
2022-09-26 00:11:39 +02:00
this . router . get ( '/users/:id' , UserController . middleware . bind ( this ) , UserController . findOne . bind ( this ) )
this . router . patch ( '/users/:id' , UserController . middleware . bind ( this ) , UserController . update . bind ( this ) )
this . router . delete ( '/users/:id' , UserController . middleware . bind ( this ) , UserController . delete . bind ( this ) )
2021-11-22 03:00:40 +01:00
2022-09-26 00:11:39 +02:00
this . router . get ( '/users/:id/listening-sessions' , UserController . middleware . bind ( this ) , UserController . getListeningSessions . bind ( this ) )
this . router . get ( '/users/:id/listening-stats' , UserController . middleware . bind ( this ) , UserController . getListeningStats . bind ( this ) )
2021-11-22 03:00:40 +01:00
//
// Collection Routes
//
2022-08-31 22:46:10 +02:00
this . router . post ( '/collections' , CollectionController . middleware . bind ( this ) , CollectionController . create . bind ( this ) )
2021-11-22 03:00:40 +01:00
this . router . get ( '/collections' , CollectionController . findAll . bind ( this ) )
2022-08-31 22:46:10 +02:00
this . router . get ( '/collections/:id' , CollectionController . middleware . bind ( this ) , CollectionController . findOne . bind ( this ) )
this . router . patch ( '/collections/:id' , CollectionController . middleware . bind ( this ) , CollectionController . update . bind ( this ) )
this . router . delete ( '/collections/:id' , CollectionController . middleware . bind ( this ) , CollectionController . delete . bind ( this ) )
this . router . post ( '/collections/:id/book' , CollectionController . middleware . bind ( this ) , CollectionController . addBook . bind ( this ) )
this . router . delete ( '/collections/:id/book/:bookId' , CollectionController . middleware . bind ( this ) , CollectionController . removeBook . bind ( this ) )
this . router . post ( '/collections/:id/batch/add' , CollectionController . middleware . bind ( this ) , CollectionController . addBatch . bind ( this ) )
this . router . post ( '/collections/:id/batch/remove' , CollectionController . middleware . bind ( this ) , CollectionController . removeBatch . bind ( this ) )
2021-11-22 03:00:40 +01:00
2022-11-26 22:14:45 +01:00
//
// Playlist Routes
//
2022-12-18 00:31:19 +01:00
this . router . post ( '/playlists' , PlaylistController . create . bind ( this ) )
2022-11-26 22:14:45 +01:00
this . router . get ( '/playlists' , PlaylistController . findAllForUser . bind ( this ) )
this . router . get ( '/playlists/:id' , PlaylistController . middleware . bind ( this ) , PlaylistController . findOne . bind ( this ) )
this . router . patch ( '/playlists/:id' , PlaylistController . middleware . bind ( this ) , PlaylistController . update . bind ( this ) )
this . router . delete ( '/playlists/:id' , PlaylistController . middleware . bind ( this ) , PlaylistController . delete . bind ( this ) )
this . router . post ( '/playlists/:id/item' , PlaylistController . middleware . bind ( this ) , PlaylistController . addItem . bind ( this ) )
this . router . delete ( '/playlists/:id/item/:libraryItemId/:episodeId?' , PlaylistController . middleware . bind ( this ) , PlaylistController . removeItem . bind ( this ) )
this . router . post ( '/playlists/:id/batch/add' , PlaylistController . middleware . bind ( this ) , PlaylistController . addBatch . bind ( this ) )
this . router . post ( '/playlists/:id/batch/remove' , PlaylistController . middleware . bind ( this ) , PlaylistController . removeBatch . bind ( this ) )
2022-12-18 00:31:19 +01:00
this . router . post ( '/playlists/collection/:collectionId' , PlaylistController . createFromCollection . bind ( this ) )
2022-11-26 22:14:45 +01:00
2021-11-22 03:00:40 +01:00
//
// Current User Routes (Me)
//
2023-02-05 23:52:17 +01:00
this . router . get ( '/me' , MeController . getCurrentUser . bind ( this ) )
2021-11-22 03:00:40 +01:00
this . router . get ( '/me/listening-sessions' , MeController . getListeningSessions . bind ( this ) )
this . router . get ( '/me/listening-stats' , MeController . getListeningStats . bind ( this ) )
2022-09-29 00:45:39 +02:00
this . router . get ( '/me/progress/:id/remove-from-continue-listening' , MeController . removeItemFromContinueListening . bind ( this ) )
2022-06-04 01:59:42 +02:00
this . router . get ( '/me/progress/:id/:episodeId?' , MeController . getMediaProgress . bind ( this ) )
2022-04-24 00:17:05 +02:00
this . router . patch ( '/me/progress/batch/update' , MeController . batchUpdateMediaProgress . bind ( this ) )
2022-03-26 17:59:34 +01:00
this . router . patch ( '/me/progress/:id' , MeController . createUpdateMediaProgress . bind ( this ) )
this . router . delete ( '/me/progress/:id' , MeController . removeMediaProgress . bind ( this ) )
2022-03-26 23:41:26 +01:00
this . router . patch ( '/me/progress/:id/:episodeId' , MeController . createUpdateEpisodeMediaProgress . bind ( this ) )
2022-03-18 02:28:04 +01:00
this . router . post ( '/me/item/:id/bookmark' , MeController . createBookmark . bind ( this ) )
this . router . patch ( '/me/item/:id/bookmark' , MeController . updateBookmark . bind ( this ) )
this . router . delete ( '/me/item/:id/bookmark/:time' , MeController . removeBookmark . bind ( this ) )
2021-11-22 03:00:40 +01:00
this . router . patch ( '/me/password' , MeController . updatePassword . bind ( this ) )
2023-02-05 23:52:17 +01:00
this . router . post ( '/me/sync-local-progress' , MeController . syncLocalMediaProgress . bind ( this ) ) // TODO: Deprecated. Removed from Android. Only used in iOS app now.
2022-08-14 17:24:41 +02:00
this . router . get ( '/me/items-in-progress' , MeController . getAllLibraryItemsInProgress . bind ( this ) )
2022-09-29 00:45:39 +02:00
this . router . get ( '/me/series/:id/remove-from-continue-listening' , MeController . removeSeriesFromContinueListening . bind ( this ) )
2022-11-16 00:20:57 +01:00
this . router . get ( '/me/series/:id/readd-to-continue-listening' , MeController . readdSeriesFromContinueListening . bind ( this ) )
2021-11-22 03:00:40 +01:00
//
// Backup Routes
//
2022-11-24 20:14:29 +01:00
this . router . get ( '/backups' , BackupController . middleware . bind ( this ) , BackupController . getAll . bind ( this ) )
this . router . post ( '/backups' , BackupController . middleware . bind ( this ) , BackupController . create . bind ( this ) )
this . router . delete ( '/backups/:id' , BackupController . middleware . bind ( this ) , BackupController . delete . bind ( this ) )
2023-06-27 23:41:32 +02:00
this . router . get ( '/backups/:id/download' , BackupController . middleware . bind ( this ) , BackupController . download . bind ( this ) )
2022-11-24 20:14:29 +01:00
this . router . get ( '/backups/:id/apply' , BackupController . middleware . bind ( this ) , BackupController . apply . bind ( this ) )
this . router . post ( '/backups/upload' , BackupController . middleware . bind ( this ) , BackupController . upload . bind ( this ) )
2021-11-22 03:00:40 +01:00
2021-12-26 18:25:07 +01:00
//
// File System Routes
//
this . router . get ( '/filesystem' , FileSystemController . getPaths . bind ( this ) )
2023-05-27 23:00:34 +02:00
this . router . post ( '/filesystem/pathexists' , FileSystemController . checkPathExists . bind ( this ) )
2021-12-26 18:25:07 +01:00
2021-11-22 03:00:40 +01:00
//
2022-03-12 02:46:32 +01:00
// Author Routes
2021-11-22 03:00:40 +01:00
//
2022-03-13 12:42:43 +01:00
this . router . get ( '/authors/:id' , AuthorController . middleware . bind ( this ) , AuthorController . findOne . bind ( this ) )
2022-03-15 00:53:49 +01:00
this . router . patch ( '/authors/:id' , AuthorController . middleware . bind ( this ) , AuthorController . update . bind ( this ) )
2023-09-25 00:06:32 +02:00
this . router . delete ( '/authors/:id' , AuthorController . middleware . bind ( this ) , AuthorController . delete . bind ( this ) )
2022-03-13 12:42:43 +01:00
this . router . post ( '/authors/:id/match' , AuthorController . middleware . bind ( this ) , AuthorController . match . bind ( this ) )
2022-03-13 16:35:35 +01:00
this . router . get ( '/authors/:id/image' , AuthorController . middleware . bind ( this ) , AuthorController . getImage . bind ( this ) )
2023-10-14 00:37:37 +02:00
this . router . post ( '/authors/:id/image' , AuthorController . middleware . bind ( this ) , AuthorController . uploadImage . bind ( this ) )
this . router . delete ( '/authors/:id/image' , AuthorController . middleware . bind ( this ) , AuthorController . deleteImage . bind ( this ) )
2021-11-18 02:19:24 +01:00
2022-03-12 02:46:32 +01:00
//
// Series Routes
//
2022-03-13 23:10:48 +01:00
this . router . get ( '/series/:id' , SeriesController . middleware . bind ( this ) , SeriesController . findOne . bind ( this ) )
2022-09-28 00:48:45 +02:00
this . router . patch ( '/series/:id' , SeriesController . middleware . bind ( this ) , SeriesController . update . bind ( this ) )
2022-03-12 02:46:32 +01:00
2022-03-18 01:10:47 +01:00
//
// Playback Session Routes
//
2022-06-04 19:44:42 +02:00
this . router . get ( '/sessions' , SessionController . getAllWithUserData . bind ( this ) )
2022-08-13 19:24:19 +02:00
this . router . delete ( '/sessions/:id' , SessionController . middleware . bind ( this ) , SessionController . delete . bind ( this ) )
2023-04-09 01:01:24 +02:00
this . router . get ( '/sessions/open' , SessionController . getOpenSessions . bind ( this ) )
2023-02-05 23:52:17 +01:00
this . router . post ( '/session/local' , SessionController . syncLocal . bind ( this ) )
this . router . post ( '/session/local-all' , SessionController . syncLocalSessions . bind ( this ) )
2022-08-13 19:24:19 +02:00
// TODO: Update these endpoints because they are only for open playback sessions
this . router . get ( '/session/:id' , SessionController . openSessionMiddleware . bind ( this ) , SessionController . getOpenSession . bind ( this ) )
this . router . post ( '/session/:id/sync' , SessionController . openSessionMiddleware . bind ( this ) , SessionController . sync . bind ( this ) )
this . router . post ( '/session/:id/close' , SessionController . openSessionMiddleware . bind ( this ) , SessionController . close . bind ( this ) )
2022-03-18 01:10:47 +01:00
2022-03-19 16:13:10 +01:00
//
// Podcast Routes
//
this . router . post ( '/podcasts' , PodcastController . create . bind ( this ) )
this . router . post ( '/podcasts/feed' , PodcastController . getPodcastFeed . bind ( this ) )
2023-05-28 22:10:34 +02:00
this . router . post ( '/podcasts/opml' , PodcastController . getFeedsFromOPMLText . bind ( this ) )
2022-05-02 21:41:59 +02:00
this . router . get ( '/podcasts/:id/checknew' , PodcastController . middleware . bind ( this ) , PodcastController . checkNewEpisodes . bind ( this ) )
this . router . get ( '/podcasts/:id/downloads' , PodcastController . middleware . bind ( this ) , PodcastController . getEpisodeDownloads . bind ( this ) )
this . router . get ( '/podcasts/:id/clear-queue' , PodcastController . middleware . bind ( this ) , PodcastController . clearEpisodeDownloadQueue . bind ( this ) )
2022-07-31 20:12:37 +02:00
this . router . get ( '/podcasts/:id/search-episode' , PodcastController . middleware . bind ( this ) , PodcastController . findEpisode . bind ( this ) )
2022-05-02 21:41:59 +02:00
this . router . post ( '/podcasts/:id/download-episodes' , PodcastController . middleware . bind ( this ) , PodcastController . downloadEpisodes . bind ( this ) )
2023-01-05 01:13:46 +01:00
this . router . post ( '/podcasts/:id/match-episodes' , PodcastController . middleware . bind ( this ) , PodcastController . quickMatchEpisodes . bind ( this ) )
2023-03-04 20:04:55 +01:00
this . router . get ( '/podcasts/:id/episode/:episodeId' , PodcastController . middleware . bind ( this ) , PodcastController . getEpisode . bind ( this ) )
2022-05-02 21:41:59 +02:00
this . router . patch ( '/podcasts/:id/episode/:episodeId' , PodcastController . middleware . bind ( this ) , PodcastController . updateEpisode . bind ( this ) )
2022-05-25 01:38:25 +02:00
this . router . delete ( '/podcasts/:id/episode/:episodeId' , PodcastController . middleware . bind ( this ) , PodcastController . removeEpisode . bind ( this ) )
2022-03-19 16:13:10 +01:00
2022-09-22 01:01:10 +02:00
//
2022-12-26 23:58:36 +01:00
// Notification Routes (Admin and up)
2022-09-22 01:01:10 +02:00
//
this . router . get ( '/notifications' , NotificationController . middleware . bind ( this ) , NotificationController . get . bind ( this ) )
this . router . patch ( '/notifications' , NotificationController . middleware . bind ( this ) , NotificationController . update . bind ( this ) )
2022-09-23 01:12:48 +02:00
this . router . get ( '/notificationdata' , NotificationController . middleware . bind ( this ) , NotificationController . getData . bind ( this ) )
2022-09-24 23:15:16 +02:00
this . router . get ( '/notifications/test' , NotificationController . middleware . bind ( this ) , NotificationController . fireTestEvent . bind ( this ) )
2022-09-24 01:10:03 +02:00
this . router . post ( '/notifications' , NotificationController . middleware . bind ( this ) , NotificationController . createNotification . bind ( this ) )
this . router . delete ( '/notifications/:id' , NotificationController . middleware . bind ( this ) , NotificationController . deleteNotification . bind ( this ) )
this . router . patch ( '/notifications/:id' , NotificationController . middleware . bind ( this ) , NotificationController . updateNotification . bind ( this ) )
this . router . get ( '/notifications/:id/test' , NotificationController . middleware . bind ( this ) , NotificationController . sendNotificationTest . bind ( this ) )
2022-09-22 01:01:10 +02:00
2023-05-30 00:38:38 +02:00
//
// Email Routes (Admin and up)
//
this . router . get ( '/emails/settings' , EmailController . middleware . bind ( this ) , EmailController . getSettings . bind ( this ) )
this . router . patch ( '/emails/settings' , EmailController . middleware . bind ( this ) , EmailController . updateSettings . bind ( this ) )
this . router . post ( '/emails/test' , EmailController . middleware . bind ( this ) , EmailController . sendTest . bind ( this ) )
this . router . post ( '/emails/ereader-devices' , EmailController . middleware . bind ( this ) , EmailController . updateEReaderDevices . bind ( this ) )
this . router . post ( '/emails/send-ebook-to-device' , EmailController . middleware . bind ( this ) , EmailController . sendEBookToDevice . bind ( this ) )
2022-11-19 20:28:06 +01:00
//
// Search Routes
//
this . router . get ( '/search/covers' , SearchController . findCovers . bind ( this ) )
this . router . get ( '/search/books' , SearchController . findBooks . bind ( this ) )
this . router . get ( '/search/podcast' , SearchController . findPodcasts . bind ( this ) )
this . router . get ( '/search/authors' , SearchController . findAuthor . bind ( this ) )
this . router . get ( '/search/chapters' , SearchController . findChapters . bind ( this ) )
2023-01-07 20:05:33 +01:00
this . router . get ( '/search/tracks' , SearchController . findMusicTrack . bind ( this ) )
2022-11-19 20:28:06 +01:00
//
2022-12-26 23:58:36 +01:00
// Cache Routes (Admin and up)
2022-11-19 20:28:06 +01:00
//
this . router . post ( '/cache/purge' , CacheController . purgeCache . bind ( this ) )
this . router . post ( '/cache/items/purge' , CacheController . purgeItemsCache . bind ( this ) )
//
2022-12-26 23:58:36 +01:00
// Tools Routes (Admin and up)
2022-11-19 20:28:06 +01:00
//
2023-04-02 23:13:18 +02:00
this . router . post ( '/tools/item/:id/encode-m4b' , ToolsController . middleware . bind ( this ) , ToolsController . encodeM4b . bind ( this ) )
this . router . delete ( '/tools/item/:id/encode-m4b' , ToolsController . middleware . bind ( this ) , ToolsController . cancelM4bEncode . bind ( this ) )
this . router . post ( '/tools/item/:id/embed-metadata' , ToolsController . middleware . bind ( this ) , ToolsController . embedAudioFileMetadata . bind ( this ) )
this . router . post ( '/tools/batch/embed-metadata' , ToolsController . middleware . bind ( this ) , ToolsController . batchEmbedMetadata . bind ( this ) )
2022-11-19 20:28:06 +01:00
2023-08-22 18:42:55 +02:00
//
2022-12-26 23:58:36 +01:00
// RSS Feed Routes (Admin and up)
//
2023-08-22 23:37:22 +02:00
this . router . get ( '/feeds' , RSSFeedController . middleware . bind ( this ) , RSSFeedController . getAll . bind ( this ) )
2022-12-26 23:58:36 +01:00
this . router . post ( '/feeds/item/:itemId/open' , RSSFeedController . middleware . bind ( this ) , RSSFeedController . openRSSFeedForItem . bind ( this ) )
2022-12-27 00:48:39 +01:00
this . router . post ( '/feeds/collection/:collectionId/open' , RSSFeedController . middleware . bind ( this ) , RSSFeedController . openRSSFeedForCollection . bind ( this ) )
2022-12-31 23:58:19 +01:00
this . router . post ( '/feeds/series/:seriesId/open' , RSSFeedController . middleware . bind ( this ) , RSSFeedController . openRSSFeedForSeries . bind ( this ) )
2022-12-26 23:58:36 +01:00
this . router . post ( '/feeds/:id/close' , RSSFeedController . middleware . bind ( this ) , RSSFeedController . closeRSSFeed . bind ( this ) )
2022-03-12 02:46:32 +01:00
//
// Misc Routes
//
2022-03-18 17:51:55 +01:00
this . router . post ( '/upload' , MiscController . handleUpload . bind ( this ) )
2022-10-02 21:16:17 +02:00
this . router . get ( '/tasks' , MiscController . getTasks . bind ( this ) )
2022-10-02 21:46:48 +02:00
this . router . patch ( '/settings' , MiscController . updateServerSettings . bind ( this ) )
2023-09-08 19:32:30 +02:00
this . router . patch ( '/sorting-prefixes' , MiscController . updateSortingPrefixes . bind ( this ) )
2022-03-18 17:51:55 +01:00
this . router . post ( '/authorize' , MiscController . authorize . bind ( this ) )
2022-03-20 12:29:08 +01:00
this . router . get ( '/tags' , MiscController . getAllTags . bind ( this ) )
2022-12-18 21:17:52 +01:00
this . router . post ( '/tags/rename' , MiscController . renameTag . bind ( this ) )
this . router . delete ( '/tags/:tag' , MiscController . deleteTag . bind ( this ) )
2022-12-18 21:52:53 +01:00
this . router . get ( '/genres' , MiscController . getAllGenres . bind ( this ) )
this . router . post ( '/genres/rename' , MiscController . renameGenre . bind ( this ) )
this . router . delete ( '/genres/:genre' , MiscController . deleteGenre . bind ( this ) )
2022-08-02 01:06:22 +02:00
this . router . post ( '/validate-cron' , MiscController . validateCronExpression . bind ( this ) )
2021-09-04 21:17:26 +02:00
}
2021-10-05 05:11:42 +02:00
async getDirectories ( dir , relpath , excludedDirs , level = 0 ) {
try {
2022-11-24 23:35:26 +01:00
const paths = await fs . readdir ( dir )
2021-10-05 05:11:42 +02:00
2022-11-24 23:35:26 +01:00
let dirs = await Promise . all ( paths . map ( async dirname => {
const fullPath = Path . join ( dir , dirname )
const path = Path . join ( relpath , dirname )
2021-10-05 05:11:42 +02:00
2022-11-24 23:35:26 +01:00
const isDir = ( await fs . lstat ( fullPath ) ) . isDirectory ( )
2021-10-11 02:29:22 +02:00
if ( isDir && ! excludedDirs . includes ( path ) && dirname !== 'node_modules' ) {
2021-10-05 05:11:42 +02:00
return {
path ,
dirname ,
fullPath ,
level ,
dirs : level < 4 ? ( await this . getDirectories ( fullPath , path , excludedDirs , level + 1 ) ) : [ ]
}
} else {
return false
}
} ) )
dirs = dirs . filter ( d => d )
return dirs
} catch ( error ) {
Logger . error ( 'Failed to readdir' , dir , error )
return [ ]
}
}
2021-11-22 03:00:40 +01:00
//
// Helper Methods
//
2023-08-14 00:45:53 +02:00
/ * *
* Remove library item and associated entities
2023-08-22 18:42:55 +02:00
* @ param { string } mediaType
* @ param { string } libraryItemId
2023-08-14 00:45:53 +02:00
* @ param { string [ ] } mediaItemIds array of bookId or podcastEpisodeId
* /
async handleDeleteLibraryItem ( mediaType , libraryItemId , mediaItemIds ) {
2023-07-05 01:14:44 +02:00
// Remove media progress for this library item from all users
2023-08-20 20:34:03 +02:00
const users = await Database . userModel . getOldUsers ( )
2023-07-22 22:32:20 +02:00
for ( const user of users ) {
2023-08-14 00:45:53 +02:00
for ( const mediaProgress of user . getAllMediaProgressForLibraryItem ( libraryItemId ) ) {
2023-07-05 01:14:44 +02:00
await Database . removeMediaProgress ( mediaProgress . id )
2021-11-22 03:00:40 +01:00
}
}
2022-11-24 23:35:26 +01:00
// TODO: Remove open sessions for library item
2023-08-14 00:45:53 +02:00
// Remove series if empty
if ( mediaType === 'book' ) {
2023-08-18 00:58:57 +02:00
// TODO: update filter data
2023-08-20 20:34:03 +02:00
const bookSeries = await Database . bookSeriesModel . findAll ( {
2023-08-14 00:45:53 +02:00
where : {
bookId : mediaItemIds [ 0 ]
} ,
include : {
2023-08-20 20:34:03 +02:00
model : Database . seriesModel ,
2023-08-14 00:45:53 +02:00
include : {
2023-08-20 20:34:03 +02:00
model : Database . bookModel
2023-08-14 00:45:53 +02:00
}
}
} )
for ( const bs of bookSeries ) {
if ( bs . series . books . length === 1 ) {
await this . removeEmptySeries ( bs . series )
}
}
2022-11-27 21:49:21 +01:00
}
// remove item from playlists
2023-08-20 20:34:03 +02:00
const playlistsWithItem = await Database . playlistModel . getPlaylistsForMediaItemIds ( mediaItemIds )
2023-07-23 16:42:57 +02:00
for ( const playlist of playlistsWithItem ) {
2023-08-13 18:22:38 +02:00
let numMediaItems = playlist . playlistMediaItems . length
let order = 1
// Remove items in playlist and re-order
for ( const playlistMediaItem of playlist . playlistMediaItems ) {
if ( mediaItemIds . includes ( playlistMediaItem . mediaItemId ) ) {
await playlistMediaItem . destroy ( )
numMediaItems --
} else {
if ( playlistMediaItem . order !== order ) {
playlistMediaItem . update ( {
order
} )
}
order ++
}
}
2022-11-27 21:49:21 +01:00
// If playlist is now empty then remove it
2023-08-13 18:22:38 +02:00
const jsonExpanded = await playlist . getOldJsonExpanded ( )
if ( ! numMediaItems ) {
2022-11-27 21:49:21 +01:00
Logger . info ( ` [ApiRouter] Playlist " ${ playlist . name } " has no more items - removing it ` )
2023-08-13 18:22:38 +02:00
await playlist . destroy ( )
SocketAuthority . clientEmitter ( playlist . userId , 'playlist_removed' , jsonExpanded )
2022-11-27 21:49:21 +01:00
} else {
2023-08-13 18:22:38 +02:00
SocketAuthority . clientEmitter ( playlist . userId , 'playlist_updated' , jsonExpanded )
2022-11-27 21:49:21 +01:00
}
2021-11-13 02:43:16 +01:00
}
2021-11-22 03:00:40 +01:00
2022-12-31 21:08:34 +01:00
// Close rss feed - remove from db and emit socket event
2023-08-14 00:45:53 +02:00
await this . rssFeedManager . closeFeedForEntityId ( libraryItemId )
2022-12-31 21:08:34 +01:00
2021-12-13 00:15:37 +01:00
// purge cover cache
2023-09-07 00:48:50 +02:00
await CacheManager . purgeCoverCache ( libraryItemId )
2021-12-13 00:15:37 +01:00
2023-08-14 00:45:53 +02:00
const itemMetadataPath = Path . join ( global . MetadataPath , 'items' , libraryItemId )
2023-04-16 23:23:13 +02:00
if ( await fs . pathExists ( itemMetadataPath ) ) {
Logger . debug ( ` [ApiRouter] Removing item metadata path " ${ itemMetadataPath } " ` )
await fs . remove ( itemMetadataPath )
}
2023-08-14 00:45:53 +02:00
await Database . removeLibraryItem ( libraryItemId )
2023-08-13 22:10:26 +02:00
SocketAuthority . emitter ( 'item_removed' , {
2023-08-14 00:45:53 +02:00
id : libraryItemId
2023-08-13 22:10:26 +02:00
} )
2021-11-22 03:00:40 +01:00
}
2023-08-18 00:58:57 +02:00
/ * *
* Used when a series is removed from a book
* Series is removed if it only has 1 book
2023-08-20 00:12:24 +02:00
*
* @ param { string } bookId
* @ param { string [ ] } seriesIds
2023-08-18 00:58:57 +02:00
* /
async checkRemoveEmptySeries ( bookId , seriesIds ) {
if ( ! seriesIds ? . length ) return
2023-08-20 20:34:03 +02:00
const bookSeries = await Database . bookSeriesModel . findAll ( {
2023-08-18 00:58:57 +02:00
where : {
bookId ,
seriesId : seriesIds
} ,
include : [
{
2023-08-20 20:34:03 +02:00
model : Database . seriesModel ,
2023-08-18 00:58:57 +02:00
include : {
2023-08-20 20:34:03 +02:00
model : Database . bookModel
2023-08-18 00:58:57 +02:00
}
}
]
} )
for ( const bs of bookSeries ) {
if ( bs . series . books . length === 1 ) {
await this . removeEmptySeries ( bs . series )
2022-12-31 23:58:19 +01:00
}
}
}
2023-08-18 21:40:36 +02:00
/ * *
* Remove an empty series & close an open RSS feed
* @ param { import ( '../models/Series' ) } series
* /
2023-08-14 00:45:53 +02:00
async removeEmptySeries ( series ) {
await this . rssFeedManager . closeFeedForEntityId ( series . id )
Logger . info ( ` [ApiRouter] Series " ${ series . name } " is now empty. Removing series ` )
await Database . removeSeries ( series . id )
2023-08-18 21:40:36 +02:00
// Remove series from library filter data
Database . removeSeriesFromFilterData ( series . libraryId , series . id )
SocketAuthority . emitter ( 'series_removed' , {
id : series . id ,
libraryId : series . libraryId
} )
2023-08-14 00:45:53 +02:00
}
2021-11-22 03:00:40 +01:00
async getUserListeningSessionsHelper ( userId ) {
2023-07-05 01:14:44 +02:00
const userSessions = await Database . getPlaybackSessions ( { userId } )
2022-03-18 01:10:47 +01:00
return userSessions . sort ( ( a , b ) => b . updatedAt - a . updatedAt )
2021-11-13 02:43:16 +01:00
}
2022-06-04 19:44:42 +02:00
async getAllSessionsWithUserData ( ) {
2023-07-05 01:14:44 +02:00
const sessions = await Database . getPlaybackSessions ( )
2022-06-04 19:44:42 +02:00
sessions . sort ( ( a , b ) => b . updatedAt - a . updatedAt )
2023-08-20 20:34:03 +02:00
const minifiedUserObjects = await Database . userModel . getMinifiedUserObjects ( )
2022-06-04 19:44:42 +02:00
return sessions . map ( se => {
2022-11-24 23:35:26 +01:00
return {
2022-06-04 19:44:42 +02:00
... se ,
2023-07-22 22:32:20 +02:00
user : minifiedUserObjects . find ( u => u . id === se . userId ) || null
2022-06-04 19:44:42 +02:00
}
} )
}
2021-11-13 02:43:16 +01:00
async getUserListeningStatsHelpers ( userId ) {
const today = date . format ( new Date ( ) , 'YYYY-MM-DD' )
2022-11-24 23:35:26 +01:00
const listeningSessions = await this . getUserListeningSessionsHelper ( userId )
const listeningStats = {
2021-11-13 02:43:16 +01:00
totalTime : 0 ,
2022-03-18 01:10:47 +01:00
items : { } ,
2021-11-13 02:43:16 +01:00
days : { } ,
dayOfWeek : { } ,
2021-12-29 22:53:19 +01:00
today : 0 ,
recentSessions : listeningSessions . slice ( 0 , 10 )
2021-11-13 02:43:16 +01:00
}
listeningSessions . forEach ( ( s ) => {
2022-11-24 23:35:26 +01:00
let sessionTimeListening = s . timeListening
2022-04-21 01:16:27 +02:00
if ( typeof sessionTimeListening == 'string' ) {
sessionTimeListening = Number ( sessionTimeListening )
}
2021-11-13 02:43:16 +01:00
if ( s . dayOfWeek ) {
if ( ! listeningStats . dayOfWeek [ s . dayOfWeek ] ) listeningStats . dayOfWeek [ s . dayOfWeek ] = 0
2022-04-21 01:16:27 +02:00
listeningStats . dayOfWeek [ s . dayOfWeek ] += sessionTimeListening
2021-11-13 02:43:16 +01:00
}
2022-04-21 01:16:27 +02:00
if ( s . date && sessionTimeListening > 0 ) {
2021-11-13 02:43:16 +01:00
if ( ! listeningStats . days [ s . date ] ) listeningStats . days [ s . date ] = 0
2022-04-21 01:16:27 +02:00
listeningStats . days [ s . date ] += sessionTimeListening
2021-11-13 02:43:16 +01:00
if ( s . date === today ) {
2022-04-21 01:16:27 +02:00
listeningStats . today += sessionTimeListening
2021-11-13 02:43:16 +01:00
}
}
2022-03-18 01:10:47 +01:00
if ( ! listeningStats . items [ s . libraryItemId ] ) {
listeningStats . items [ s . libraryItemId ] = {
id : s . libraryItemId ,
2022-04-21 01:16:27 +02:00
timeListening : sessionTimeListening ,
2022-03-18 01:10:47 +01:00
mediaMetadata : s . mediaMetadata ,
2021-12-29 22:53:19 +01:00
lastUpdate : s . lastUpdate
}
} else {
2022-04-21 01:16:27 +02:00
listeningStats . items [ s . libraryItemId ] . timeListening += sessionTimeListening
2021-12-29 22:53:19 +01:00
}
2021-11-13 02:43:16 +01:00
2022-04-21 01:16:27 +02:00
listeningStats . totalTime += sessionTimeListening
2021-11-13 02:43:16 +01:00
} )
return listeningStats
}
2021-12-13 00:15:37 +01:00
2023-07-08 16:57:32 +02:00
async createAuthorsAndSeriesForItemUpdate ( mediaPayload , libraryId ) {
2022-03-13 23:10:48 +01:00
if ( mediaPayload . metadata ) {
2022-11-24 23:35:26 +01:00
const mediaMetadata = mediaPayload . metadata
2022-03-13 23:10:48 +01:00
// Create new authors if in payload
2023-08-18 21:40:36 +02:00
if ( mediaMetadata . authors ? . length ) {
2022-11-24 23:35:26 +01:00
const newAuthors = [ ]
2022-03-13 23:10:48 +01:00
for ( let i = 0 ; i < mediaMetadata . authors . length ; i ++ ) {
2023-04-01 00:04:26 +02:00
const authorName = ( mediaMetadata . authors [ i ] . name || '' ) . trim ( )
if ( ! authorName ) {
Logger . error ( ` [ApiRouter] Invalid author object, no name ` , mediaMetadata . authors [ i ] )
continue
}
2023-09-17 22:29:39 +02:00
if ( mediaMetadata . authors [ i ] . id ? . startsWith ( 'new' ) ) {
mediaMetadata . authors [ i ] . id = null
}
2023-08-18 21:40:36 +02:00
// Ensure the ID for the author exists
if ( mediaMetadata . authors [ i ] . id && ! ( await Database . checkAuthorExists ( libraryId , mediaMetadata . authors [ i ] . id ) ) ) {
Logger . warn ( ` [ApiRouter] Author id " ${ mediaMetadata . authors [ i ] . id } " does not exist ` )
mediaMetadata . authors [ i ] . id = null
}
2023-09-17 22:29:39 +02:00
if ( ! mediaMetadata . authors [ i ] . id ) {
2023-09-03 17:49:02 +02:00
let author = await Database . authorModel . getOldByNameAndLibrary ( authorName , libraryId )
2022-03-13 23:10:48 +01:00
if ( ! author ) {
author = new Author ( )
2023-07-08 16:57:32 +02:00
author . setData ( mediaMetadata . authors [ i ] , libraryId )
2022-03-18 01:10:47 +01:00
Logger . debug ( ` [ApiRouter] Created new author " ${ author . name } " ` )
2022-03-13 23:10:48 +01:00
newAuthors . push ( author )
2023-08-18 21:40:36 +02:00
// Update filter data
Database . addAuthorToFilterData ( libraryId , author . name , author . id )
2022-03-13 23:10:48 +01:00
}
// Update ID in original payload
mediaMetadata . authors [ i ] . id = author . id
}
}
if ( newAuthors . length ) {
2023-07-05 01:14:44 +02:00
await Database . createBulkAuthors ( newAuthors )
2022-12-22 23:26:11 +01:00
SocketAuthority . emitter ( 'authors_added' , newAuthors . map ( au => au . toJSON ( ) ) )
2022-03-13 23:10:48 +01:00
}
}
// Create new series if in payload
if ( mediaMetadata . series && mediaMetadata . series . length ) {
2022-11-24 23:35:26 +01:00
const newSeries = [ ]
2022-03-13 23:10:48 +01:00
for ( let i = 0 ; i < mediaMetadata . series . length ; i ++ ) {
2023-04-01 00:04:26 +02:00
const seriesName = ( mediaMetadata . series [ i ] . name || '' ) . trim ( )
if ( ! seriesName ) {
Logger . error ( ` [ApiRouter] Invalid series object, no name ` , mediaMetadata . series [ i ] )
continue
}
2023-09-17 22:29:39 +02:00
if ( mediaMetadata . series [ i ] . id ? . startsWith ( 'new' ) ) {
mediaMetadata . series [ i ] . id = null
}
2023-08-18 21:40:36 +02:00
// Ensure the ID for the series exists
if ( mediaMetadata . series [ i ] . id && ! ( await Database . checkSeriesExists ( libraryId , mediaMetadata . series [ i ] . id ) ) ) {
Logger . warn ( ` [ApiRouter] Series id " ${ mediaMetadata . series [ i ] . id } " does not exist ` )
mediaMetadata . series [ i ] . id = null
}
2023-09-17 22:29:39 +02:00
if ( ! mediaMetadata . series [ i ] . id ) {
2023-09-03 17:49:02 +02:00
let seriesItem = await Database . seriesModel . getOldByNameAndLibrary ( seriesName , libraryId )
2022-03-13 23:10:48 +01:00
if ( ! seriesItem ) {
seriesItem = new Series ( )
2023-07-08 16:57:32 +02:00
seriesItem . setData ( mediaMetadata . series [ i ] , libraryId )
2022-03-18 01:10:47 +01:00
Logger . debug ( ` [ApiRouter] Created new series " ${ seriesItem . name } " ` )
2022-03-13 23:10:48 +01:00
newSeries . push ( seriesItem )
2023-08-18 21:40:36 +02:00
// Update filter data
Database . addSeriesToFilterData ( libraryId , seriesItem . name , seriesItem . id )
2022-03-13 23:10:48 +01:00
}
// Update ID in original payload
mediaMetadata . series [ i ] . id = seriesItem . id
}
}
if ( newSeries . length ) {
2023-07-05 01:14:44 +02:00
await Database . createBulkSeries ( newSeries )
2022-12-22 23:26:11 +01:00
SocketAuthority . emitter ( 'multiple_series_added' , newSeries . map ( se => se . toJSON ( ) ) )
2022-03-13 23:10:48 +01:00
}
}
}
}
2021-08-18 00:01:11 +02:00
}
2023-02-27 03:56:07 +01:00
module . exports = ApiRouter