From 7e54577c5414dd91bfc6d891d428ec5dd38505af Mon Sep 17 00:00:00 2001 From: "The Estrada Lab (University of Michigan)" <56398660+estradalab@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:05:27 -0400 Subject: [PATCH 01/95] Added initial documentation for API Calls --- .gitignore | 3 +- docs/API_Calls.md | 383 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 docs/API_Calls.md diff --git a/.gitignore b/.gitignore index 456b7869..921d3375 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ riderModule.iml /.idea cover.jpg cover.png -/.vscode \ No newline at end of file +/.vscode +/.vs/ \ No newline at end of file diff --git a/docs/API_Calls.md b/docs/API_Calls.md new file mode 100644 index 00000000..59e10e64 --- /dev/null +++ b/docs/API_Calls.md @@ -0,0 +1,383 @@ +## Tranga API Calls +This document serves to outline all of the different HTTP API calls that Tranga accepts. Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them. +In the examples below, `{apiUri}` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. Parameters are included in the HTTP request URI and the request body is in JSON format. Tranga responses are always +in the JSON format within the Response Body. + +#### [GET] /Connectors +Retrieves the available manga sites (connectors) that Tranga is currently able to download manga from. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Jobs +Retrieves all jobs that Tranga is keeping track of, includes Running Jobs, Waiting Jobs, Manga Tracking (Monitoring) Jobs. + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /Jobs +Removes the specified job given by the job ID + +- Request Variables: + - None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [POST] /Jobs/Cancel +Cancels a running job or prevents a queued job from running. + +- Parameters: + None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [POST] /Jobs/DownloadNewChapters +Manually adds a Job to Tranga's queue to check for and download new chapters for a specified manga + +- Parameters: + None + +- Request Body: + ``` + { + connector: ${Manga Connector to Download From} + internalId: ${Tranga Manga ID} + translatedLanguage: ${Manga Language} + } + ``` + +#### [GET] /Jobs/Running +Retrieves all currently running jobs. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Jobs/StartNow +Manually starts a configured job +- Parameters: + None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [GET]/Jobs/Waiting +Retrieves all currently queued jobs. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Jobs/MonitorJobs +Retrieves all jobs for Mangas that Tranga is currently tracking. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Jobs/MonitorManga +Adds a new manga for Tranga to monitor + +- Parameters: + None + +- Request Body: + ``` + { + connector: ${Manga Connector to download from} + internalId: ${Tranga Manga ID} + interval: ${Interval at which to run job, in the HH:MM:SS format} + translatedLanguage: ${Supported language code} + ignoreBelowChapterNum: ${Chapter number to start downloading from} + customFolderName: ${Folder Name to save Manga to} + } + ``` + +#### [GET] /Jobs/Progress +Retrieves the current completion progress of a running or waiting job. Tranga's ID for the Job is returned with each of the `GET /Job/` API calls. + +- Parameters: + - `{jobId}`: Tranga Job ID + +- Request Body: + None + +#### [POST] /Jobs/UpdateMetadata +Updates the metadata for all monitored mangas + +- Parameters: + None + +- Request Body: + None + +#### [GET] /LibraryConnectors +Retrieves the currently configured library servers + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /LibraryConnectors/Reset +Resets or clears a configured library connector + +- Parameters: + None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + } + ``` + +#### [POST] /LibraryConnectors/Test +Verifies the behavior of a library connector before saving it. The connector must be checked to verify that the connection is active. + + +- Parameters: + None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + libraryURL: ${Library URL} + komgaAuth: Only for when libraryConnector = Komga + kavitaUsername: Only for when libraryConnector = Kavita + kavitaPassword: Only for when libraryConnector = Kavita + } + ``` + +#### [GET] /LibraryConnectors/Types +Retrives Key-Value pairs for all of Tranga's currently supported library servers. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /LibraryConnectors/Update +Updates or Adds a Library Connector to Tranga + +- Parameters: None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + libraryURL: ${Library URL} + komgaAuth: Only for when libraryConnector = Komga + kavitaUsername: Only for when libraryConnector = Kavita + kavitaPassword: Only for when libraryConnector = Kavita + } + ``` + +#### [GET] /LogFile +Retrieves the log file from the running Tranga instance + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Manga/FromConnector +Retrieves the details about a specified manga from a specific connector. If the manga title returned by Tranga is a URL (determined by the presence of `http` in the title, the API call should use the second +call with the `url` rather than the `title`. + +- Parameters: + - `{connector}`: Manga Connector + - `{url/title}`: Manga URL/Title + +- Request Body: + None + +#### [GET] /Manga/Chapters +Retrieves the currently available chapters for a specified manga from a connector. The `{internalId}` is how Tranga uniquely recognizes and distinguishes different Manga. + +- Parameters: + - `{connector}`: Manga Connector + - `{internalId}`: Tranga Manga ID + - `{translatedLanguage}`: Translated Language + +- Request Body: + None + +#### [GET] /Manga/Cover +Retrives the URL of the cover image for a specific manga that Tranga is tracking. + +- Parameters: + - `{internalId}`: Tranga Manga ID + +- Request Body: + None + +#### [GET] /NotificationConnectors +Retrieves the currently configured notification providers + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /NotificationConnectors/Reset +Resets or clears a configured notification connector + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + } + ``` + +#### [POST] /NotificationConnectors/Test +Tests a notification connector with the currently input settings. The connector behavior must be checked to verify that the input settings are correct. + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + + gotifyUrl: + gotifyAppToken: + + lunaseaWebhook: + + ntfyUrl: + ntfyAuth: + } + ``` + +#### [POST] /NotificationConnectors/Update +Updates or Adds a notification connector to Tranga + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + + gotifyUrl: + gotifyAppToken: + + lunaseaWebhook: + + ntfyUrl: + ntfyAuth: + } + ``` + +#### [GET] /NotificationConnectors/Types +Retrives Key-Value pairs for all of Tranga's currently supported notification providers. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Ping +This call is used periodically by the web frontend to establish that connection to the server is active. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Settings +Retrieves the content of Tranga's `settings.json` + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Settings/customRequestLimit +Retrieves the configured rate limits for different types of manga connector requests. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Settings/customRequestLimit +Sets the rate limits for different types of manga connector requests. + +- Parameters: + None + +- Request Body: + ``` + { + requestType: {Request Byte} + requestsPerMinute: {Rate Limit in Requests Per Minute} + } + ``` + +#### [POST] /Settings/UpdateDownloadLocation +Updates the root directory of where Tranga downloads manga + +- Parameters: + None + +- Request Body: + ``` + { + downloadLocation: {New Root Directory} + moveFiles: "true"/"false" + } + ``` +#### [POST] /Settings/userAgent +Updates the user agent that Tranga uses when scraping the web + +- Parameters + +- Request Body: + ``` + { + userAgent: {User Agent String} + } + ``` + From dd965d886a683a83a1ba625876c7b268228c7618 Mon Sep 17 00:00:00 2001 From: Dity <125827669+db-2001@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:11:03 -0400 Subject: [PATCH 02/95] Revert "Added initial documentation for API Calls" This reverts commit 7e54577c5414dd91bfc6d891d428ec5dd38505af. --- .gitignore | 3 +- docs/API_Calls.md | 383 ---------------------------------------------- 2 files changed, 1 insertion(+), 385 deletions(-) delete mode 100644 docs/API_Calls.md diff --git a/.gitignore b/.gitignore index 921d3375..456b7869 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,4 @@ riderModule.iml /.idea cover.jpg cover.png -/.vscode -/.vs/ \ No newline at end of file +/.vscode \ No newline at end of file diff --git a/docs/API_Calls.md b/docs/API_Calls.md deleted file mode 100644 index 59e10e64..00000000 --- a/docs/API_Calls.md +++ /dev/null @@ -1,383 +0,0 @@ -## Tranga API Calls -This document serves to outline all of the different HTTP API calls that Tranga accepts. Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them. -In the examples below, `{apiUri}` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. Parameters are included in the HTTP request URI and the request body is in JSON format. Tranga responses are always -in the JSON format within the Response Body. - -#### [GET] /Connectors -Retrieves the available manga sites (connectors) that Tranga is currently able to download manga from. - -- Parameters: - None - -- Request Body: - None - -#### [GET] /Jobs -Retrieves all jobs that Tranga is keeping track of, includes Running Jobs, Waiting Jobs, Manga Tracking (Monitoring) Jobs. - -- Parameters: - None - -- Request Body: - None - -#### [DELETE] /Jobs -Removes the specified job given by the job ID - -- Request Variables: - - None - -- Request Body: - ``` - { - jobId: ${Tranga Job ID} - } - ``` - -#### [POST] /Jobs/Cancel -Cancels a running job or prevents a queued job from running. - -- Parameters: - None - -- Request Body: - ``` - { - jobId: ${Tranga Job ID} - } - ``` - -#### [POST] /Jobs/DownloadNewChapters -Manually adds a Job to Tranga's queue to check for and download new chapters for a specified manga - -- Parameters: - None - -- Request Body: - ``` - { - connector: ${Manga Connector to Download From} - internalId: ${Tranga Manga ID} - translatedLanguage: ${Manga Language} - } - ``` - -#### [GET] /Jobs/Running -Retrieves all currently running jobs. - -- Parameters: - None - -- Request Body: - None - -#### [POST] /Jobs/StartNow -Manually starts a configured job -- Parameters: - None - -- Request Body: - ``` - { - jobId: ${Tranga Job ID} - } - ``` - -#### [GET]/Jobs/Waiting -Retrieves all currently queued jobs. - -- Parameters: - None - -- Request Body: - None - -#### [GET] /Jobs/MonitorJobs -Retrieves all jobs for Mangas that Tranga is currently tracking. - -- Parameters: - None - -- Request Body: - None - -#### [POST] /Jobs/MonitorManga -Adds a new manga for Tranga to monitor - -- Parameters: - None - -- Request Body: - ``` - { - connector: ${Manga Connector to download from} - internalId: ${Tranga Manga ID} - interval: ${Interval at which to run job, in the HH:MM:SS format} - translatedLanguage: ${Supported language code} - ignoreBelowChapterNum: ${Chapter number to start downloading from} - customFolderName: ${Folder Name to save Manga to} - } - ``` - -#### [GET] /Jobs/Progress -Retrieves the current completion progress of a running or waiting job. Tranga's ID for the Job is returned with each of the `GET /Job/` API calls. - -- Parameters: - - `{jobId}`: Tranga Job ID - -- Request Body: - None - -#### [POST] /Jobs/UpdateMetadata -Updates the metadata for all monitored mangas - -- Parameters: - None - -- Request Body: - None - -#### [GET] /LibraryConnectors -Retrieves the currently configured library servers - -- Parameters: - None - -- Request Body: - None - -#### [DELETE] /LibraryConnectors/Reset -Resets or clears a configured library connector - -- Parameters: - None - -- Request Body: - ``` - { - libraryConnector: Komga/Kavita - } - ``` - -#### [POST] /LibraryConnectors/Test -Verifies the behavior of a library connector before saving it. The connector must be checked to verify that the connection is active. - - -- Parameters: - None - -- Request Body: - ``` - { - libraryConnector: Komga/Kavita - libraryURL: ${Library URL} - komgaAuth: Only for when libraryConnector = Komga - kavitaUsername: Only for when libraryConnector = Kavita - kavitaPassword: Only for when libraryConnector = Kavita - } - ``` - -#### [GET] /LibraryConnectors/Types -Retrives Key-Value pairs for all of Tranga's currently supported library servers. - -- Parameters: - None - -- Request Body: - None - -#### [POST] /LibraryConnectors/Update -Updates or Adds a Library Connector to Tranga - -- Parameters: None - -- Request Body: - ``` - { - libraryConnector: Komga/Kavita - libraryURL: ${Library URL} - komgaAuth: Only for when libraryConnector = Komga - kavitaUsername: Only for when libraryConnector = Kavita - kavitaPassword: Only for when libraryConnector = Kavita - } - ``` - -#### [GET] /LogFile -Retrieves the log file from the running Tranga instance - -- Parameters: - None - -- Request Body: - None - -#### [GET] /Manga/FromConnector -Retrieves the details about a specified manga from a specific connector. If the manga title returned by Tranga is a URL (determined by the presence of `http` in the title, the API call should use the second -call with the `url` rather than the `title`. - -- Parameters: - - `{connector}`: Manga Connector - - `{url/title}`: Manga URL/Title - -- Request Body: - None - -#### [GET] /Manga/Chapters -Retrieves the currently available chapters for a specified manga from a connector. The `{internalId}` is how Tranga uniquely recognizes and distinguishes different Manga. - -- Parameters: - - `{connector}`: Manga Connector - - `{internalId}`: Tranga Manga ID - - `{translatedLanguage}`: Translated Language - -- Request Body: - None - -#### [GET] /Manga/Cover -Retrives the URL of the cover image for a specific manga that Tranga is tracking. - -- Parameters: - - `{internalId}`: Tranga Manga ID - -- Request Body: - None - -#### [GET] /NotificationConnectors -Retrieves the currently configured notification providers - -- Parameters: - None - -- Request Body: - None - -#### [DELETE] /NotificationConnectors/Reset -Resets or clears a configured notification connector - -- Parameters: - None - -- Request Body: - ``` - { - notificationConnector: Gotify/Ntfy/LunaSea - } - ``` - -#### [POST] /NotificationConnectors/Test -Tests a notification connector with the currently input settings. The connector behavior must be checked to verify that the input settings are correct. - -- Parameters: - None - -- Request Body: - ``` - { - notificationConnector: Gotify/Ntfy/LunaSea - - gotifyUrl: - gotifyAppToken: - - lunaseaWebhook: - - ntfyUrl: - ntfyAuth: - } - ``` - -#### [POST] /NotificationConnectors/Update -Updates or Adds a notification connector to Tranga - -- Parameters: - None - -- Request Body: - ``` - { - notificationConnector: Gotify/Ntfy/LunaSea - - gotifyUrl: - gotifyAppToken: - - lunaseaWebhook: - - ntfyUrl: - ntfyAuth: - } - ``` - -#### [GET] /NotificationConnectors/Types -Retrives Key-Value pairs for all of Tranga's currently supported notification providers. - -- Parameters: - None - -- Request Body: - None - -#### [GET] /Ping -This call is used periodically by the web frontend to establish that connection to the server is active. - -- Parameters: - None - -- Request Body: - None - -#### [GET] /Settings -Retrieves the content of Tranga's `settings.json` - -- Parameters: - None - -- Request Body: - None - -#### [GET] /Settings/customRequestLimit -Retrieves the configured rate limits for different types of manga connector requests. - -- Parameters: - None - -- Request Body: - None - -#### [POST] /Settings/customRequestLimit -Sets the rate limits for different types of manga connector requests. - -- Parameters: - None - -- Request Body: - ``` - { - requestType: {Request Byte} - requestsPerMinute: {Rate Limit in Requests Per Minute} - } - ``` - -#### [POST] /Settings/UpdateDownloadLocation -Updates the root directory of where Tranga downloads manga - -- Parameters: - None - -- Request Body: - ``` - { - downloadLocation: {New Root Directory} - moveFiles: "true"/"false" - } - ``` -#### [POST] /Settings/userAgent -Updates the user agent that Tranga uses when scraping the web - -- Parameters - -- Request Body: - ``` - { - userAgent: {User Agent String} - } - ``` - From 7628510b874d272b5cd8e23e660288bf75e4d173 Mon Sep 17 00:00:00 2001 From: Dity <125827669+db-2001@users.noreply.github.com> Date: Sat, 6 Apr 2024 18:13:31 -0400 Subject: [PATCH 03/95] Documentation for API Calls --- .gitignore | 4 +- docs/API_Calls.md | 383 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 docs/API_Calls.md diff --git a/.gitignore b/.gitignore index 456b7869..21f7aed1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,6 @@ riderModule.iml /.idea cover.jpg cover.png -/.vscode \ No newline at end of file +/.vscode +/.vs/ +Tranga/Properties/launchSettings.json \ No newline at end of file diff --git a/docs/API_Calls.md b/docs/API_Calls.md new file mode 100644 index 00000000..59e10e64 --- /dev/null +++ b/docs/API_Calls.md @@ -0,0 +1,383 @@ +## Tranga API Calls +This document serves to outline all of the different HTTP API calls that Tranga accepts. Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them. +In the examples below, `{apiUri}` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. Parameters are included in the HTTP request URI and the request body is in JSON format. Tranga responses are always +in the JSON format within the Response Body. + +#### [GET] /Connectors +Retrieves the available manga sites (connectors) that Tranga is currently able to download manga from. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Jobs +Retrieves all jobs that Tranga is keeping track of, includes Running Jobs, Waiting Jobs, Manga Tracking (Monitoring) Jobs. + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /Jobs +Removes the specified job given by the job ID + +- Request Variables: + - None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [POST] /Jobs/Cancel +Cancels a running job or prevents a queued job from running. + +- Parameters: + None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [POST] /Jobs/DownloadNewChapters +Manually adds a Job to Tranga's queue to check for and download new chapters for a specified manga + +- Parameters: + None + +- Request Body: + ``` + { + connector: ${Manga Connector to Download From} + internalId: ${Tranga Manga ID} + translatedLanguage: ${Manga Language} + } + ``` + +#### [GET] /Jobs/Running +Retrieves all currently running jobs. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Jobs/StartNow +Manually starts a configured job +- Parameters: + None + +- Request Body: + ``` + { + jobId: ${Tranga Job ID} + } + ``` + +#### [GET]/Jobs/Waiting +Retrieves all currently queued jobs. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Jobs/MonitorJobs +Retrieves all jobs for Mangas that Tranga is currently tracking. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Jobs/MonitorManga +Adds a new manga for Tranga to monitor + +- Parameters: + None + +- Request Body: + ``` + { + connector: ${Manga Connector to download from} + internalId: ${Tranga Manga ID} + interval: ${Interval at which to run job, in the HH:MM:SS format} + translatedLanguage: ${Supported language code} + ignoreBelowChapterNum: ${Chapter number to start downloading from} + customFolderName: ${Folder Name to save Manga to} + } + ``` + +#### [GET] /Jobs/Progress +Retrieves the current completion progress of a running or waiting job. Tranga's ID for the Job is returned with each of the `GET /Job/` API calls. + +- Parameters: + - `{jobId}`: Tranga Job ID + +- Request Body: + None + +#### [POST] /Jobs/UpdateMetadata +Updates the metadata for all monitored mangas + +- Parameters: + None + +- Request Body: + None + +#### [GET] /LibraryConnectors +Retrieves the currently configured library servers + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /LibraryConnectors/Reset +Resets or clears a configured library connector + +- Parameters: + None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + } + ``` + +#### [POST] /LibraryConnectors/Test +Verifies the behavior of a library connector before saving it. The connector must be checked to verify that the connection is active. + + +- Parameters: + None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + libraryURL: ${Library URL} + komgaAuth: Only for when libraryConnector = Komga + kavitaUsername: Only for when libraryConnector = Kavita + kavitaPassword: Only for when libraryConnector = Kavita + } + ``` + +#### [GET] /LibraryConnectors/Types +Retrives Key-Value pairs for all of Tranga's currently supported library servers. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /LibraryConnectors/Update +Updates or Adds a Library Connector to Tranga + +- Parameters: None + +- Request Body: + ``` + { + libraryConnector: Komga/Kavita + libraryURL: ${Library URL} + komgaAuth: Only for when libraryConnector = Komga + kavitaUsername: Only for when libraryConnector = Kavita + kavitaPassword: Only for when libraryConnector = Kavita + } + ``` + +#### [GET] /LogFile +Retrieves the log file from the running Tranga instance + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Manga/FromConnector +Retrieves the details about a specified manga from a specific connector. If the manga title returned by Tranga is a URL (determined by the presence of `http` in the title, the API call should use the second +call with the `url` rather than the `title`. + +- Parameters: + - `{connector}`: Manga Connector + - `{url/title}`: Manga URL/Title + +- Request Body: + None + +#### [GET] /Manga/Chapters +Retrieves the currently available chapters for a specified manga from a connector. The `{internalId}` is how Tranga uniquely recognizes and distinguishes different Manga. + +- Parameters: + - `{connector}`: Manga Connector + - `{internalId}`: Tranga Manga ID + - `{translatedLanguage}`: Translated Language + +- Request Body: + None + +#### [GET] /Manga/Cover +Retrives the URL of the cover image for a specific manga that Tranga is tracking. + +- Parameters: + - `{internalId}`: Tranga Manga ID + +- Request Body: + None + +#### [GET] /NotificationConnectors +Retrieves the currently configured notification providers + +- Parameters: + None + +- Request Body: + None + +#### [DELETE] /NotificationConnectors/Reset +Resets or clears a configured notification connector + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + } + ``` + +#### [POST] /NotificationConnectors/Test +Tests a notification connector with the currently input settings. The connector behavior must be checked to verify that the input settings are correct. + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + + gotifyUrl: + gotifyAppToken: + + lunaseaWebhook: + + ntfyUrl: + ntfyAuth: + } + ``` + +#### [POST] /NotificationConnectors/Update +Updates or Adds a notification connector to Tranga + +- Parameters: + None + +- Request Body: + ``` + { + notificationConnector: Gotify/Ntfy/LunaSea + + gotifyUrl: + gotifyAppToken: + + lunaseaWebhook: + + ntfyUrl: + ntfyAuth: + } + ``` + +#### [GET] /NotificationConnectors/Types +Retrives Key-Value pairs for all of Tranga's currently supported notification providers. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Ping +This call is used periodically by the web frontend to establish that connection to the server is active. + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Settings +Retrieves the content of Tranga's `settings.json` + +- Parameters: + None + +- Request Body: + None + +#### [GET] /Settings/customRequestLimit +Retrieves the configured rate limits for different types of manga connector requests. + +- Parameters: + None + +- Request Body: + None + +#### [POST] /Settings/customRequestLimit +Sets the rate limits for different types of manga connector requests. + +- Parameters: + None + +- Request Body: + ``` + { + requestType: {Request Byte} + requestsPerMinute: {Rate Limit in Requests Per Minute} + } + ``` + +#### [POST] /Settings/UpdateDownloadLocation +Updates the root directory of where Tranga downloads manga + +- Parameters: + None + +- Request Body: + ``` + { + downloadLocation: {New Root Directory} + moveFiles: "true"/"false" + } + ``` +#### [POST] /Settings/userAgent +Updates the user agent that Tranga uses when scraping the web + +- Parameters + +- Request Body: + ``` + { + userAgent: {User Agent String} + } + ``` + From 6f5fb7e0bb747c6980b93d2354f1d53af1a29d66 Mon Sep 17 00:00:00 2001 From: Dity <125827669+db-2001@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:20:28 -0400 Subject: [PATCH 04/95] API rewrite to parse JSON body for POST and DELETE --- Tranga/Server.cs | 160 ++++++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 66 deletions(-) diff --git a/Tranga/Server.cs b/Tranga/Server.cs index cf2d15a8..8adaecc0 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -12,17 +12,17 @@ namespace Tranga; public class Server : GlobalBase { - private readonly HttpListener _listener = new (); + private readonly HttpListener _listener = new(); private readonly Tranga _parent; - + public Server(Tranga parent) : base(parent) { this._parent = parent; - if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); else this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/"); - Thread listenThread = new (Listen); + Thread listenThread = new(Listen); listenThread.Start(); Thread watchThread = new(WatchRunning); watchThread.Start(); @@ -30,7 +30,7 @@ public Server(Tranga parent) : base(parent) private void WatchRunning() { - while(_parent.keepRunning) + while (_parent.keepRunning) Thread.Sleep(1000); this._listener.Close(); } @@ -38,7 +38,7 @@ private void WatchRunning() private void Listen() { this._listener.Start(); - foreach(string prefix in this._listener.Prefixes) + foreach (string prefix in this._listener.Prefixes) Log($"Listening on {prefix}"); while (this._listener.IsListening && _parent.keepRunning) { @@ -54,7 +54,7 @@ private void Listen() } catch (HttpListenerException) { - + } } } @@ -63,9 +63,9 @@ private void HandleRequest(HttpListenerContext context) { HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; - if(request.HttpMethod == "OPTIONS") + if (request.HttpMethod == "OPTIONS") SendResponse(HttpStatusCode.OK, context.Response); - if(request.Url!.LocalPath.Contains("favicon")) + if (request.Url!.LocalPath.Contains("favicon")) SendResponse(HttpStatusCode.NoContent, response); switch (request.HttpMethod) @@ -79,16 +79,16 @@ private void HandleRequest(HttpListenerContext context) case "DELETE": HandleDelete(request, response); break; - default: + default: SendResponse(HttpStatusCode.BadRequest, response); break; } } - + private Dictionary GetRequestVariables(string query) { Dictionary ret = new(); - Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*"); + Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*"); if (!queryRex.IsMatch(query)) return ret; query = query.Substring(1); @@ -102,6 +102,22 @@ private Dictionary GetRequestVariables(string query) return ret; } + private Dictionary GetRequestBody(HttpListenerRequest request) + { + if (!request.HasEntityBody) + { + Log("No request body"); + Dictionary emptyBody = new(); + return emptyBody; + } + Stream body = request.InputStream; + Encoding encoding = request.ContentEncoding; + StreamReader reader = new StreamReader(body, encoding); + string s = reader.ReadToEnd(); + Dictionary requestBody = JsonConvert.DeserializeObject>(s); + return requestBody; + } + private void HandleGet(HttpListenerRequest request, HttpListenerResponse response) { Dictionary requestVariables = GetRequestVariables(request.Url!.Query); @@ -265,7 +281,13 @@ private void HandleGet(HttpListenerRequest request, HttpListenerResponse respons private void HandlePost(HttpListenerRequest request, HttpListenerResponse response) { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); + Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI + Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body + Dictionary requestParams = new(); //The actual variable used for the API + + //Concatenate the two dictionaries for compatibility with older versions of front-ends + requestParams = requestVariables.Concat(requestBody).ToDictionary(x => x.Key, x => x.Value); + string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage, notificationConnectorStr, libraryConnectorStr; MangaConnector? connector; Manga? tmpManga; @@ -277,7 +299,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon switch (path) { case "Manga": - if(!requestVariables.TryGetValue("internalId", out internalId) || + if(!requestParams.TryGetValue("internalId", out internalId) || !_parent.TryGetPublicationById(internalId, out tmpManga)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -287,9 +309,9 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.OK, response, manga); break; case "Jobs/MonitorManga": - if(!requestVariables.TryGetValue("connector", out connectorName) || - !requestVariables.TryGetValue("internalId", out internalId) || - !requestVariables.TryGetValue("interval", out string? intervalStr) || + if(!requestParams.TryGetValue("connector", out connectorName) || + !requestParams.TryGetValue("internalId", out internalId) || + !requestParams.TryGetValue("interval", out string? intervalStr) || !_parent.TryGetConnector(connectorName, out connector)|| !_parent.TryGetPublicationById(internalId, out tmpManga) || !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) @@ -300,7 +322,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon manga = (Manga)tmpManga!; - if (requestVariables.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) + if (requestParams.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) { if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum)) { @@ -310,16 +332,16 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon manga.ignoreChaptersBelow = chapterNum; } - if (requestVariables.TryGetValue("customFolderName", out customFolderName)) + if (requestParams.TryGetValue("customFolderName", out customFolderName)) manga.MovePublicationFolder(settings.downloadLocation, customFolderName); - requestVariables.TryGetValue("translatedLanguage", out translatedLanguage); + requestParams.TryGetValue("translatedLanguage", out translatedLanguage); _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, true, interval, translatedLanguage: translatedLanguage??"en")); SendResponse(HttpStatusCode.Accepted, response); break; case "Jobs/DownloadNewChapters": - if(!requestVariables.TryGetValue("connector", out connectorName) || - !requestVariables.TryGetValue("internalId", out internalId) || + if(!requestParams.TryGetValue("connector", out connectorName) || + !requestParams.TryGetValue("internalId", out internalId) || !_parent.TryGetConnector(connectorName, out connector)|| !_parent.TryGetPublicationById(internalId, out tmpManga)) { @@ -329,7 +351,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon manga = (Manga)tmpManga!; - if (requestVariables.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) + if (requestParams.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) { if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum)) { @@ -339,15 +361,15 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon manga.ignoreChaptersBelow = chapterNum; } - if (requestVariables.TryGetValue("customFolderName", out customFolderName)) + if (requestParams.TryGetValue("customFolderName", out customFolderName)) manga.MovePublicationFolder(settings.downloadLocation, customFolderName); - requestVariables.TryGetValue("translatedLanguage", out translatedLanguage); + requestParams.TryGetValue("translatedLanguage", out translatedLanguage); _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, false, translatedLanguage: translatedLanguage??"en")); SendResponse(HttpStatusCode.Accepted, response); break; case "Jobs/UpdateMetadata": - if (!requestVariables.TryGetValue("internalId", out internalId)) + if (!requestParams.TryGetValue("internalId", out internalId)) { foreach (Job pJob in _parent.jobBoss.jobs.Where(possibleDncJob => possibleDncJob.jobType is Job.JobType.DownloadNewChaptersJob).ToArray())//ToArray to avoid modyifying while adding new jobs @@ -375,7 +397,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon } break; case "Jobs/StartNow": - if (!requestVariables.TryGetValue("jobId", out jobId) || + if (!requestParams.TryGetValue("jobId", out jobId) || !_parent.jobBoss.TryGetJobById(jobId, out job)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -385,7 +407,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "Jobs/Cancel": - if (!requestVariables.TryGetValue("jobId", out jobId) || + if (!requestParams.TryGetValue("jobId", out jobId) || !_parent.jobBoss.TryGetJobById(jobId, out job)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -395,8 +417,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "Settings/UpdateDownloadLocation": - if (!requestVariables.TryGetValue("downloadLocation", out string? downloadLocation) || - !requestVariables.TryGetValue("moveFiles", out string? moveFilesStr) || + if (!requestParams.TryGetValue("downloadLocation", out string? downloadLocation) || + !requestParams.TryGetValue("moveFiles", out string? moveFilesStr) || !Boolean.TryParse(moveFilesStr, out bool moveFiles)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -406,7 +428,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; /*case "Settings/UpdateWorkingDirectory": - if (!requestVariables.TryGetValue("workingDirectory", out string? workingDirectory)) + if (!requestParams.TryGetValue("workingDirectory", out string? workingDirectory)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -415,7 +437,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break;*/ case "Settings/userAgent": - if(!requestVariables.TryGetValue("userAgent", out string? customUserAgent)) + if(!requestParams.TryGetValue("userAgent", out string? customUserAgent)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -428,8 +450,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "Settings/customRequestLimit": - if (!requestVariables.TryGetValue("requestType", out string? requestTypeStr) || - !requestVariables.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) || + if (!requestParams.TryGetValue("requestType", out string? requestTypeStr) || + !requestParams.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) || !Enum.TryParse(requestTypeStr, out RequestType requestType) || !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute)) { @@ -450,7 +472,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon settings.ExportSettings(); break; case "NotificationConnectors/Update": - if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) || + if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -459,8 +481,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) { - if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) || - !requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) + if (!requestParams.TryGetValue("gotifyUrl", out string? gotifyUrl) || + !requestParams.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -469,7 +491,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) { - if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) + if (!requestParams.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -478,8 +500,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) { - if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) || - !requestVariables.TryGetValue("ntfyAuth", out string? ntfyAuth)) + if (!requestParams.TryGetValue("ntfyUrl", out string? ntfyUrl) || + !requestParams.TryGetValue("ntfyAuth", out string? ntfyAuth)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -494,7 +516,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon break; case "NotificationConnectors/Test": NotificationConnector notificationConnector; - if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) || + if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -503,8 +525,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) { - if (!requestVariables.TryGetValue("gotifyUrl", out string? gotifyUrl) || - !requestVariables.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) + if (!requestParams.TryGetValue("gotifyUrl", out string? gotifyUrl) || + !requestParams.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -512,7 +534,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon notificationConnector = new Gotify(this, gotifyUrl, gotifyAppToken); }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) { - if (!requestVariables.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) + if (!requestParams.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -520,8 +542,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon notificationConnector = new LunaSea(this, lunaseaWebhook); }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) { - if (!requestVariables.TryGetValue("ntfyUrl", out string? ntfyUrl) || - !requestVariables.TryGetValue("ntfyAuth", out string? ntfyAuth)) + if (!requestParams.TryGetValue("ntfyUrl", out string? ntfyUrl) || + !requestParams.TryGetValue("ntfyAuth", out string? ntfyAuth)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -538,7 +560,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "NotificationConnectors/Reset": - if (!requestVariables.TryGetValue("notificationConnector", out notificationConnectorStr) || + if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -548,7 +570,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "LibraryConnectors/Update": - if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) || + if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -557,9 +579,9 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) { - if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) || - !requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) || - !requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword)) + if (!requestParams.TryGetValue("kavitaUrl", out string? kavitaUrl) || + !requestParams.TryGetValue("kavitaUsername", out string? kavitaUsername) || + !requestParams.TryGetValue("kavitaPassword", out string? kavitaPassword)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -568,8 +590,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) { - if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) || - !requestVariables.TryGetValue("komgaAuth", out string? komgaAuth)) + if (!requestParams.TryGetValue("komgaUrl", out string? komgaUrl) || + !requestParams.TryGetValue("komgaAuth", out string? komgaAuth)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -584,7 +606,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon break; case "LibraryConnectors/Test": LibraryConnector libraryConnector; - if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) || + if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -593,9 +615,9 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) { - if (!requestVariables.TryGetValue("kavitaUrl", out string? kavitaUrl) || - !requestVariables.TryGetValue("kavitaUsername", out string? kavitaUsername) || - !requestVariables.TryGetValue("kavitaPassword", out string? kavitaPassword)) + if (!requestParams.TryGetValue("kavitaUrl", out string? kavitaUrl) || + !requestParams.TryGetValue("kavitaUsername", out string? kavitaUsername) || + !requestParams.TryGetValue("kavitaPassword", out string? kavitaPassword)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -603,8 +625,8 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon libraryConnector = new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword); }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) { - if (!requestVariables.TryGetValue("komgaUrl", out string? komgaUrl) || - !requestVariables.TryGetValue("komgaAuth", out string? komgaAuth)) + if (!requestParams.TryGetValue("komgaUrl", out string? komgaUrl) || + !requestParams.TryGetValue("komgaAuth", out string? komgaAuth)) { SendResponse(HttpStatusCode.BadRequest, response); break; @@ -620,7 +642,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "LibraryConnectors/Reset": - if (!requestVariables.TryGetValue("libraryConnector", out libraryConnectorStr) || + if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -637,7 +659,13 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response) { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); + Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI + Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body + Dictionary requestParams = new(); //The actual variable used for the API + + //Concatenate the two dictionaries for compatibility with older versions of front-ends + requestParams = requestVariables.Concat(requestBody).ToDictionary(x => x.Key, x => x.Value); + string? connectorName, internalId; MangaConnector connector; Manga manga; @@ -645,7 +673,7 @@ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse resp switch (path) { case "Jobs": - if (!requestVariables.TryGetValue("jobId", out string? jobId) || + if (!requestParams.TryGetValue("jobId", out string? jobId) || !_parent.jobBoss.TryGetJobById(jobId, out Job? job)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -655,8 +683,8 @@ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse resp SendResponse(HttpStatusCode.Accepted, response); break; case "Jobs/DownloadNewChapters": - if(!requestVariables.TryGetValue("connector", out connectorName) || - !requestVariables.TryGetValue("internalId", out internalId) || + if(!requestParams.TryGetValue("connector", out connectorName) || + !requestParams.TryGetValue("internalId", out internalId) || _parent.GetConnector(connectorName) is null || _parent.GetPublicationById(internalId) is null) { @@ -669,7 +697,7 @@ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse resp SendResponse(HttpStatusCode.Accepted, response); break; case "NotificationConnectors": - if (!requestVariables.TryGetValue("notificationConnector", out string? notificationConnectorStr) || + if (!requestParams.TryGetValue("notificationConnector", out string? notificationConnectorStr) || !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType)) { SendResponse(HttpStatusCode.BadRequest, response); @@ -679,7 +707,7 @@ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse resp SendResponse(HttpStatusCode.Accepted, response); break; case "LibraryConnectors": - if (!requestVariables.TryGetValue("libraryConnectors", out string? libraryConnectorStr) || + if (!requestParams.TryGetValue("libraryConnector", out string? libraryConnectorStr) || !Enum.TryParse(libraryConnectorStr, out LibraryConnector.LibraryType libraryConnectoryType)) { From 33b8ede4927c4008d3724bc9f574c0e9f6f1118b Mon Sep 17 00:00:00 2001 From: db-2001 Date: Thu, 18 Apr 2024 17:58:23 -0400 Subject: [PATCH 05/95] Use new requestParams variable for AprilFoolsMode setting --- Tranga/Server.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server.cs b/Tranga/Server.cs index 0aa2ca94..e7e55c8b 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -431,7 +431,7 @@ private void HandlePost(HttpListenerRequest request, HttpListenerResponse respon SendResponse(HttpStatusCode.Accepted, response); break; case "Settings/AprilFoolsMode": - if (!requestVariables.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) || + if (!requestParams.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) || bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled)) { SendResponse(HttpStatusCode.BadRequest, response); From 01bab621905a5115045ce49fccd15f7548090010 Mon Sep 17 00:00:00 2001 From: db-2001 Date: Thu, 18 Apr 2024 18:32:49 -0400 Subject: [PATCH 06/95] Log request if unknown --- Tranga/Server.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tranga/Server.cs b/Tranga/Server.cs index e7e55c8b..e7c0d41d 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -731,6 +731,9 @@ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse resp SendResponse(HttpStatusCode.Accepted, response); break; default: + Log("Invalid Request:"); + Log(request.Url!.Query); + Log(requestParams); SendResponse(HttpStatusCode.BadRequest, response); break; } From a8aa7d33707eacc55e2f4b9e31563d6514c4ead8 Mon Sep 17 00:00:00 2001 From: db-2001 Date: Thu, 18 Apr 2024 18:45:19 -0400 Subject: [PATCH 07/95] Okay, actually write request variables to log. --- Tranga/Server.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tranga/Server.cs b/Tranga/Server.cs index e7c0d41d..923680c2 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -733,7 +733,10 @@ private void HandleDelete(HttpListenerRequest request, HttpListenerResponse resp default: Log("Invalid Request:"); Log(request.Url!.Query); - Log(requestParams); + foreach (KeyValuePair kvp in requestParams) + { + Log("Request variable = {0}, Variable Value = {1}", kvp.Key, kvp.Value); + } SendResponse(HttpStatusCode.BadRequest, response); break; } From 7e5fa6ce41aa7d1404110d7895ee338476c17b17 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 21:23:15 +0200 Subject: [PATCH 08/95] API v2 --- Tranga/Server.cs | 8 +- Tranga/ServerV2.cs | 80 ++++ docs/API_Calls_v2.md | 895 +++++++++++++++++++++++++++++++++++++++++++ docs/Types.md | 41 ++ 4 files changed, 1023 insertions(+), 1 deletion(-) create mode 100644 Tranga/ServerV2.cs create mode 100644 docs/API_Calls_v2.md create mode 100644 docs/Types.md diff --git a/Tranga/Server.cs b/Tranga/Server.cs index 9b99c7a5..8a085f9f 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -10,7 +10,7 @@ namespace Tranga; -public class Server : GlobalBase +public partial class Server : GlobalBase { private readonly HttpListener _listener = new (); private readonly Tranga _parent; @@ -68,6 +68,12 @@ private void HandleRequest(HttpListenerContext context) if(request.Url!.LocalPath.Contains("favicon")) SendResponse(HttpStatusCode.NoContent, response); + if (Regex.IsMatch(request.Url.LocalPath, "")) + { + HandleRequestV2(context); + return; + } + switch (request.HttpMethod) { case "GET": diff --git a/Tranga/ServerV2.cs b/Tranga/ServerV2.cs new file mode 100644 index 00000000..6c115d11 --- /dev/null +++ b/Tranga/ServerV2.cs @@ -0,0 +1,80 @@ +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace Tranga; + +public partial class Server +{ + private void HandleRequestV2(HttpListenerContext context) + { + HttpListenerRequest request = context.Request; + HttpListenerResponse response = context.Response; + string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; + + Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI + Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body + Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) + .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API + + + switch (request.HttpMethod) + { + case "GET": + HandleGetV2(path, response, requestParams); + break; + case "POST": + HandlePostV2(path, response, requestParams); + break; + case "DELETE": + HandleDeleteV2(path, response, requestParams); + break; + default: + SendResponse(HttpStatusCode.MethodNotAllowed, response); + break; + } + } + + private Dictionary GetRequestBody(HttpListenerRequest request) + { + if (!request.HasEntityBody) + { + Log("No request body"); + return new Dictionary(); + } + Stream body = request.InputStream; + Encoding encoding = request.ContentEncoding; + using StreamReader streamReader = new (body, encoding); + try + { + Dictionary requestBody = + JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) + ?? new(); + return requestBody; + } + catch (JsonException e) + { + Log(e.Message); + } + return new Dictionary(); + } + + private void HandleGetV2(string path, HttpListenerResponse response, + Dictionary requestParameters) + { + throw new NotImplementedException("v2 not implemented yet"); + } + + private void HandlePostV2(string path, HttpListenerResponse response, + Dictionary requestParameters) + { + throw new NotImplementedException("v2 not implemented yet"); + } + + private void HandleDeleteV2(string path, HttpListenerResponse response, + Dictionary requestParameters) + { + throw new NotImplementedException("v2 not implemented yet"); + } +} \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md new file mode 100644 index 00000000..e81f287e --- /dev/null +++ b/docs/API_Calls_v2.md @@ -0,0 +1,895 @@ + +# Tranga API Calls v2 +This document outlines all different HTTP API calls that Tranga accepts. +Tranga expects specific HTTP methods for its calls and therefore careful attention must be paid when making them. + +`apiUri` refers to your `http(s)://TRANGA.FRONTEND.URI/api`. + +Parameters are included in the HTTP request URI and/or the request body. +The request Body is in JSON key-value-pair format, with all values as strings. +Tranga responses are always in the JSON format within the Response Body. + +Parameters in *italics* are optional + + + + + +### Quick Entry + +* [Connectors](#connectors-suptopsup) +* [Manga](#manga-suptopsup) +* [Jobs](#jobs-suptopsup) +* [Settings](#settings-suptopsup) +* [Library Connectors](#library-connectors-suptopsup) +* [Notification Connectors](#notification-connectors-suptopsup) +* [Miscellaneous](#miscellaneous-suptopsup) + +## Connectors [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector/Types` + +Returns available Manga Connectors (Scanlation sites) + +
+ Returns +List of strings with Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector//GetManga` + +Returns the Manga from the specified Manga Connector. + +
+ Request + +`ConnectorName` is returned in the response of [GET /v2/Connector/Types](#/v2/Connector) + +Use either `title` or `url` Parameter. + +| Parameter | Value | +|-----------|-------------------------------------------------| +| title | Search Term | +| url | Direct link (URL) to the Manga on the used Site | +
+ +
+ Returns + +List of [Manga](Types.md/#Manga) + +| StatusCode | Meaning | +|------------|--------------------------| +| 400 | Connector does not exist | +| 404 | URL/Connector Mismatch | +
+ +## Manga [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/` + +Returns the specified Manga. + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) +
+ +
+ Returns + +[Manga](Types.md/#manga) + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Manga with `internalId` could not be found | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Manga/` + +Deletes all associated Jobs for the specified Manga + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 200 | Jobs were deleted | +| 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Cover` + +Returns the URL for the Cover of the specified Manga. + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) +
+ +
+ Returns + +String with the url. + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Chapters` + +Returns the Chapter-list for the specified Manga. + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) +
+ +
+ Returns + +List of [Chapters](Types.md/#chapter) + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Manga with `internalId` could not be found | +
+ +## Jobs [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs` + +Returns all configured Jobs. + +
+ Returns + +List of [Jobs](Types.md#job) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Running` + +Returns all Running Jobs. + +
+ Returns + +List of [Jobs](Types.md#job) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Waiting` + +Returns all Waiting Jobs. + +
+ Returns + +List of [Jobs](Types.md#job) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` + +Returns all Monitoring Jobs. + +
+ Returns + +List of [Jobs](Types.md#job) +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/Monitor/` + +Creates a Monitoring-Job for the specified Manga at the specified Interval. + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + | Parameter | Value | + |-----------|--------------------------------------------------------| + | interval | Interval at which the Job is re-run in HH:MM:SS format | +
+ +
+ Returns + +[Job](Types.md/#job) + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Manga with `internalId` could not be found | +| 500 | Error parsing interval | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/DownloadNewChapters/` + +Creates a Job to check for new Chapters and Download new ones of the specified Manga. + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) +
+ +
+ Returns + +[Job](Types.md/#job) + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Manga with `internalId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/UpdateMetadata` + +Creates a Job to update the Metadata of all Manga. + +
+ Returns + +[Job](Types.md/#job) +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/UpdateMetadata/` + +Updates the Metadata of the specified Manga. + +
+ Request + +`internalId` is returned in the response of +* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) +
+ +
+ Returns + +[Job](Types.md/#job) + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Manga with `internalId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` + +Returns the specified Job. + +
+ Request + +`jobId` is returned in the response of + +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +
+ +
+ Returns + +[Job](Types.md/#job) + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 404 | Manga with `jobId` could not be found | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Job/` + +Deletes the specified Job and all descendants. + +
+ Request + +`jobId` is returned in the response of +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 200 | Job deleted | +| 404 | Manga with `jobId` could not be found | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job//Progress` + +Returns the progress the of the specified Job. + +
+ Request + +`jobId` is returned in the response of +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +
+ +
+ Returns + +[ProgressToken](Types.md#progresstoken) + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 404 | Manga with `jobId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//StartNow` + +Starts the specified Job. + +
+ Request + +`jobId` is returned in the response of +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 200 | Job started | +| 404 | Manga with `jobId` could not be found | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//Cancel` + +Cancels the specified Job, or dequeues it. + +
+ Request + +`jobId` is returned in the response of +* [GET /v2/Jobs](#subsub-v2jobs) +* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) +* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) +* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 200 | Job cancelled | +| 404 | Manga with `jobId` could not be found | +
+ +## Settings [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings` + +Returns the `settings.json` file. + +
+ Returns + +[Settings](Types.md/#settings) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/UserAgent` + +Returns the current User Agent used for Requests. + +
+ Returns + +[UserAgent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/UserAgent` + +Sets the User Agent. If left empty, User Agent is reset to default. + +
+ Request + +| Parameter | Value | +|-----------|----------------------------------------------------------------------------------------| +| value | New [UserAgent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) | +
+ +
+ Returns +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/Types` + +Returns the configurable Rate-Limits. + +
+ Returns + +List of Rate-Limit-Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit` + +Returns the current configuration of Rate-Limits for Requests. + +
+ Returns + +Dictionary of `Rate-Limits` and `Requests per Minute` +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit` + +Sets the Rate-Limits for all Requests. If left empty, resets to default Rate-Limits. + +
+ Request + +For each Rate-Limit set as follows: + +| Parameter | Value | +|---------------------------------------|---------------------| +| [Type](#/v2/Settings/RateLimit/Types) | Requests per Minute | + +`Type` is returned by [GET /v2/Settings/RateLimit/Types](#/v2/Settings/RateLimit/Types) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|--------------------------------| +| 404 | Rate-Limit-Name does not exist | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/` + +Returns the current Rate-Limit for the Request-Type. + +
+ Request + +`Type` is returned by [GET /v2/Settings/RateLimit/Types](#/v2/Settings/RateLimit/Types) +
+ +
+ Returns + +Integer with Requests per Minute. +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit/` + +Sets the Rate-Limit for the Request-Type in Requests per Minute. + +
+ Request + +`Type` is returned by [GET /v2/Settings/RateLimit/Types](#/v2/Settings/RateLimit/Types) + +| Parameter | Value | +|-----------|---------------------| +| value | Requests per Minute | +
+
+ Returns + +| StatusCode | Meaning | +|------------|--------------------------------| +| 404 | Rate-Limit-Name does not exist | +| 500 | Parsing Error | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/AprilFoolsMode` + +Returns the current state of the April-Fools-Mode setting. + +
+ Returns + +Boolean +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/ApriFoolsMode` + +Enables/Disables April-Fools-Mode. + +
+ Request + +| Parameter | Value | +|-----------|------------| +| value | true/false | +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|--------------------------------| +| 404 | Rate-Limit-Name does not exist | +| 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/DownloadLocation` + +Updates the default Download-Location. + +
+ Request + +| Parameter | Value | +|-------------|------------------| +| location | New Folder-Path | +| *moveFiles* | __*true*__/false | +
+ +
+ Returns + + +| StatusCode | Meaning | +|------------|--------------------------| +| 200 | Successfully changed | +| 500 | Files could not be moved | +
+ +## Library Connectors [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector` + +Returns the configured Library-Connectors. + +
+ Returns + +List of [LibraryConnectors](Types.md#libraryconnector) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector/Types` + +Returns the available Library-Connector types. + +
+ Returns + +List of String of Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector/` + +Returns the Library-Connector for the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) +
+ +
+ Returns + +[LibraryConnector](Types.md#libraryconnector) + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 404 | Library Connector Type does not exist | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector/` + +Creates a Library-Connector of the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) + +| Parameter | Value | +|-------------|--------------------| +| URL | URL of the Library | + +#### Type specific Parameters (must be included for each) +* Komga + +| Parameter | Value | +|-----------|-------------------------------------------------------------------------------------------------------------------| +| auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | + +* Kavita + +| Parameter | Value | +|-----------|-----------------| +| username | Kavita Username | +| password | Kavita Password | +
+ +
+ Returns + +[LibraryConnector](Types.md#libraryconnector) + +| StatusCode | Meaning | +|------------|----------------------------------| +| 404 | Library Connector does not exist | +| 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector//Test` + +Tests a Library-Connector of the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) + +| Parameter | Value | +|-------------|--------------------| +| URL | URL of the Library | + +#### Type specific Parameters (must be included for each) +* Komga + +| Parameter | Value | +|-----------|-------------------------------------------------------------------------------------------------------------------| +| auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | + +* Kavita + +| Parameter | Value | +|-----------|-----------------| +| username | Kavita Username | +| password | Kavita Password | +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 200 | Test successful | +| 404 | Library Connector Type does not exist | +| 408 | Test failed | +| 500 | Parsing Error | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/LibraryConnector/` + +Deletes the Library-Connector of the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 200 | Deleted | +| 404 | Library Connector Type does not exist | +
+ +## Notification Connectors [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector` + +Returns the configured Notification-Connectors. + +
+ Returns + +List of [NotificationConnectors](Types.md#notificationconnector) +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector/Types` + +Returns the available Notification-Connectors. + +
+ Returns + +List of String of Names. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector/` + +Returns the configured Notification-Connector of the specified Type. + +
+ Returns + +[Notification Connector](Types.md#notificationconnector) + +| StatusCode | Meaning | +|------------|---------------------------------------| +| 404 | Library Connector Type does not exist | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector/` + +Creates a Notification-Connector of the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/NotificationConnector/Types](#/v2/NotificationConnector/Types) + +#### Type specific Parameters (must be included for each) +* Gotify + +| Parameter | Value | +|-----------|---------------------------------------| +| url | URL of the Gotify Instance | +| appToken | AppToken of the configured Gotify App | + +* LunaSea + +| Parameter | Value | +|-----------|-----------------| +| webhook | LunaSea Webhook | + +* Nty + +| Parameter | Value | +|-----------|--------------------------| +| url | URL of the Ntfy Instance | +| auth | Auth-String | +
+ +
+ Returns + +[NotificationConnector](Types.md#notificationconnector) + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 404 | Notification Connector Type does not exist | +| 500 | Parsing Error | +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector//Test` + +Tests a Notification-Connector of the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/NotificationConnector/Types](#/v2/NotificationConnector/Types) + +#### Type specific Parameters (must be included for each) +* Gotify + +| Parameter | Value | +|-----------|---------------------------------------| +| url | URL of the Gotify Instance | +| appToken | AppToken of the configured Gotify App | + +* LunaSea + +| Parameter | Value | +|-----------|-----------------| +| webhook | LunaSea Webhook | + +* Ntfy + +| Parameter | Value | +|-----------|--------------------------| +| url | URL of the Ntfy Instance | +| auth | Auth-String | +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 200 | Test successful | +| 404 | Notification Connector Type does not exist | +| 408 | Test failed | +| 500 | Parsing Error | +
+ +### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/NotificationConnector/` + +Deletes the Notification-Connector of the specified Type. + +
+ Request + +`Type` is returned by [GET /v2/NotificationConnector/Types](#/v2/NotificationConnector/Types) +
+ +
+ Returns + +| StatusCode | Meaning | +|------------|--------------------------------------------| +| 200 | Deleted | +| 404 | Notification Connector Type does not exist | +
+ +## Miscellaneous [^top](#top) + +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LogFile` + +Returns the current log-file. + +
+ Returns + +The Logfile as Stream. +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Ping` + +Pong! + +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Ping` + +Pong! diff --git a/docs/Types.md b/docs/Types.md new file mode 100644 index 00000000..9a7ae3df --- /dev/null +++ b/docs/Types.md @@ -0,0 +1,41 @@ +## Manga +```json +{ +} +``` + +## Chapter +```json +{ +} +``` + +## Job +```json +{ +} +``` + +## ProgressToken +```json +{ +} +``` + +## Settings +```json +{ +} +``` + +## LibraryConnector +```json +{ +} +``` + +## NotificationConnector +```json +{ +} +``` \ No newline at end of file From f5cecb9e301be28ad9516b97b47c3c859285c64b Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 21:35:38 +0200 Subject: [PATCH 09/95] Github Reference Link Style --- docs/API_Calls_v2.md | 758 ++++++++++++++++++++++--------------------- 1 file changed, 382 insertions(+), 376 deletions(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index e81f287e..6f089219 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -17,13 +17,13 @@ Parameters in *italics* are optional ### Quick Entry -* [Connectors](#connectors-suptopsup) -* [Manga](#manga-suptopsup) -* [Jobs](#jobs-suptopsup) -* [Settings](#settings-suptopsup) -* [Library Connectors](#library-connectors-suptopsup) -* [Notification Connectors](#notification-connectors-suptopsup) -* [Miscellaneous](#miscellaneous-suptopsup) +* [Connectors](#connectors-top) +* [Manga](#manga-top) +* [Jobs](#jobs-top) +* [Settings](#settings-top) +* [Library Connectors](#library-connectors-top) +* [Notification Connectors](#notification-connectors-top) +* [Miscellaneous](#miscellaneous-top) ## Connectors [^top](#top) @@ -33,7 +33,8 @@ Returns available Manga Connectors (Scanlation sites)
Returns -List of strings with Names. + + List of strings with Names.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector//GetManga` @@ -42,26 +43,26 @@ Returns the Manga from the specified Manga Connector.
Request - -`ConnectorName` is returned in the response of [GET /v2/Connector/Types](#/v2/Connector) - -Use either `title` or `url` Parameter. - -| Parameter | Value | -|-----------|-------------------------------------------------| -| title | Search Term | -| url | Direct link (URL) to the Manga on the used Site | + + `ConnectorName` is returned in the response of [GET /v2/Connector/Types](#-v2connectortypes) + + Use either `title` or `url` Parameter. + + | Parameter | Value | + |-----------|-------------------------------------------------| + | title | Search Term | + | url | Direct link (URL) to the Manga on the used Site |
Returns - -List of [Manga](Types.md/#Manga) - -| StatusCode | Meaning | -|------------|--------------------------| -| 400 | Connector does not exist | -| 404 | URL/Connector Mismatch | + + List of [Manga](Types.md#Manga) + + | StatusCode | Meaning | + |------------|--------------------------| + | 400 | Connector does not exist | + | 404 | URL/Connector Mismatch |
## Manga [^top](#top) @@ -72,24 +73,24 @@ Returns the specified Manga.
Request - -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs)
Returns - -[Manga](Types.md/#manga) - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Manga with `internalId` could not be found | + + [Manga](Types.md#manga) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found |
### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Manga/` @@ -98,23 +99,23 @@ Deletes all associated Jobs for the specified Manga
Request - -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs)
Returns - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 200 | Jobs were deleted | -| 404 | Manga with `internalId` could not be found | + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 200 | Jobs were deleted | + | 404 | Manga with `internalId` could not be found |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Cover` @@ -123,24 +124,24 @@ Returns the URL for the Cover of the specified Manga.
Request - -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs)
Returns String with the url. - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Manga with `internalId` could not be found | + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Chapters` @@ -149,24 +150,24 @@ Returns the Chapter-list for the specified Manga.
Request - -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs)
Returns - -List of [Chapters](Types.md/#chapter) - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Manga with `internalId` could not be found | + + List of [Chapters](Types.md/#chapter) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found |
## Jobs [^top](#top) @@ -177,8 +178,8 @@ Returns all configured Jobs.
Returns - -List of [Jobs](Types.md#job) + + List of [Jobs](Types.md#job)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Running` @@ -187,8 +188,8 @@ Returns all Running Jobs.
Returns - -List of [Jobs](Types.md#job) + + List of [Jobs](Types.md#job)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Waiting` @@ -197,8 +198,8 @@ Returns all Waiting Jobs.
Returns - -List of [Jobs](Types.md#job) + + List of [Jobs](Types.md#job)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` @@ -207,8 +208,8 @@ Returns all Monitoring Jobs.
Returns - -List of [Jobs](Types.md#job) + + List of [Jobs](Types.md#job)
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/Monitor/` @@ -217,29 +218,29 @@ Creates a Monitoring-Job for the specified Manga at the specified Interval.
Request + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs) -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) - - | Parameter | Value | - |-----------|--------------------------------------------------------| - | interval | Interval at which the Job is re-run in HH:MM:SS format | + | Parameter | Value | + |-----------|--------------------------------------------------------| + | interval | Interval at which the Job is re-run in HH:MM:SS format |
Returns - -[Job](Types.md/#job) - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Manga with `internalId` could not be found | -| 500 | Error parsing interval | + + [Job](Types.md#job) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | + | 500 | Error parsing interval |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/DownloadNewChapters/` @@ -248,24 +249,24 @@ Creates a Job to check for new Chapters and Download new ones of the specified M
Request - -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs)
Returns - -[Job](Types.md/#job) - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Manga with `internalId` could not be found | + + [Job](Types.md#job) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/UpdateMetadata` @@ -274,8 +275,8 @@ Creates a Job to update the Metadata of all Manga.
Returns - -[Job](Types.md/#job) + + [Job](Types.md#job)
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/UpdateMetadata/` @@ -284,24 +285,24 @@ Updates the Metadata of the specified Manga.
Request - -`internalId` is returned in the response of -* [GET /v2/Connector/*ConnectorName*/GetManga](#subsub-v2connectorconnectornamegetmanga) -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) -* [GET /v2/Jobs/*jobId*](#subsub-v2jobs) + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Jobs/*jobId*](#-v2jobs)
Returns - -[Job](Types.md/#job) - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Manga with `internalId` could not be found | + + [Job](Types.md#job) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` @@ -310,23 +311,22 @@ Returns the specified Job.
Request - -`jobId` is returned in the response of - -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring)
Returns - -[Job](Types.md/#job) - -| StatusCode | Meaning | -|------------|---------------------------------------| -| 404 | Manga with `jobId` could not be found | + + [Job](Types.md#job) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Manga with `jobId` could not be found |
### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Job/` @@ -335,21 +335,21 @@ Deletes the specified Job and all descendants.
Request - -`jobId` is returned in the response of -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring)
Returns - -| StatusCode | Meaning | -|------------|---------------------------------------| -| 200 | Job deleted | -| 404 | Manga with `jobId` could not be found | + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Job deleted | + | 404 | Manga with `jobId` could not be found |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job//Progress` @@ -358,22 +358,22 @@ Returns the progress the of the specified Job.
Request - -`jobId` is returned in the response of -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring)
Returns -[ProgressToken](Types.md#progresstoken) - -| StatusCode | Meaning | -|------------|---------------------------------------| -| 404 | Manga with `jobId` could not be found | + [ProgressToken](Types.md#progresstoken) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Manga with `jobId` could not be found |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//StartNow` @@ -382,21 +382,21 @@ Starts the specified Job.
Request - -`jobId` is returned in the response of -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring)
Returns -| StatusCode | Meaning | -|------------|---------------------------------------| -| 200 | Job started | -| 404 | Manga with `jobId` could not be found | + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Job started | + | 404 | Manga with `jobId` could not be found |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//Cancel` @@ -406,20 +406,20 @@ Cancels the specified Job, or dequeues it.
Request -`jobId` is returned in the response of -* [GET /v2/Jobs](#subsub-v2jobs) -* [GET /v2/Jobs/Running](#subsub-v2jobsrunning) -* [GET /v2/Jobs/Waiting](#subsub-v2jobswaiting) -* [GET /v2/Jobs/Monitoring](#subsub-v2jobsmonitoring) + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring)
Returns -| StatusCode | Meaning | -|------------|---------------------------------------| -| 200 | Job cancelled | -| 404 | Manga with `jobId` could not be found | + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Job cancelled | + | 404 | Manga with `jobId` could not be found |
## Settings [^top](#top) @@ -431,7 +431,7 @@ Returns the `settings.json` file.
Returns -[Settings](Types.md/#settings) + [Settings](Types.md#settings)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/UserAgent` @@ -441,7 +441,7 @@ Returns the current User Agent used for Requests.
Returns -[UserAgent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) + [UserAgent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent)
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/UserAgent` @@ -467,7 +467,7 @@ Returns the configurable Rate-Limits.
Returns -List of Rate-Limit-Names. + List of Rate-Limit-Names.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit` @@ -477,7 +477,7 @@ Returns the current configuration of Rate-Limits for Requests.
Returns -Dictionary of `Rate-Limits` and `Requests per Minute` + Dictionary of `Rate-Limits` and `Requests per Minute`
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit` @@ -487,21 +487,21 @@ Sets the Rate-Limits for all Requests. If left empty, resets to default Rate-Lim
Request -For each Rate-Limit set as follows: - -| Parameter | Value | -|---------------------------------------|---------------------| -| [Type](#/v2/Settings/RateLimit/Types) | Requests per Minute | - -`Type` is returned by [GET /v2/Settings/RateLimit/Types](#/v2/Settings/RateLimit/Types) + For each Rate-Limit set as follows: + + | Parameter | Value | + |------------------------------------|---------------------| + | [Type](#-v2settingsratelimittypes) | Requests per Minute | + + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes)
Returns -| StatusCode | Meaning | -|------------|--------------------------------| -| 404 | Rate-Limit-Name does not exist | + | StatusCode | Meaning | + |------------|--------------------------------| + | 404 | Rate-Limit-Name does not exist |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/` @@ -511,13 +511,13 @@ Returns the current Rate-Limit for the Request-Type.
Request -`Type` is returned by [GET /v2/Settings/RateLimit/Types](#/v2/Settings/RateLimit/Types) + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes)
Returns -Integer with Requests per Minute. + Integer with Requests per Minute.
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit/` @@ -527,19 +527,19 @@ Sets the Rate-Limit for the Request-Type in Requests per Minute.
Request -`Type` is returned by [GET /v2/Settings/RateLimit/Types](#/v2/Settings/RateLimit/Types) - -| Parameter | Value | -|-----------|---------------------| -| value | Requests per Minute | + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes) + + | Parameter | Value | + |-----------|---------------------| + | value | Requests per Minute |
Returns -| StatusCode | Meaning | -|------------|--------------------------------| -| 404 | Rate-Limit-Name does not exist | -| 500 | Parsing Error | + | StatusCode | Meaning | + |------------|--------------------------------| + | 404 | Rate-Limit-Name does not exist | + | 500 | Parsing Error |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/AprilFoolsMode` @@ -549,7 +549,7 @@ Returns the current state of the April-Fools-Mode setting.
Returns -Boolean + Boolean
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/ApriFoolsMode` @@ -559,18 +559,18 @@ Enables/Disables April-Fools-Mode.
Request -| Parameter | Value | -|-----------|------------| -| value | true/false | + | Parameter | Value | + |-----------|------------| + | value | true/false |
Returns -| StatusCode | Meaning | -|------------|--------------------------------| -| 404 | Rate-Limit-Name does not exist | -| 500 | Parsing Error | + | StatusCode | Meaning | + |------------|--------------------------------| + | 404 | Rate-Limit-Name does not exist | + | 500 | Parsing Error |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/DownloadLocation` @@ -580,20 +580,20 @@ Updates the default Download-Location.
Request -| Parameter | Value | -|-------------|------------------| -| location | New Folder-Path | -| *moveFiles* | __*true*__/false | + | Parameter | Value | + |-------------|------------------| + | location | New Folder-Path | + | *moveFiles* | __*true*__/false |
Returns -| StatusCode | Meaning | -|------------|--------------------------| -| 200 | Successfully changed | -| 500 | Files could not be moved | + | StatusCode | Meaning | + |------------|--------------------------| + | 200 | Successfully changed | + | 500 | Files could not be moved |
## Library Connectors [^top](#top) @@ -605,7 +605,7 @@ Returns the configured Library-Connectors.
Returns -List of [LibraryConnectors](Types.md#libraryconnector) + List of [LibraryConnectors](Types.md#libraryconnector)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector/Types` @@ -615,7 +615,7 @@ Returns the available Library-Connector types.
Returns -List of String of Names. + List of String of Names.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/LibraryConnector/` @@ -625,17 +625,17 @@ Returns the Library-Connector for the specified Type.
Request -`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes)
Returns -[LibraryConnector](Types.md#libraryconnector) - -| StatusCode | Meaning | -|------------|---------------------------------------| -| 404 | Library Connector Type does not exist | + [LibraryConnector](Types.md#libraryconnector) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Library Connector Type does not exist |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector/` @@ -645,36 +645,36 @@ Creates a Library-Connector of the specified Type.
Request -`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) - -| Parameter | Value | -|-------------|--------------------| -| URL | URL of the Library | - -#### Type specific Parameters (must be included for each) -* Komga - -| Parameter | Value | -|-----------|-------------------------------------------------------------------------------------------------------------------| -| auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | - -* Kavita - -| Parameter | Value | -|-----------|-----------------| -| username | Kavita Username | -| password | Kavita Password | + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) + + | Parameter | Value | + |-------------|--------------------| + | URL | URL of the Library | + + #### Type specific Parameters (must be included for each) + * Komga + + | Parameter | Value | + |-----------|-------------------------------------------------------------------------------------------------------------------| + | auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | + + * Kavita + + | Parameter | Value | + |-----------|-----------------| + | username | Kavita Username | + | password | Kavita Password |
Returns -[LibraryConnector](Types.md#libraryconnector) - -| StatusCode | Meaning | -|------------|----------------------------------| -| 404 | Library Connector does not exist | -| 500 | Parsing Error | + [LibraryConnector](Types.md#libraryconnector) + + | StatusCode | Meaning | + |------------|----------------------------------| + | 404 | Library Connector does not exist | + | 500 | Parsing Error |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector//Test` @@ -684,36 +684,36 @@ Tests a Library-Connector of the specified Type.
Request -`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) - -| Parameter | Value | -|-------------|--------------------| -| URL | URL of the Library | - -#### Type specific Parameters (must be included for each) -* Komga - -| Parameter | Value | -|-----------|-------------------------------------------------------------------------------------------------------------------| -| auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | - -* Kavita - -| Parameter | Value | -|-----------|-----------------| -| username | Kavita Username | -| password | Kavita Password | -
- -
- Returns - -| StatusCode | Meaning | -|------------|---------------------------------------| -| 200 | Test successful | -| 404 | Library Connector Type does not exist | -| 408 | Test failed | -| 500 | Parsing Error | + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) + + | Parameter | Value | + |-------------|--------------------| + | URL | URL of the Library | + + #### Type specific Parameters (must be included for each) + * Komga + + | Parameter | Value | + |-----------|-------------------------------------------------------------------------------------------------------------------| + | auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | + + * Kavita + + | Parameter | Value | + |-----------|-----------------| + | username | Kavita Username | + | password | Kavita Password | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Test successful | + | 404 | Library Connector Type does not exist | + | 408 | Test failed | + | 500 | Parsing Error |
### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/LibraryConnector/` @@ -723,16 +723,16 @@ Deletes the Library-Connector of the specified Type.
Request -`Type` is returned by [GET /v2/LibraryConnector/Types](#/v2/LibraryConnector/Types) + `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes)
Returns -| StatusCode | Meaning | -|------------|---------------------------------------| -| 200 | Deleted | -| 404 | Library Connector Type does not exist | + | StatusCode | Meaning | + |------------|---------------------------------------| + | 200 | Deleted | + | 404 | Library Connector Type does not exist |
## Notification Connectors [^top](#top) @@ -744,7 +744,7 @@ Returns the configured Notification-Connectors.
Returns -List of [NotificationConnectors](Types.md#notificationconnector) + List of [NotificationConnectors](Types.md#notificationconnector)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector/Types` @@ -753,8 +753,8 @@ Returns the available Notification-Connectors.
Returns - -List of String of Names. + + List of String of Names.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/NotificationConnector/` @@ -762,13 +762,19 @@ List of String of Names. Returns the configured Notification-Connector of the specified Type.
- Returns + Request -[Notification Connector](Types.md#notificationconnector) + `Type` is returned by [GET /v2/NotificationConnector/Types](#-v2notificationconnectortypes) +
-| StatusCode | Meaning | -|------------|---------------------------------------| -| 404 | Library Connector Type does not exist | +
+ Returns + + [Notification Connector](Types.md#notificationconnector) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Library Connector Type does not exist |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector/` @@ -778,39 +784,39 @@ Creates a Notification-Connector of the specified Type.
Request -`Type` is returned by [GET /v2/NotificationConnector/Types](#/v2/NotificationConnector/Types) - -#### Type specific Parameters (must be included for each) -* Gotify - -| Parameter | Value | -|-----------|---------------------------------------| -| url | URL of the Gotify Instance | -| appToken | AppToken of the configured Gotify App | - -* LunaSea - -| Parameter | Value | -|-----------|-----------------| -| webhook | LunaSea Webhook | - -* Nty - -| Parameter | Value | -|-----------|--------------------------| -| url | URL of the Ntfy Instance | -| auth | Auth-String | -
- -
- Returns - -[NotificationConnector](Types.md#notificationconnector) - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 404 | Notification Connector Type does not exist | -| 500 | Parsing Error | + `Type` is returned by [GET /v2/NotificationConnector/Types](-v2notificationconnectortypes) + + #### Type specific Parameters (must be included for each) + * Gotify + + | Parameter | Value | + |-----------|---------------------------------------| + | url | URL of the Gotify Instance | + | appToken | AppToken of the configured Gotify App | + + * LunaSea + + | Parameter | Value | + |-----------|-----------------| + | webhook | LunaSea Webhook | + + * Nty + + | Parameter | Value | + |-----------|--------------------------| + | url | URL of the Ntfy Instance | + | auth | Auth-String | +
+ +
+ Returns + + [NotificationConnector](Types.md#notificationconnector) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Notification Connector Type does not exist | + | 500 | Parsing Error |
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector//Test` @@ -820,39 +826,39 @@ Tests a Notification-Connector of the specified Type.
Request -`Type` is returned by [GET /v2/NotificationConnector/Types](#/v2/NotificationConnector/Types) - -#### Type specific Parameters (must be included for each) -* Gotify - -| Parameter | Value | -|-----------|---------------------------------------| -| url | URL of the Gotify Instance | -| appToken | AppToken of the configured Gotify App | - -* LunaSea - -| Parameter | Value | -|-----------|-----------------| -| webhook | LunaSea Webhook | - -* Ntfy - -| Parameter | Value | -|-----------|--------------------------| -| url | URL of the Ntfy Instance | -| auth | Auth-String | -
- -
- Returns - -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 200 | Test successful | -| 404 | Notification Connector Type does not exist | -| 408 | Test failed | -| 500 | Parsing Error | + `Type` is returned by [GET /v2/NotificationConnector/Types](#-v2notificationconnectortypes) + + #### Type specific Parameters (must be included for each) + * Gotify + + | Parameter | Value | + |-----------|---------------------------------------| + | url | URL of the Gotify Instance | + | appToken | AppToken of the configured Gotify App | + + * LunaSea + + | Parameter | Value | + |-----------|-----------------| + | webhook | LunaSea Webhook | + + * Ntfy + + | Parameter | Value | + |-----------|--------------------------| + | url | URL of the Ntfy Instance | + | auth | Auth-String | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 200 | Test successful | + | 404 | Notification Connector Type does not exist | + | 408 | Test failed | + | 500 | Parsing Error |
### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/NotificationConnector/` @@ -862,16 +868,16 @@ Deletes the Notification-Connector of the specified Type.
Request -`Type` is returned by [GET /v2/NotificationConnector/Types](#/v2/NotificationConnector/Types) + `Type` is returned by [GET /v2/NotificationConnector/Types](#-v2notificationconnectortypes)
Returns -| StatusCode | Meaning | -|------------|--------------------------------------------| -| 200 | Deleted | -| 404 | Notification Connector Type does not exist | + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 200 | Deleted | + | 404 | Notification Connector Type does not exist |
## Miscellaneous [^top](#top) @@ -883,7 +889,7 @@ Returns the current log-file.
Returns -The Logfile as Stream. + The Logfile as Stream.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Ping` From 0313d81204a0d77d54a45ebee936b067b6564755 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 21:38:46 +0200 Subject: [PATCH 10/95] Return JobIds instead of full jobs. /v2/Jobs /v2/Jobs/Running /v2/Jobs/Waiting /v2/Jobs/Monitoring --- docs/API_Calls_v2.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 6f089219..e1f46d4d 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -179,7 +179,7 @@ Returns all configured Jobs.
Returns - List of [Jobs](Types.md#job) + List of JobIds.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Running` @@ -188,8 +188,8 @@ Returns all Running Jobs.
Returns - - List of [Jobs](Types.md#job) + + List of JobIds.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Waiting` @@ -198,8 +198,8 @@ Returns all Waiting Jobs.
Returns - - List of [Jobs](Types.md#job) + + List of JobIds.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` @@ -208,8 +208,8 @@ Returns all Monitoring Jobs.
Returns - - List of [Jobs](Types.md#job) + + List of JobIds.
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/Monitor/` From 238395a3da27e8c5b89747133266f69690ed7479 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 21:38:46 +0200 Subject: [PATCH 11/95] Return JobIds instead of full jobs. /v2/Jobs /v2/Jobs/Running /v2/Jobs/Waiting /v2/Jobs/Monitoring --- docs/API_Calls_v2.md | 44 ++++++++------------------------------------ 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 6f089219..00b0cb98 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -76,10 +76,6 @@ Returns the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) * [GET /v2/Jobs/*jobId*](#-v2jobs) @@ -102,10 +98,6 @@ Deletes all associated Jobs for the specified Manga `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) * [GET /v2/Jobs/*jobId*](#-v2jobs) @@ -126,11 +118,7 @@ Returns the URL for the Cover of the specified Manga. Request `internalId` is returned in the response of - * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Jobs/*jobId*](#-v2jobs) @@ -153,10 +141,6 @@ Returns the Chapter-list for the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) * [GET /v2/Jobs/*jobId*](#-v2jobs) @@ -179,7 +163,7 @@ Returns all configured Jobs.
Returns - List of [Jobs](Types.md#job) + List of JobIds.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Running` @@ -188,8 +172,8 @@ Returns all Running Jobs.
Returns - - List of [Jobs](Types.md#job) + + List of JobIds.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Waiting` @@ -198,8 +182,8 @@ Returns all Waiting Jobs.
Returns - - List of [Jobs](Types.md#job) + + List of JobIds.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` @@ -208,8 +192,8 @@ Returns all Monitoring Jobs.
Returns - - List of [Jobs](Types.md#job) + + List of JobIds.
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/Monitor/` @@ -221,10 +205,6 @@ Creates a Monitoring-Job for the specified Manga at the specified Interval. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) * [GET /v2/Jobs/*jobId*](#-v2jobs) | Parameter | Value | @@ -252,10 +232,6 @@ Creates a Job to check for new Chapters and Download new ones of the specified M `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) * [GET /v2/Jobs/*jobId*](#-v2jobs) @@ -288,10 +264,6 @@ Updates the Metadata of the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs](#-v2jobs) - * [GET /v2/Jobs/Running](#-v2jobsrunning) - * [GET /v2/Jobs/Waiting](#-v2jobswaiting) - * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) * [GET /v2/Jobs/*jobId*](#-v2jobs) From d22b49cfa83e5c7690c6619d1049e35fe133585a Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 21:58:29 +0200 Subject: [PATCH 12/95] Change Method Header for Handlers to return the response to HandleRequest so we don't forget to send a response. --- Tranga/ServerV2.cs | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/Tranga/ServerV2.cs b/Tranga/ServerV2.cs index 6c115d11..423ad0c4 100644 --- a/Tranga/ServerV2.cs +++ b/Tranga/ServerV2.cs @@ -17,23 +17,16 @@ private void HandleRequestV2(HttpListenerContext context) Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API - - - switch (request.HttpMethod) + + ValueTuple responseMessage = request.HttpMethod switch { - case "GET": - HandleGetV2(path, response, requestParams); - break; - case "POST": - HandlePostV2(path, response, requestParams); - break; - case "DELETE": - HandleDeleteV2(path, response, requestParams); - break; - default: - SendResponse(HttpStatusCode.MethodNotAllowed, response); - break; - } + "GET" => HandleGetV2(path, response, requestParams), + "POST" => HandlePostV2(path, response, requestParams), + "DELETE" => HandleDeleteV2(path, response, requestParams), + _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) + }; + + SendResponse(responseMessage.Item1, response, responseMessage.Item2); } private Dictionary GetRequestBody(HttpListenerRequest request) @@ -60,19 +53,19 @@ private Dictionary GetRequestBody(HttpListenerRequest request) return new Dictionary(); } - private void HandleGetV2(string path, HttpListenerResponse response, + private ValueTuple HandleGetV2(string path, HttpListenerResponse response, Dictionary requestParameters) { throw new NotImplementedException("v2 not implemented yet"); } - private void HandlePostV2(string path, HttpListenerResponse response, + private ValueTuple HandlePostV2(string path, HttpListenerResponse response, Dictionary requestParameters) { throw new NotImplementedException("v2 not implemented yet"); } - private void HandleDeleteV2(string path, HttpListenerResponse response, + private ValueTuple HandleDeleteV2(string path, HttpListenerResponse response, Dictionary requestParameters) { throw new NotImplementedException("v2 not implemented yet"); From 2828fec316c72273895c09a99a0264f260b4406a Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 22:08:03 +0200 Subject: [PATCH 13/95] Merge --- Tranga/Server.cs | 20 ++++++++++++++------ Tranga/ServerV2.cs | 26 -------------------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/Tranga/Server.cs b/Tranga/Server.cs index 92028582..17c2d2a9 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -113,15 +113,23 @@ private Dictionary GetRequestBody(HttpListenerRequest request) if (!request.HasEntityBody) { Log("No request body"); - Dictionary emptyBody = new(); - return emptyBody; + return new Dictionary(); } Stream body = request.InputStream; Encoding encoding = request.ContentEncoding; - StreamReader reader = new StreamReader(body, encoding); - string s = reader.ReadToEnd(); - Dictionary requestBody = JsonConvert.DeserializeObject>(s); - return requestBody; + using StreamReader streamReader = new (body, encoding); + try + { + Dictionary requestBody = + JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) + ?? new(); + return requestBody; + } + catch (JsonException e) + { + Log(e.Message); + } + return new Dictionary(); } private void HandleGet(HttpListenerRequest request, HttpListenerResponse response) diff --git a/Tranga/ServerV2.cs b/Tranga/ServerV2.cs index 423ad0c4..48a11a23 100644 --- a/Tranga/ServerV2.cs +++ b/Tranga/ServerV2.cs @@ -1,7 +1,5 @@ using System.Net; -using System.Text; using System.Text.RegularExpressions; -using Newtonsoft.Json; namespace Tranga; @@ -29,30 +27,6 @@ private void HandleRequestV2(HttpListenerContext context) SendResponse(responseMessage.Item1, response, responseMessage.Item2); } - private Dictionary GetRequestBody(HttpListenerRequest request) - { - if (!request.HasEntityBody) - { - Log("No request body"); - return new Dictionary(); - } - Stream body = request.InputStream; - Encoding encoding = request.ContentEncoding; - using StreamReader streamReader = new (body, encoding); - try - { - Dictionary requestBody = - JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) - ?? new(); - return requestBody; - } - catch (JsonException e) - { - Log(e.Message); - } - return new Dictionary(); - } - private ValueTuple HandleGetV2(string path, HttpListenerResponse response, Dictionary requestParameters) { From f79743ee930e99319e1c103b29f1335c287db6b2 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 19 Apr 2024 22:20:24 +0200 Subject: [PATCH 14/95] actually use v2 API --- Tranga/Server.cs | 2 +- Tranga/ServerV2.cs | 21 +++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Tranga/Server.cs b/Tranga/Server.cs index 17c2d2a9..aa6aea28 100644 --- a/Tranga/Server.cs +++ b/Tranga/Server.cs @@ -68,7 +68,7 @@ private void HandleRequest(HttpListenerContext context) if (request.Url!.LocalPath.Contains("favicon")) SendResponse(HttpStatusCode.NoContent, response); - if (Regex.IsMatch(request.Url.LocalPath, "")) + if (Regex.IsMatch(request.Url.LocalPath, "/v2(/.*)?")) { HandleRequestV2(context); return; diff --git a/Tranga/ServerV2.cs b/Tranga/ServerV2.cs index 48a11a23..092a8cb4 100644 --- a/Tranga/ServerV2.cs +++ b/Tranga/ServerV2.cs @@ -18,30 +18,27 @@ private void HandleRequestV2(HttpListenerContext context) ValueTuple responseMessage = request.HttpMethod switch { - "GET" => HandleGetV2(path, response, requestParams), - "POST" => HandlePostV2(path, response, requestParams), - "DELETE" => HandleDeleteV2(path, response, requestParams), + "GET" => HandleGetV2(path, requestParams), + "POST" => HandlePostV2(path, requestParams), + "DELETE" => HandleDeleteV2(path, requestParams), _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) }; SendResponse(responseMessage.Item1, response, responseMessage.Item2); } - private ValueTuple HandleGetV2(string path, HttpListenerResponse response, - Dictionary requestParameters) + private ValueTuple HandleGetV2(string path, Dictionary requestParameters) { - throw new NotImplementedException("v2 not implemented yet"); + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); } - private ValueTuple HandlePostV2(string path, HttpListenerResponse response, - Dictionary requestParameters) + private ValueTuple HandlePostV2(string path, Dictionary requestParameters) { - throw new NotImplementedException("v2 not implemented yet"); + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); } - private ValueTuple HandleDeleteV2(string path, HttpListenerResponse response, - Dictionary requestParameters) + private ValueTuple HandleDeleteV2(string path, Dictionary requestParameters) { - throw new NotImplementedException("v2 not implemented yet"); + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); } } \ No newline at end of file From fa2598084f612f1ab108f534aa8d336e77d1db4d Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 16:54:58 +0200 Subject: [PATCH 15/95] Hard cutover https://github.com/C9Glax/tranga/pull/167#issuecomment-2067689986 --- Tranga/Server.cs | 807 ---------------------------------------- Tranga/Server/Server.cs | 196 ++++++++++ Tranga/ServerV2.cs | 44 --- Tranga/Tranga.cs | 4 +- 4 files changed, 198 insertions(+), 853 deletions(-) delete mode 100644 Tranga/Server.cs create mode 100644 Tranga/Server/Server.cs delete mode 100644 Tranga/ServerV2.cs diff --git a/Tranga/Server.cs b/Tranga/Server.cs deleted file mode 100644 index aa6aea28..00000000 --- a/Tranga/Server.cs +++ /dev/null @@ -1,807 +0,0 @@ -using System.Net; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Tranga.Jobs; -using Tranga.LibraryConnectors; -using Tranga.MangaConnectors; -using Tranga.NotificationConnectors; - -namespace Tranga; - -public partial class Server : GlobalBase -{ - private readonly HttpListener _listener = new(); - private readonly Tranga _parent; - - public Server(Tranga parent) : base(parent) - { - this._parent = parent; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); - else - this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/"); - Thread listenThread = new(Listen); - listenThread.Start(); - Thread watchThread = new(WatchRunning); - watchThread.Start(); - } - - private void WatchRunning() - { - while (_parent.keepRunning) - Thread.Sleep(1000); - this._listener.Close(); - } - - private void Listen() - { - this._listener.Start(); - foreach (string prefix in this._listener.Prefixes) - Log($"Listening on {prefix}"); - while (this._listener.IsListening && _parent.keepRunning) - { - try - { - HttpListenerContext context = this._listener.GetContext(); - //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}"); - Task t = new(() => - { - HandleRequest(context); - }); - t.Start(); - } - catch (HttpListenerException) - { - - } - } - } - - private void HandleRequest(HttpListenerContext context) - { - HttpListenerRequest request = context.Request; - HttpListenerResponse response = context.Response; - if (request.HttpMethod == "OPTIONS") - SendResponse(HttpStatusCode.OK, context.Response); - if (request.Url!.LocalPath.Contains("favicon")) - SendResponse(HttpStatusCode.NoContent, response); - - if (Regex.IsMatch(request.Url.LocalPath, "/v2(/.*)?")) - { - HandleRequestV2(context); - return; - } - - switch (request.HttpMethod) - { - case "GET": - HandleGet(request, response); - break; - case "POST": - HandlePost(request, response); - break; - case "DELETE": - HandleDelete(request, response); - break; - default: - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private Dictionary GetRequestVariables(string query) - { - Dictionary ret = new(); - Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*"); - if (!queryRex.IsMatch(query)) - return ret; - query = query.Substring(1); - foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3)) - { - string var = keyValuePair.Split('=')[0]; - string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " "); - val = Regex.Replace(val, "%[0-9]{2}", ""); - ret.Add(var, val); - } - return ret; - } - - private Dictionary GetRequestBody(HttpListenerRequest request) - { - if (!request.HasEntityBody) - { - Log("No request body"); - return new Dictionary(); - } - Stream body = request.InputStream; - Encoding encoding = request.ContentEncoding; - using StreamReader streamReader = new (body, encoding); - try - { - Dictionary requestBody = - JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) - ?? new(); - return requestBody; - } - catch (JsonException e) - { - Log(e.Message); - } - return new Dictionary(); - } - - private void HandleGet(HttpListenerRequest request, HttpListenerResponse response) - { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); - string? connectorName, jobId, internalId; - MangaConnector? connector; - Manga? manga; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - switch (path) - { - case "Connectors": - SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(con => con.name).ToArray()); - break; - case "Manga/Cover": - if (!requestVariables.TryGetValue("internalId", out internalId) || - !_parent.TryGetPublicationById(internalId, out manga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - string filePath = settings.GetFullCoverPath((Manga)manga!); - if (File.Exists(filePath)) - { - FileStream coverStream = new(filePath, FileMode.Open); - SendResponse(HttpStatusCode.OK, response, coverStream); - } - else - { - SendResponse(HttpStatusCode.NotFound, response); - } - break; - case "Manga/FromConnector": - requestVariables.TryGetValue("title", out string? title); - requestVariables.TryGetValue("url", out string? url); - if (!requestVariables.TryGetValue("connector", out connectorName) || - !_parent.TryGetConnector(connectorName, out connector) || - (title is null && url is null)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (url is not null) - { - HashSet ret = new(); - manga = connector!.GetMangaFromUrl(url); - if (manga is not null) - ret.Add((Manga)manga); - SendResponse(HttpStatusCode.OK, response, ret); - }else - SendResponse(HttpStatusCode.OK, response, connector!.GetManga(title!)); - break; - case "Manga/Chapters": - if(!requestVariables.TryGetValue("connector", out connectorName) || - !requestVariables.TryGetValue("internalId", out internalId) || - !_parent.TryGetConnector(connectorName, out connector) || - !_parent.TryGetPublicationById(internalId, out manga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - requestVariables.TryGetValue("translatedLanguage", out string? translatedLanguage); - SendResponse(HttpStatusCode.OK, response, connector!.GetChapters((Manga)manga!, translatedLanguage??"en")); - break; - case "Jobs": - if (!requestVariables.TryGetValue("jobId", out jobId)) - { - if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId)) - SendResponse(HttpStatusCode.BadRequest, response); - else - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId)); - break; - } - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs); - break; - case "Jobs/Progress": - if (requestVariables.TryGetValue("jobId", out jobId)) - { - if(!_parent.jobBoss.jobs.Any(jjob => jjob.id == jobId)) - SendResponse(HttpStatusCode.BadRequest, response); - else - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken); - break; - } - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Select(jjob => jjob.progressToken)); - break; - case "Jobs/Running": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running)); - break; - case "Jobs/Waiting": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby).OrderBy(jjob => jjob.nextExecution)); - break; - case "Jobs/MonitorJobs": - SendResponse(HttpStatusCode.OK, response, _parent.jobBoss.jobs.Where(jjob => jjob is DownloadNewChapters).OrderBy(jjob => ((DownloadNewChapters)jjob).manga.sortName)); - break; - case "Settings": - SendResponse(HttpStatusCode.OK, response, settings); - break; - case "Settings/userAgent": - SendResponse(HttpStatusCode.OK, response, settings.userAgent); - break; - case "Settings/customRequestLimit": - SendResponse(HttpStatusCode.OK, response, settings.requestLimits); - break; - case "Settings/AprilFoolsMode": - SendResponse(HttpStatusCode.OK, response, settings.aprilFoolsMode); - break; - case "NotificationConnectors": - SendResponse(HttpStatusCode.OK, response, notificationConnectors); - break; - case "NotificationConnectors/Types": - SendResponse(HttpStatusCode.OK, response, - Enum.GetValues().Select(nc => new KeyValuePair((byte)nc, Enum.GetName(nc)))); - break; - case "LibraryConnectors": - SendResponse(HttpStatusCode.OK, response, libraryConnectors); - break; - case "LibraryConnectors/Types": - SendResponse(HttpStatusCode.OK, response, - Enum.GetValues().Select(lc => new KeyValuePair((byte)lc, Enum.GetName(lc)))); - break; - case "Ping": - SendResponse(HttpStatusCode.OK, response, "Pong"); - break; - case "LogMessages": - if (logger is null || !File.Exists(logger?.logFilePath)) - { - SendResponse(HttpStatusCode.NotFound, response); - break; - } - - if (requestVariables.TryGetValue("count", out string? count)) - { - try - { - uint messageCount = uint.Parse(count); - SendResponse(HttpStatusCode.OK, response, logger.Tail(messageCount)); - } - catch (FormatException f) - { - SendResponse(HttpStatusCode.InternalServerError, response, f); - } - }else - SendResponse(HttpStatusCode.OK, response, logger.GetLog()); - break; - case "LogFile": - if (logger is null || !File.Exists(logger?.logFilePath)) - { - SendResponse(HttpStatusCode.NotFound, response); - break; - } - - string logDir = new FileInfo(logger.logFilePath).DirectoryName!; - string tmpFilePath = Path.Join(logDir, "Tranga.log"); - File.Copy(logger.logFilePath, tmpFilePath); - SendResponse(HttpStatusCode.OK, response, new FileStream(tmpFilePath, FileMode.Open)); - File.Delete(tmpFilePath); - break; - default: - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private void HandlePost(HttpListenerRequest request, HttpListenerResponse response) - { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI - Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body - Dictionary requestParams = new(); //The actual variable used for the API - - //Concatenate the two dictionaries for compatibility with older versions of front-ends - requestParams = requestVariables.Concat(requestBody).ToDictionary(x => x.Key, x => x.Value); - - string? connectorName, internalId, jobId, chapterNumStr, customFolderName, translatedLanguage, notificationConnectorStr, libraryConnectorStr; - MangaConnector? connector; - Manga? tmpManga; - Manga manga; - Job? job; - NotificationConnector.NotificationConnectorType notificationConnectorType; - LibraryConnector.LibraryType libraryConnectorType; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - switch (path) - { - case "Manga": - if(!requestParams.TryGetValue("internalId", out internalId) || - !_parent.TryGetPublicationById(internalId, out tmpManga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - manga = (Manga)tmpManga!; - SendResponse(HttpStatusCode.OK, response, manga); - break; - case "Jobs/MonitorManga": - if(!requestParams.TryGetValue("connector", out connectorName) || - !requestParams.TryGetValue("internalId", out internalId) || - !requestParams.TryGetValue("interval", out string? intervalStr) || - !_parent.TryGetConnector(connectorName, out connector)|| - !_parent.TryGetPublicationById(internalId, out tmpManga) || - !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - manga = (Manga)tmpManga!; - - if (requestParams.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) - { - if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - manga.ignoreChaptersBelow = chapterNum; - } - - if (requestParams.TryGetValue("customFolderName", out customFolderName)) - manga.MovePublicationFolder(settings.downloadLocation, customFolderName); - requestParams.TryGetValue("translatedLanguage", out translatedLanguage); - - _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, true, interval, translatedLanguage: translatedLanguage??"en")); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/DownloadNewChapters": - if(!requestParams.TryGetValue("connector", out connectorName) || - !requestParams.TryGetValue("internalId", out internalId) || - !_parent.TryGetConnector(connectorName, out connector)|| - !_parent.TryGetPublicationById(internalId, out tmpManga)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - manga = (Manga)tmpManga!; - - if (requestParams.TryGetValue("ignoreBelowChapterNum", out chapterNumStr)) - { - if (!float.TryParse(chapterNumStr, numberFormatDecimalPoint, out float chapterNum)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - manga.ignoreChaptersBelow = chapterNum; - } - - if (requestParams.TryGetValue("customFolderName", out customFolderName)) - manga.MovePublicationFolder(settings.downloadLocation, customFolderName); - requestParams.TryGetValue("translatedLanguage", out translatedLanguage); - - _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector!, manga, false, translatedLanguage: translatedLanguage??"en")); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/UpdateMetadata": - if (!requestParams.TryGetValue("internalId", out internalId)) - { - foreach (Job pJob in _parent.jobBoss.jobs.Where(possibleDncJob => - possibleDncJob.jobType is Job.JobType.DownloadNewChaptersJob).ToArray())//ToArray to avoid modyifying while adding new jobs - { - DownloadNewChapters dncJob = pJob as DownloadNewChapters ?? - throw new Exception("Has to be DownloadNewChapters Job"); - _parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga)); - } - SendResponse(HttpStatusCode.Accepted, response); - } - else - { - Job[] possibleDncJobs = _parent.jobBoss.GetJobsLike(internalId: internalId).ToArray(); - switch (possibleDncJobs.Length) - { - case <1: SendResponse(HttpStatusCode.BadRequest, response, "Could not find matching release"); break; - case >1: SendResponse(HttpStatusCode.BadRequest, response, "Multiple releases??"); break; - default: - DownloadNewChapters dncJob = possibleDncJobs[0] as DownloadNewChapters ?? - throw new Exception("Has to be DownloadNewChapters Job"); - _parent.jobBoss.AddJob(new UpdateMetadata(this, dncJob.mangaConnector, dncJob.manga)); - SendResponse(HttpStatusCode.Accepted, response); - break; - } - } - break; - case "Jobs/StartNow": - if (!requestParams.TryGetValue("jobId", out jobId) || - !_parent.jobBoss.TryGetJobById(jobId, out job)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - _parent.jobBoss.AddJobToQueue(job!); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/Cancel": - if (!requestParams.TryGetValue("jobId", out jobId) || - !_parent.jobBoss.TryGetJobById(jobId, out job)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - job!.Cancel(); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/UpdateDownloadLocation": - if (!requestParams.TryGetValue("downloadLocation", out string? downloadLocation) || - !requestParams.TryGetValue("moveFiles", out string? moveFilesStr) || - !bool.TryParse(moveFilesStr, out bool moveFiles)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateDownloadLocation(downloadLocation, moveFiles); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/AprilFoolsMode": - if (!requestParams.TryGetValue("enabled", out string? aprilFoolsModeEnabledStr) || - bool.TryParse(aprilFoolsModeEnabledStr, out bool aprilFoolsModeEnabled)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateAprilFoolsMode(aprilFoolsModeEnabled); - SendResponse(HttpStatusCode.Accepted, response); - break; - /*case "Settings/UpdateWorkingDirectory": - if (!requestParams.TryGetValue("workingDirectory", out string? workingDirectory)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateWorkingDirectory(workingDirectory); - SendResponse(HttpStatusCode.Accepted, response); - break;*/ - case "Settings/userAgent": - if(!requestParams.TryGetValue("userAgent", out string? customUserAgent)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - settings.UpdateUserAgent(customUserAgent); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/userAgent/Reset": - settings.UpdateUserAgent(null); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Settings/customRequestLimit": - if (!requestParams.TryGetValue("requestType", out string? requestTypeStr) || - !requestParams.TryGetValue("requestsPerMinute", out string? requestsPerMinuteStr) || - !Enum.TryParse(requestTypeStr, out RequestType requestType) || - !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (settings.requestLimits.ContainsKey(requestType)) - { - settings.requestLimits[requestType] = requestsPerMinute; - SendResponse(HttpStatusCode.Accepted, response); - }else - SendResponse(HttpStatusCode.BadRequest, response); - settings.ExportSettings(); - break; - case "Settings/customRequestLimit/Reset": - settings.requestLimits = TrangaSettings.DefaultRequestLimits; - settings.ExportSettings(); - break; - case "NotificationConnectors/Update": - if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) - { - if (!requestParams.TryGetValue("gotifyUrl", out string? gotifyUrl) || - !requestParams.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddNotificationConnector(new Gotify(this, gotifyUrl, gotifyAppToken)); - SendResponse(HttpStatusCode.Accepted, response); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) - { - if (!requestParams.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddNotificationConnector(new LunaSea(this, lunaseaWebhook)); - SendResponse(HttpStatusCode.Accepted, response); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) - { - if (!requestParams.TryGetValue("ntfyUrl", out string? ntfyUrl) || - !requestParams.TryGetValue("ntfyAuth", out string? ntfyAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddNotificationConnector(new Ntfy(this, ntfyUrl, ntfyAuth)); - SendResponse(HttpStatusCode.Accepted, response); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - } - break; - case "NotificationConnectors/Test": - NotificationConnector notificationConnector; - if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Gotify) - { - if (!requestParams.TryGetValue("gotifyUrl", out string? gotifyUrl) || - !requestParams.TryGetValue("gotifyAppToken", out string? gotifyAppToken)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - notificationConnector = new Gotify(this, gotifyUrl, gotifyAppToken); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.LunaSea) - { - if (!requestParams.TryGetValue("lunaseaWebhook", out string? lunaseaWebhook)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - notificationConnector = new LunaSea(this, lunaseaWebhook); - }else if (notificationConnectorType is NotificationConnector.NotificationConnectorType.Ntfy) - { - if (!requestParams.TryGetValue("ntfyUrl", out string? ntfyUrl) || - !requestParams.TryGetValue("ntfyAuth", out string? ntfyAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - notificationConnector = new Ntfy(this, ntfyUrl, ntfyAuth); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - notificationConnector.SendNotification("Tranga Test", "This is Test-Notification."); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "NotificationConnectors/Reset": - if (!requestParams.TryGetValue("notificationConnector", out notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteNotificationConnector(notificationConnectorType); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "LibraryConnectors/Update": - if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) - { - if (!requestParams.TryGetValue("kavitaUrl", out string? kavitaUrl) || - !requestParams.TryGetValue("kavitaUsername", out string? kavitaUsername) || - !requestParams.TryGetValue("kavitaPassword", out string? kavitaPassword)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddLibraryConnector(new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword)); - SendResponse(HttpStatusCode.Accepted, response); - }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) - { - if (!requestParams.TryGetValue("komgaUrl", out string? komgaUrl) || - !requestParams.TryGetValue("komgaAuth", out string? komgaAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - AddLibraryConnector(new Komga(this, komgaUrl, komgaAuth)); - SendResponse(HttpStatusCode.Accepted, response); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - } - break; - case "LibraryConnectors/Test": - LibraryConnector libraryConnector; - if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - - if (libraryConnectorType is LibraryConnector.LibraryType.Kavita) - { - if (!requestParams.TryGetValue("kavitaUrl", out string? kavitaUrl) || - !requestParams.TryGetValue("kavitaUsername", out string? kavitaUsername) || - !requestParams.TryGetValue("kavitaPassword", out string? kavitaPassword)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - libraryConnector = new Kavita(this, kavitaUrl, kavitaUsername, kavitaPassword); - }else if (libraryConnectorType is LibraryConnector.LibraryType.Komga) - { - if (!requestParams.TryGetValue("komgaUrl", out string? komgaUrl) || - !requestParams.TryGetValue("komgaAuth", out string? komgaAuth)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - libraryConnector = new Komga(this, komgaUrl, komgaAuth); - } - else - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - libraryConnector.UpdateLibrary(); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "LibraryConnectors/Reset": - if (!requestParams.TryGetValue("libraryConnector", out libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, out libraryConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteLibraryConnector(libraryConnectorType); - SendResponse(HttpStatusCode.Accepted, response); - break; - default: - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response) - { - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI - Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body - Dictionary requestParams = new(); //The actual variable used for the API - - //Concatenate the two dictionaries for compatibility with older versions of front-ends - requestParams = requestVariables.Concat(requestBody).ToDictionary(x => x.Key, x => x.Value); - - string? connectorName, internalId; - MangaConnector connector; - Manga manga; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - switch (path) - { - case "Jobs": - if (!requestParams.TryGetValue("jobId", out string? jobId) || - !_parent.jobBoss.TryGetJobById(jobId, out Job? job)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - _parent.jobBoss.RemoveJob(job!); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "Jobs/DownloadNewChapters": - if(!requestParams.TryGetValue("connector", out connectorName) || - !requestParams.TryGetValue("internalId", out internalId) || - _parent.GetConnector(connectorName) is null || - _parent.GetPublicationById(internalId) is null) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - connector = _parent.GetConnector(connectorName)!; - manga = (Manga)_parent.GetPublicationById(internalId)!; - _parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga)); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "NotificationConnectors": - if (!requestParams.TryGetValue("notificationConnector", out string? notificationConnectorStr) || - !Enum.TryParse(notificationConnectorStr, out NotificationConnector.NotificationConnectorType notificationConnectorType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteNotificationConnector(notificationConnectorType); - SendResponse(HttpStatusCode.Accepted, response); - break; - case "LibraryConnectors": - if (!requestParams.TryGetValue("libraryConnector", out string? libraryConnectorStr) || - !Enum.TryParse(libraryConnectorStr, - out LibraryConnector.LibraryType libraryConnectoryType)) - { - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - DeleteLibraryConnector(libraryConnectoryType); - SendResponse(HttpStatusCode.Accepted, response); - break; - default: - Log("Invalid Request:"); - Log(request.Url!.Query); - foreach (KeyValuePair kvp in requestParams) - { - Log("Request variable = {0}, Variable Value = {1}", kvp.Key, kvp.Value); - } - SendResponse(HttpStatusCode.BadRequest, response); - break; - } - } - - private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null) - { - //Log($"Response: {statusCode} {content}"); - response.StatusCode = (int)statusCode; - response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); - response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE"); - response.AddHeader("Access-Control-Max-Age", "1728000"); - response.AppendHeader("Access-Control-Allow-Origin", "*"); - - if (content is not Stream) - { - response.ContentType = "application/json"; - try - { - response.OutputStream.Write(content is not null - ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) - : Array.Empty()); - response.OutputStream.Close(); - } - catch (HttpListenerException e) - { - Log(e.ToString()); - } - } - else if(content is FileStream stream) - { - string contentType = stream.Name.Split('.')[^1]; - switch (contentType.ToLower()) - { - case "gif": - response.ContentType = "image/gif"; - break; - case "png": - response.ContentType = "image/png"; - break; - case "jpg": - case "jpeg": - response.ContentType = "image/jpeg"; - break; - case "log": - response.ContentType = "text/plain"; - break; - } - stream.CopyTo(response.OutputStream); - response.OutputStream.Close(); - stream.Close(); - } - } -} \ No newline at end of file diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs new file mode 100644 index 00000000..d090aff4 --- /dev/null +++ b/Tranga/Server/Server.cs @@ -0,0 +1,196 @@ +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace Tranga.Server; + +public class Server : GlobalBase, IDisposable +{ + private readonly HttpListener _listener = new(); + private readonly Tranga _parent; + private bool _running = true; + + public Server(Tranga parent) : base(parent) + { + this._parent = parent; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); + else + this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/"); + Thread listenThread = new(Listen); + listenThread.Start(); + while(_parent.keepRunning && _running) + Thread.Sleep(100); + this.Dispose(); + } + + private void Listen() + { + this._listener.Start(); + foreach (string prefix in this._listener.Prefixes) + Log($"Listening on {prefix}"); + while (this._listener.IsListening && _parent.keepRunning) + { + try + { + HttpListenerContext context = this._listener.GetContext(); + //Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}"); + Task t = new(() => + { + HandleRequest(context); + }); + t.Start(); + } + catch (HttpListenerException) + { + + } + } + } + + private void HandleRequest(HttpListenerContext context) + { + HttpListenerRequest request = context.Request; + HttpListenerResponse response = context.Response; + if (request.HttpMethod == "OPTIONS") + SendResponse(HttpStatusCode.OK, context.Response); + if (request.Url!.LocalPath.Contains("favicon")) + SendResponse(HttpStatusCode.NoContent, response); + string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; + + if (!Regex.IsMatch(request.Url.LocalPath, "/v2(/.*)?")) + { + SendResponse(HttpStatusCode.NotFound, response); + return; + } + + Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI + Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body + Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) + .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API + + ValueTuple responseMessage = request.HttpMethod switch + { + "GET" => HandleGet(path, requestParams), + "POST" => HandlePost(path, requestParams), + "DELETE" => HandleDelete(path, requestParams), + _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) + }; + + SendResponse(responseMessage.Item1, response, responseMessage.Item2); + } + + private Dictionary GetRequestVariables(string query) + { + Dictionary ret = new(); + Regex queryRex = new(@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*"); + if (!queryRex.IsMatch(query)) + return ret; + query = query.Substring(1); + foreach (string keyValuePair in query.Split('&').Where(str => str.Length >= 3)) + { + string var = keyValuePair.Split('=')[0]; + string val = Regex.Replace(keyValuePair.Substring(var.Length + 1), "%20", " "); + val = Regex.Replace(val, "%[0-9]{2}", ""); + ret.Add(var, val); + } + return ret; + } + + private Dictionary GetRequestBody(HttpListenerRequest request) + { + if (!request.HasEntityBody) + { + Log("No request body"); + return new Dictionary(); + } + Stream body = request.InputStream; + Encoding encoding = request.ContentEncoding; + using StreamReader streamReader = new (body, encoding); + try + { + Dictionary requestBody = + JsonConvert.DeserializeObject>(streamReader.ReadToEnd()) + ?? new(); + return requestBody; + } + catch (JsonException e) + { + Log(e.Message); + } + return new Dictionary(); + } + + private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null) + { + //Log($"Response: {statusCode} {content}"); + response.StatusCode = (int)statusCode; + response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); + response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE"); + response.AddHeader("Access-Control-Max-Age", "1728000"); + response.AppendHeader("Access-Control-Allow-Origin", "*"); + + if (content is not Stream) + { + response.ContentType = "application/json"; + try + { + response.OutputStream.Write(content is not null + ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) + : Array.Empty()); + response.OutputStream.Close(); + } + catch (HttpListenerException e) + { + Log(e.ToString()); + } + } + else if(content is FileStream stream) + { + string contentType = stream.Name.Split('.')[^1]; + switch (contentType.ToLower()) + { + case "gif": + response.ContentType = "image/gif"; + break; + case "png": + response.ContentType = "image/png"; + break; + case "jpg": + case "jpeg": + response.ContentType = "image/jpeg"; + break; + case "log": + response.ContentType = "text/plain"; + break; + } + stream.CopyTo(response.OutputStream); + response.OutputStream.Close(); + stream.Close(); + } + } + + private ValueTuple HandleGet(string path, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); + } + + private ValueTuple HandlePost(string path, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); + } + + private ValueTuple HandleDelete(string path, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); + } + + + public void Dispose() + { + _running = false; + ((IDisposable)_listener).Dispose(); + } +} \ No newline at end of file diff --git a/Tranga/ServerV2.cs b/Tranga/ServerV2.cs deleted file mode 100644 index 092a8cb4..00000000 --- a/Tranga/ServerV2.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Net; -using System.Text.RegularExpressions; - -namespace Tranga; - -public partial class Server -{ - private void HandleRequestV2(HttpListenerContext context) - { - HttpListenerRequest request = context.Request; - HttpListenerResponse response = context.Response; - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; - - Dictionary requestVariables = GetRequestVariables(request.Url!.Query); //Variables in the URI - Dictionary requestBody = GetRequestBody(request); //Variables in the JSON body - Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) - .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API - - ValueTuple responseMessage = request.HttpMethod switch - { - "GET" => HandleGetV2(path, requestParams), - "POST" => HandlePostV2(path, requestParams), - "DELETE" => HandleDeleteV2(path, requestParams), - _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) - }; - - SendResponse(responseMessage.Item1, response, responseMessage.Item2); - } - - private ValueTuple HandleGetV2(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } - - private ValueTuple HandlePostV2(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } - - private ValueTuple HandleDeleteV2(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } -} \ No newline at end of file diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index cd3b3bc6..91b1f193 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -8,7 +8,7 @@ public partial class Tranga : GlobalBase { public bool keepRunning; public JobBoss jobBoss; - private Server _server; + private Server.Server _server; private HashSet _connectors; public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings) @@ -30,7 +30,7 @@ public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings) dir.Delete(); jobBoss = new(this, this._connectors); StartJobBoss(); - this._server = new Server(this); + this._server = new Server.Server(this); string[] emojis = { "(•‿•)", "(づ \u25d5‿\u25d5 )づ", "( \u02d8\u25bd\u02d8)っ\u2668", "=\uff3e\u25cf \u22cf \u25cf\uff3e=", "(ΦωΦ)", "(\u272a\u3268\u272a)", "( ノ・o・ )ノ", "(〜^\u2207^ )〜", "~(\u2267ω\u2266)~","૮ \u00b4• ﻌ \u00b4• ა", "(\u02c3ᆺ\u02c2)", "(=\ud83d\udf66 \u0f1d \ud83d\udf66=)"}; SendNotifications("Tranga Started", emojis[Random.Shared.Next(0,emojis.Length-1)]); } From 630e5075641ad36848f74adee5119b348a718bb0 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 16:57:06 +0200 Subject: [PATCH 16/95] #74 API Documentation --- docs/API_Calls_v2.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 00b0cb98..f4934c08 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -154,6 +154,28 @@ Returns the Chapter-list for the specified Manga. | 404 | Manga with `internalId` could not be found | +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga//Chapters/latest` + +Returns the latest Chapter of the specified Manga. + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Jobs/*jobId*](#-v2jobs) +
+ +
+ Returns + + [Chapter](Types.md/#chapter) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | +
+ ## Jobs [^top](#top) ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs` From 011af9c7a8ccef394810826b09ab6313452d2ced Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 16:59:51 +0200 Subject: [PATCH 17/95] #114 API Documentation --- docs/API_Calls_v2.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index f4934c08..34a5c934 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -416,6 +416,28 @@ Cancels the specified Job, or dequeues it. | 404 | Manga with `jobId` could not be found | +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//SetInterval` + +Edits the specified Job. + +
+ Request + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 404 | Manga with `jobId` could not be found | +
+ ## Settings [^top](#top) ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings` From 5e647099cdb7518d952544f6f05bbaa262e6be62 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 17:56:54 +0200 Subject: [PATCH 18/95] Spelling --- docs/API_Calls_v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 34a5c934..bbd9b6a4 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -568,7 +568,7 @@ Returns the current state of the April-Fools-Mode setting. Boolean -### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/ApriFoolsMode` +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/AprilFoolsMode` Enables/Disables April-Fools-Mode. From c41f04d92d810f701947f1348ff05c32d3b60946 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 18:34:20 +0200 Subject: [PATCH 19/95] All Valid Request Paths return "Not Implemented". Ping returns Pong. --- Tranga/Server/RequestPath.cs | 19 +++++ Tranga/Server/Server.cs | 94 ++++++++++++++++------- Tranga/Server/v2Connector.cs | 19 +++++ Tranga/Server/v2Jobs.cs | 73 ++++++++++++++++++ Tranga/Server/v2LibraryConnectors.cs | 37 +++++++++ Tranga/Server/v2Manga.cs | 27 +++++++ Tranga/Server/v2Miscellaneous.cs | 22 ++++++ Tranga/Server/v2NotificationConnectors.cs | 37 +++++++++ Tranga/Server/v2Settings.cs | 62 +++++++++++++++ Tranga/Tranga.cs | 4 +- 10 files changed, 366 insertions(+), 28 deletions(-) create mode 100644 Tranga/Server/RequestPath.cs create mode 100644 Tranga/Server/v2Connector.cs create mode 100644 Tranga/Server/v2Jobs.cs create mode 100644 Tranga/Server/v2LibraryConnectors.cs create mode 100644 Tranga/Server/v2Manga.cs create mode 100644 Tranga/Server/v2Miscellaneous.cs create mode 100644 Tranga/Server/v2NotificationConnectors.cs create mode 100644 Tranga/Server/v2Settings.cs diff --git a/Tranga/Server/RequestPath.cs b/Tranga/Server/RequestPath.cs new file mode 100644 index 00000000..c8db596b --- /dev/null +++ b/Tranga/Server/RequestPath.cs @@ -0,0 +1,19 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +internal struct RequestPath +{ + internal readonly string HttpMethod; + internal readonly string RegexStr; + internal readonly Func, ValueTuple> Method; + + public RequestPath(string httpHttpMethod, string regexStr, + Func, ValueTuple> method) + { + this.HttpMethod = httpHttpMethod; + this.RegexStr = regexStr; + this.Method = method; + } +} \ No newline at end of file diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index d090aff4..8f35f86a 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -6,14 +6,68 @@ namespace Tranga.Server; -public class Server : GlobalBase, IDisposable +public partial class Server : GlobalBase, IDisposable { private readonly HttpListener _listener = new(); private readonly Tranga _parent; private bool _running = true; + private readonly List _apiRequestPaths; + public Server(Tranga parent) : base(parent) { + /* + * Contains all valid Request Methods, Paths (with Regex Group Matching for specific Parameters) and Handling Methods + */ + _apiRequestPaths = new List + { + new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes), + new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga), + new ("GET", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})", GetV2MangaInternalId), + new ("DELETE", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})", DeleteV2MangaInternalId), + new ("GET", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})/Cover", GetV2MangaInternalIdCover), + new ("GET", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})/Chapters", GetV2MangaInternalIdChapters), + new ("GET", @"/v2/Jobs", GetV2Jobs), + new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), + new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), + new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), + new ("POST", @"/v2/Jobs/Create/Monitor/(^[-A-Za-z0-9+/]*={0,3}$)", PostV2JobsCreateMonitorInternalId), + new ("POST", @"/v2/Jobs/Create/DownloadNewChapters/(^[-A-Za-z0-9+/]*={0,3}$)", PostV2JobsCreateDownloadNewChaptersInternalId), + new ("POST", @"/v2/Jobs/Create/UpdateMetadata", PostV2JobsCreateUpdateMetadata), + new ("POST", @"/v2/Jobs/Create/UpdateMetadata/(^[-A-Za-z0-9+/]*={0,3}$)", PostV2JobsCreateUpdateMetadataInternalId), + new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), + new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), + new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), + new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/StartNow", PostV2JobJobIdStartNow), + new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Cancel", PostV2JobJobIdCancel), + new ("GET", @"/v2/Settings", GetV2Settings), + new ("GET", @"/v2/Settings/UserAgent", GetV2SettingsUserAgent), + new ("POST", @"/v2/Settings/UserAgent", PostV2SettingsUserAgent), + new ("GET", @"/v2/Settings/RateLimit/Types", GetV2SettingsRateLimitTypes), + new ("GET", @"/v2/Settings/RateLimit", GetV2SettingsRateLimit), + new ("POST", @"/v2/Settings/RateLimit", PostV2SettingsRateLimit), + new ("GET", @"/v2/Settings/RateLimit/([a-zA-Z]+)", GetV2SettingsRateLimitType), + new ("POST", @"/v2/Settings/RateLimit/([a-zA-Z]+)", PostV2SettingsRateLimitType), + new ("GET", @"/v2/Settings/AprilFoolsMode", GetV2SettingsAprilFoolsMode), + new ("POST", @"/v2/Settings/AprilFoolsMode", PostV2SettingsAprilFoolsMode), + new ("POST", @"/v2/Settings/DownloadLocation", PostV2SettingsDownloadLocation), + new ("GET", @"/v2/LibraryConnector", GetV2LibraryConnector), + new ("GET", @"/v2/LibraryConnector/Types", GetV2LibraryConnectorTypes), + new ("GET", @"/v2/LibraryConnector/([a-zA-Z]+)", GetV2LibraryConnectorType), + new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)", PostV2LibraryConnectorType), + new ("POST", @"/v2/LibraryConnector/([a-zA-Z]+)/Test", PostV2LibraryConnectorTypeTest), + new ("DELETE", @"/v2/LibraryConnector/([a-zA-Z]+)", DeleteV2LibraryConnectorType), + new ("GET", @"/v2/NotificationConnector", GetV2NotificationConnector), + new ("GET", @"/v2/NotificationConnector/Types", GetV2NotificationConnectorTypes), + new ("GET", @"/v2/NotificationConnector/([a-zA-Z]+)", GetV2NotificationConnectorType), + new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)", PostV2NotificationConnectorType), + new ("POST", @"/v2/NotificationConnector/([a-zA-Z]+)/Test", PostV2NotificationConnectorTypeTest), + new ("DELETE", @"/v2/NotificationConnector/([a-zA-Z]+)", DeleteV2NotificationConnectorType), + new ("GET", @"/v2/LogFile", GetV2LogFile), + new ("GET", @"/v2/Ping", GetV2Ping), + new ("POST", @"/v2/Ping", PostV2Ping) + }; + this._parent = parent; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); @@ -55,14 +109,14 @@ private void HandleRequest(HttpListenerContext context) HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; if (request.HttpMethod == "OPTIONS") - SendResponse(HttpStatusCode.OK, context.Response); + SendResponse(HttpStatusCode.OK, context.Response); //Response always contains all valid Request-Methods if (request.Url!.LocalPath.Contains("favicon")) SendResponse(HttpStatusCode.NoContent, response); - string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value; + string path = Regex.Match(request.Url.LocalPath, @"\/[A-z0-9]+(\/[A-z0-9]+)*").Value; //Local Path - if (!Regex.IsMatch(request.Url.LocalPath, "/v2(/.*)?")) + if (!Regex.IsMatch(path, "/v2(/.*)?")) //Use only v2 API { - SendResponse(HttpStatusCode.NotFound, response); + SendResponse(HttpStatusCode.NotFound, response, "Use Version 2 API"); return; } @@ -71,13 +125,16 @@ private void HandleRequest(HttpListenerContext context) Dictionary requestParams = requestVariables.UnionBy(requestBody, v => v.Key) .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API - ValueTuple responseMessage = request.HttpMethod switch + ValueTuple responseMessage; //Used to respond to the HttpRequest + if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.IsMatch(path, p.RegexStr))) //Check if Request-Path is valid { - "GET" => HandleGet(path, requestParams), - "POST" => HandlePost(path, requestParams), - "DELETE" => HandleDelete(path, requestParams), - _ => new ValueTuple(HttpStatusCode.MethodNotAllowed, null) - }; + RequestPath requestPath = + _apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.IsMatch(path, p.RegexStr)); + responseMessage = + requestPath.Method.Invoke(Regex.Match(path, requestPath.RegexStr).Groups, requestParams); //Get HttpResponse content + } + else + responseMessage = new ValueTuple(HttpStatusCode.MethodNotAllowed, "Unknown Request Path"); SendResponse(responseMessage.Item1, response, responseMessage.Item2); } @@ -171,21 +228,6 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon stream.Close(); } } - - private ValueTuple HandleGet(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } - - private ValueTuple HandlePost(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } - - private ValueTuple HandleDelete(string path, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not implemented."); - } public void Dispose() diff --git a/Tranga/Server/v2Connector.cs b/Tranga/Server/v2Connector.cs new file mode 100644 index 00000000..ac3b38ac --- /dev/null +++ b/Tranga/Server/v2Connector.cs @@ -0,0 +1,19 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2ConnectorTypes(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.Accepted, _parent.GetConnectors()); + } + + private ValueTuple GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary requestParameters) + { + if(groups.Count < 1 || !_parent.GetConnectors().Contains(groups[1].Value)) + return new ValueTuple(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist."); + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } +} \ No newline at end of file diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs new file mode 100644 index 00000000..5a6813fb --- /dev/null +++ b/Tranga/Server/v2Jobs.cs @@ -0,0 +1,73 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2Jobs(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2JobsRunning(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2JobsWaiting(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2JobsMonitoring(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2JobsCreateMonitorInternalId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2JobsCreateDownloadNewChaptersInternalId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2JobsCreateUpdateMetadata(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2JobsCreateUpdateMetadataInternalId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2JobJobId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple DeleteV2JobJobId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2JobJobIdProgress(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2JobJobIdStartNow(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2JobJobIdCancel(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + +} \ No newline at end of file diff --git a/Tranga/Server/v2LibraryConnectors.cs b/Tranga/Server/v2LibraryConnectors.cs new file mode 100644 index 00000000..5ee6287b --- /dev/null +++ b/Tranga/Server/v2LibraryConnectors.cs @@ -0,0 +1,37 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2LibraryConnector(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2LibraryConnectorTypes(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2LibraryConnectorTypeTest(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple DeleteV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } +} \ No newline at end of file diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs new file mode 100644 index 00000000..834cb4f9 --- /dev/null +++ b/Tranga/Server/v2Manga.cs @@ -0,0 +1,27 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple DeleteV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2MangaInternalIdCover(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } +} \ No newline at end of file diff --git a/Tranga/Server/v2Miscellaneous.cs b/Tranga/Server/v2Miscellaneous.cs new file mode 100644 index 00000000..e0ce1a8c --- /dev/null +++ b/Tranga/Server/v2Miscellaneous.cs @@ -0,0 +1,22 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2LogFile(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2Ping(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.Accepted, "Pong!"); + } + + private ValueTuple PostV2Ping(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.Accepted, "Pong!"); + } +} \ No newline at end of file diff --git a/Tranga/Server/v2NotificationConnectors.cs b/Tranga/Server/v2NotificationConnectors.cs new file mode 100644 index 00000000..7d7cb731 --- /dev/null +++ b/Tranga/Server/v2NotificationConnectors.cs @@ -0,0 +1,37 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2NotificationConnector(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2NotificationConnectorTypes(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2NotificationConnectorTypeTest(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple DeleteV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } +} \ No newline at end of file diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs new file mode 100644 index 00000000..6615726e --- /dev/null +++ b/Tranga/Server/v2Settings.cs @@ -0,0 +1,62 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Tranga.Server; + +public partial class Server +{ + private ValueTuple GetV2Settings(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2SettingsRateLimitTypes(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple GetV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } + + private ValueTuple PostV2SettingsDownloadLocation(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + } +} \ No newline at end of file diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index 91b1f193..4f286703 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -49,9 +49,9 @@ public bool TryGetConnector(string name, out MangaConnector? connector) return connector is not null; } - public IEnumerable GetConnectors() + public IEnumerable GetConnectors() { - return _connectors; + return _connectors.Select(c => c.name); } public Manga? GetPublicationById(string internalId) From e96dd07521c39b0efd26c7d6717d461aafb4711a Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 18:41:12 +0200 Subject: [PATCH 20/95] Link API Documentation in README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05d6aeeb..33d94cc3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-

Tranga

+

Tranga v2

Automatic Manga and Metadata downloader @@ -58,7 +58,8 @@ Notifications can be sent to your devices using [Gotify](https://gotify.net/) an ### What this does and doesn't do Tranga (this git-repo) will open a port (standard 6531) and listen for requests to add Jobs to Monitor and/or download specific Manga. -The configuration is all done through HTTP-Requests. +The configuration is all done through HTTP-Requests. [Documentation](docs/API_Calls_v2.md) + _**For a web-frontend use [tranga-website](https://github.com/C9Glax/tranga-website).**_ This project downloads the images for a Manga from the specified Scanlation-Website and packages them with some metadata - from that same website - in a .cbz-archive (per chapter). From 9a02859f6b1ec45c01b1198384ca93c824a0a86e Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 18:46:00 +0200 Subject: [PATCH 21/95] Docker Image build --- .github/workflows/docker-image-serverv2.yml | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/docker-image-serverv2.yml diff --git a/.github/workflows/docker-image-serverv2.yml b/.github/workflows/docker-image-serverv2.yml new file mode 100644 index 00000000..a89b05a7 --- /dev/null +++ b/.github/workflows/docker-image-serverv2.yml @@ -0,0 +1,45 @@ +name: Docker Image CI + +on: + push: + branches: [ "Server-V2" ] + workflow_dispatch: + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # https://github.com/docker/setup-qemu-action#usage + - name: Set up QEMU + uses: docker/setup-qemu-action@v2.2.0 + + # https://github.com/marketplace/actions/docker-setup-buildx + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3.3.0 + + # https://github.com/docker/login-action#docker-hub + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # https://github.com/docker/build-push-action#multi-platform-image + - name: Build and push API + uses: docker/build-push-action@v5.3.0 + with: + context: ./ + file: ./Dockerfile + #platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 + platforms: linux/amd64 + pull: true + push: true + tags: | + glax/tranga-api:Server-V2 \ No newline at end of file From b3fb53f6d817bdfeb57d12a5497ff5c79328a766 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 20 Apr 2024 18:55:54 +0200 Subject: [PATCH 22/95] Corrected link --- docs/API_Calls_v2.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index bbd9b6a4..d39a8401 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -76,7 +76,7 @@ Returns the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid)

@@ -98,7 +98,7 @@ Deletes all associated Jobs for the specified Manga `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid)
@@ -118,8 +118,8 @@ Returns the URL for the Cover of the specified Manga. Request `internalId` is returned in the response of - * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid)
@@ -141,7 +141,7 @@ Returns the Chapter-list for the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid)
@@ -163,7 +163,7 @@ Returns the latest Chapter of the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid)
@@ -227,7 +227,7 @@ Creates a Monitoring-Job for the specified Manga at the specified Interval. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid) | Parameter | Value | |-----------|--------------------------------------------------------| @@ -254,7 +254,7 @@ Creates a Job to check for new Chapters and Download new ones of the specified M `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid)
@@ -286,7 +286,7 @@ Updates the Metadata of the specified Manga. `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Jobs/*jobId*](#-v2jobs) + * [GET /v2/Job/*jobId*](#-v2jobjobid)
From 4cb7c941a24f6b91e6fbd89323d225a7be8f1bc2 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 21 Apr 2024 21:32:03 +0200 Subject: [PATCH 23/95] Implemented /v2/Connector//GetManga --- Tranga/Server/v2Connector.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Tranga/Server/v2Connector.cs b/Tranga/Server/v2Connector.cs index ac3b38ac..29d45779 100644 --- a/Tranga/Server/v2Connector.cs +++ b/Tranga/Server/v2Connector.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.RegularExpressions; +using Tranga.MangaConnectors; namespace Tranga.Server; @@ -12,8 +13,19 @@ public partial class Server private ValueTuple GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary requestParameters) { - if(groups.Count < 1 || !_parent.GetConnectors().Contains(groups[1].Value)) + if(groups.Count < 1 || + !_parent.GetConnectors().Contains(groups[1].Value) || + !_parent.TryGetConnector(groups[1].Value, out MangaConnector? connector) || + connector is null) return new ValueTuple(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist."); - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + + if (requestParameters.TryGetValue("title", out string? title)) + { + return (HttpStatusCode.OK, connector.GetManga(title)); + }else if (requestParameters.TryGetValue("url", out string? url)) + { + return (HttpStatusCode.OK, connector.GetMangaFromUrl(url)); + }else + return new ValueTuple(HttpStatusCode.BadRequest, "Parameter 'title' or 'url' has to be set."); } } \ No newline at end of file From 4104169c197d4d6d4ca9f2a3eea17290a00286a8 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 21 Apr 2024 21:46:52 +0200 Subject: [PATCH 24/95] Fix path excluding symbols that are used in requests --- Tranga/Server/Server.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 8f35f86a..30aeafd7 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -112,7 +112,7 @@ private void HandleRequest(HttpListenerContext context) SendResponse(HttpStatusCode.OK, context.Response); //Response always contains all valid Request-Methods if (request.Url!.LocalPath.Contains("favicon")) SendResponse(HttpStatusCode.NoContent, response); - string path = Regex.Match(request.Url.LocalPath, @"\/[A-z0-9]+(\/[A-z0-9]+)*").Value; //Local Path + string path = Regex.Match(request.Url.LocalPath, @"\/[a-zA-Z0-9\.+/=-]+(\/[a-zA-Z0-9\.+/=-]+)*").Value; //Local Path if (!Regex.IsMatch(path, "/v2(/.*)?")) //Use only v2 API { From 6d48a100ca5b3cc3f44c7ba8b05c1338c4ee0ef3 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 21 Apr 2024 21:48:11 +0200 Subject: [PATCH 25/95] Implemented GET /v2/Jobs /v2/Jobs/Running /v2/Jobs/Waiting /v2/Jobs/Monitoring /v2/Job/ /v2/Job//Progress --- Tranga/Server/v2Jobs.cs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index 5a6813fb..e9c4bf9e 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.RegularExpressions; +using Tranga.Jobs; namespace Tranga.Server; @@ -7,22 +8,28 @@ public partial class Server { private ValueTuple GetV2Jobs(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs.Select(job => job.id)); } private ValueTuple GetV2JobsRunning(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs + .Where(job => job.progressToken.state is ProgressToken.State.Running) + .Select(job => job.id)); } private ValueTuple GetV2JobsWaiting(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs + .Where(job => job.progressToken.state is ProgressToken.State.Waiting) + .Select(job => job.id)); } private ValueTuple GetV2JobsMonitoring(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs + .Where(job => job.jobType is Job.JobType.DownloadNewChaptersJob) + .Select(job => job.id)); } private ValueTuple PostV2JobsCreateMonitorInternalId(GroupCollection groups, Dictionary requestParameters) @@ -47,7 +54,13 @@ public partial class Server private ValueTuple GetV2JobJobId(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) || + job is null) + { + return new ValueTuple(HttpStatusCode.BadRequest, $"Job with ID: '{groups[1].Value}' does not exist."); + } + return new ValueTuple(HttpStatusCode.OK, job); } private ValueTuple DeleteV2JobJobId(GroupCollection groups, Dictionary requestParameters) @@ -57,7 +70,14 @@ public partial class Server private ValueTuple GetV2JobJobIdProgress(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + + if (groups.Count < 1 || + !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) || + job is null) + { + return new ValueTuple(HttpStatusCode.BadRequest, $"Job with ID: '{groups[1].Value}' does not exist."); + } + return new ValueTuple(HttpStatusCode.OK, job.progressToken); } private ValueTuple PostV2JobJobIdStartNow(GroupCollection groups, Dictionary requestParameters) From 49cfff8a2f310d4be7a3a53a0eeed28662cee7e4 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 00:00:35 +0200 Subject: [PATCH 26/95] Changed the Creation Job API to a single Endpoint /v2/Job/Create/ Added and implemented GET /v2/Job/Types Implemented /v2/Job/ Implemented /v2/Job//StartNow Implemented /v2/Job//Cancel Implemented /v2/Job//SetInterval --- Tranga/Jobs/Job.cs | 2 +- Tranga/Server/Server.cs | 6 +-- Tranga/Server/v2Jobs.cs | 90 ++++++++++++++++++++++++++++++++--------- docs/API_Calls_v2.md | 72 ++++++++------------------------- 4 files changed, 92 insertions(+), 78 deletions(-) diff --git a/Tranga/Jobs/Job.cs b/Tranga/Jobs/Job.cs index deefd77f..8ce37be4 100644 --- a/Tranga/Jobs/Job.cs +++ b/Tranga/Jobs/Job.cs @@ -13,7 +13,7 @@ public abstract class Job : GlobalBase public string id => GetId(); internal IEnumerable? subJobs { get; private set; } public string? parentJobId { get; init; } - public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob } + public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob, MonitorManga } public JobType jobType; diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 30aeafd7..b2f779b9 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -31,10 +31,8 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), - new ("POST", @"/v2/Jobs/Create/Monitor/(^[-A-Za-z0-9+/]*={0,3}$)", PostV2JobsCreateMonitorInternalId), - new ("POST", @"/v2/Jobs/Create/DownloadNewChapters/(^[-A-Za-z0-9+/]*={0,3}$)", PostV2JobsCreateDownloadNewChaptersInternalId), - new ("POST", @"/v2/Jobs/Create/UpdateMetadata", PostV2JobsCreateUpdateMetadata), - new ("POST", @"/v2/Jobs/Create/UpdateMetadata/(^[-A-Za-z0-9+/]*={0,3}$)", PostV2JobsCreateUpdateMetadataInternalId), + new ("Get", @"/v2/Job/Types", GetV2JobTypes), + new ("POST", @"/v2/Job/Create/([a-zA-Z]+)>", PostV2JobsCreateType), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index e9c4bf9e..de590015 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text.RegularExpressions; using Tranga.Jobs; +using Tranga.MangaConnectors; namespace Tranga.Server; @@ -32,24 +33,55 @@ public partial class Server .Select(job => job.id)); } - private ValueTuple PostV2JobsCreateMonitorInternalId(GroupCollection groups, Dictionary requestParameters) + private ValueTuple GetV2JobTypes(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, Enum.GetNames(typeof(Job.JobType))); } - private ValueTuple PostV2JobsCreateDownloadNewChaptersInternalId(GroupCollection groups, Dictionary requestParameters) + private ValueTuple PostV2JobsCreateType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); - } - - private ValueTuple PostV2JobsCreateUpdateMetadata(GroupCollection groups, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); - } - - private ValueTuple PostV2JobsCreateUpdateMetadataInternalId(GroupCollection groups, Dictionary requestParameters) - { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out Job.JobType jobType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID: '{groups[1].Value}' does not exist."); + } + + string? connectorStr, mangaId; + MangaConnector? connector; + Manga? manga; + switch (jobType) + { + case Job.JobType.MonitorManga: + if(!requestParameters.TryGetValue("connector", out connectorStr) || + !_parent.TryGetConnector(connectorStr, out connector) || + connector is null) + return new ValueTuple(HttpStatusCode.NotFound, "Connector Parameter missing, or is not a valid connector."); + if(!requestParameters.TryGetValue("internalId", out mangaId) || + !_parent.TryGetPublicationById(mangaId, out manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); + if(!requestParameters.TryGetValue("interval", out string? intervalStr) || + !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Interval Parameter missing, or is not in correct format."); + requestParameters.TryGetValue("language", out string? language); + _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, (Manga)manga, true, interval, language)); + break; + case Job.JobType.UpdateMetaDataJob: + if(!requestParameters.TryGetValue("connector", out connectorStr) || + !_parent.TryGetConnector(connectorStr, out connector) || + connector is null) + return new ValueTuple(HttpStatusCode.NotFound, "Connector Parameter missing, or is not a valid connector."); + if(!requestParameters.TryGetValue("internalId", out mangaId) || + !_parent.TryGetPublicationById(mangaId, out manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); + _parent.jobBoss.AddJob(new UpdateMetadata(this, connector, (Manga)manga)); + break; + case Job.JobType.DownloadNewChaptersJob: //TODO + case Job.JobType.DownloadChapterJob: //TODO + default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported."); + } + return new ValueTuple(HttpStatusCode.NotImplemented, "How'd you get here."); } private ValueTuple GetV2JobJobId(GroupCollection groups, Dictionary requestParameters) @@ -58,14 +90,22 @@ public partial class Server !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) || job is null) { - return new ValueTuple(HttpStatusCode.BadRequest, $"Job with ID: '{groups[1].Value}' does not exist."); + return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist."); } return new ValueTuple(HttpStatusCode.OK, job); } private ValueTuple DeleteV2JobJobId(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) || + job is null) + { + return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist."); + } + + _parent.jobBoss.RemoveJob(job); + return new ValueTuple(HttpStatusCode.OK, null); } private ValueTuple GetV2JobJobIdProgress(GroupCollection groups, Dictionary requestParameters) @@ -82,12 +122,26 @@ public partial class Server private ValueTuple PostV2JobJobIdStartNow(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) || + job is null) + { + return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist."); + } + _parent.jobBoss.AddJobs(job.ExecuteReturnSubTasks(_parent.jobBoss)); + return new ValueTuple(HttpStatusCode.OK, null); } private ValueTuple PostV2JobJobIdCancel(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !_parent.jobBoss.TryGetJobById(groups[1].Value, out Job? job) || + job is null) + { + return new ValueTuple(HttpStatusCode.NotFound, $"Job with ID: '{groups[1].Value}' does not exist."); + } + job.Cancel(); + return new ValueTuple(HttpStatusCode.OK, null); } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index d39a8401..8e7c9e61 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -218,71 +218,32 @@ Returns all Monitoring Jobs. List of JobIds.
-### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/Monitor/` +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/Types` -Creates a Monitoring-Job for the specified Manga at the specified Interval. - -
- Request - - `internalId` is returned in the response of - * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Job/*jobId*](#-v2jobjobid) - - | Parameter | Value | - |-----------|--------------------------------------------------------| - | interval | Interval at which the Job is re-run in HH:MM:SS format | -
+Returns the valid Job-Types.
Returns - - [Job](Types.md#job) - - | StatusCode | Meaning | - |------------|--------------------------------------------| - | 404 | Manga with `internalId` could not be found | - | 500 | Error parsing interval | -
- -### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/DownloadNewChapters/` - -Creates a Job to check for new Chapters and Download new ones of the specified Manga. - -
- Request - - `internalId` is returned in the response of - * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) - * [GET /v2/Job/*jobId*](#-v2jobjobid) -
-
- Returns - - [Job](Types.md#job) - - | StatusCode | Meaning | - |------------|--------------------------------------------| - | 404 | Manga with `internalId` could not be found | + List of strings.
-### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/UpdateMetadata` +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job/Create/` -Creates a Job to update the Metadata of all Manga. +Creates a Job.
- Returns - - [Job](Types.md#job) -
+ Request -### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Jobs/Create/UpdateMetadata/` + `Type` is returned in the response of [GET /v2/Job/Types](#-v2jobtypes) -Updates the Metadata of the specified Manga. + | Parameter | Value | + |------------|---------------------------------------------------------------------------------------------------| + | connector | Name of the connector to use | + | internalId | Manga ID | + | *interval* | Interval at which the Job is re-run in HH:MM:SS format
Only for MonitorManga, UpdateMetadata | + | *language* | Translated language
Only for MonitorManga, DownloadNewChapters and DownloadChapter | -
- Request `internalId` is returned in the response of * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) @@ -294,9 +255,10 @@ Updates the Metadata of the specified Manga. [Job](Types.md#job) - | StatusCode | Meaning | - |------------|--------------------------------------------| - | 404 | Manga with `internalId` could not be found | + | StatusCode | Meaning | + |------------|------------------------------------------| + | 404 | Parameter missing or could not be found. | + | 500 | Error parsing interval |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` From 3adb103fc465304abe693a0bdf1d4e9ff4db7ace Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 03:02:49 +0200 Subject: [PATCH 27/95] Fix API-Path prematurely triggering match. --- Tranga/Server/Server.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index b2f779b9..d62b430d 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -127,7 +127,7 @@ private void HandleRequest(HttpListenerContext context) if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.IsMatch(path, p.RegexStr))) //Check if Request-Path is valid { RequestPath requestPath = - _apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.IsMatch(path, p.RegexStr)); + _apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length); responseMessage = requestPath.Method.Invoke(Regex.Match(path, requestPath.RegexStr).Groups, requestParams); //Get HttpResponse content } From cce4901a5d48b0ca9d973c3c51e7bb597afdf711 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 03:03:17 +0200 Subject: [PATCH 28/95] Implement all /v2/Settings --- Tranga/Server/v2Settings.cs | 71 ++++++++++++++++++++++++++++++------- docs/API_Calls_v2.md | 36 ++++++++++++------- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs index 6615726e..7f7ed5be 100644 --- a/Tranga/Server/v2Settings.cs +++ b/Tranga/Server/v2Settings.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.RegularExpressions; +using Tranga.MangaConnectors; namespace Tranga.Server; @@ -7,56 +8,102 @@ public partial class Server { private ValueTuple GetV2Settings(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, settings); } private ValueTuple GetV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, settings.userAgent); } - private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) + private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (!requestParameters.TryGetValue("value", out string? userAgent)) + { + settings.UpdateUserAgent(null); + return new ValueTuple(HttpStatusCode.Accepted, null); + } + else + { + settings.UpdateUserAgent(userAgent); + return new ValueTuple(HttpStatusCode.OK, null); + } } private ValueTuple GetV2SettingsRateLimitTypes(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, Enum.GetValues().ToDictionary(b =>(byte)b, b => Enum.GetName(b)) ); } private ValueTuple GetV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, settings.requestLimits); } private ValueTuple PostV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + foreach (KeyValuePair kv in requestParameters) + { + if(!Enum.TryParse(kv.Key, out RequestType requestType) || + !int.TryParse(kv.Value, out int requestsPerMinute)) + return new ValueTuple(HttpStatusCode.InternalServerError, null); + settings.requestLimits[requestType] = requestsPerMinute; + settings.ExportSettings(); + } + return new ValueTuple(HttpStatusCode.OK, settings.requestLimits); } private ValueTuple GetV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if(groups.Count < 1 || + !Enum.TryParse(groups[1].Value, out RequestType requestType)) + return new ValueTuple(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}"); + return new ValueTuple(HttpStatusCode.OK, settings.requestLimits[requestType]); } private ValueTuple PostV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if(groups.Count < 1 || + !Enum.TryParse(groups[1].Value, out RequestType requestType)) + return new ValueTuple(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}"); + if (!requestParameters.TryGetValue("value", out string? requestsPerMinuteStr) || + !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing requestsPerMinute"); + settings.requestLimits[requestType] = requestsPerMinute; + return new ValueTuple(HttpStatusCode.OK, null); } private ValueTuple GetV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, settings.aprilFoolsMode); } private ValueTuple PostV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || + !bool.TryParse(trueFalseStr, out bool trueFalse)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); + settings.UpdateAprilFoolsMode(trueFalse); + return new ValueTuple(HttpStatusCode.OK, null); } private ValueTuple PostV2SettingsDownloadLocation(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (!requestParameters.TryGetValue("location", out string? folderPath)) + return new ValueTuple(HttpStatusCode.NotFound, "Missing Parameter 'location'"); + try + { + bool moveFiles = requestParameters.TryGetValue("moveFiles", out string? moveFilesStr) switch + { + false => true, + true => bool.Parse(moveFilesStr!) + }; + settings.UpdateDownloadLocation(folderPath, moveFiles); + return new ValueTuple(HttpStatusCode.OK, null); + } + catch (FormatException) + { + return new ValueTuple(HttpStatusCode.InternalServerError, "Error Parsing Parameter 'moveFiles'"); + } } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 8e7c9e61..af542e21 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -436,6 +436,12 @@ Sets the User Agent. If left empty, User Agent is reset to default.
Returns + +| StatusCode | Meaning | +|------------|-------------------| +| 202 | UserAgent Reset | +| 201 | UserAgent Updated | +
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/Types` @@ -445,7 +451,7 @@ Returns the configurable Rate-Limits.
Returns - List of Rate-Limit-Names. + Key-Value-Pairs of Values and RateLimit-Names.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit` @@ -467,19 +473,19 @@ Sets the Rate-Limits for all Requests. If left empty, resets to default Rate-Lim For each Rate-Limit set as follows: - | Parameter | Value | - |------------------------------------|---------------------| - | [Type](#-v2settingsratelimittypes) | Requests per Minute | + | Parameter | Value | + |--------------------------------------|-----------------------| + | [Type](#-v2settingsratelimittypes) | Requests per Minute | - `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes) + `Type` is returned by [GET /v2/Settings/RateLimit/Types](#-v2settingsratelimittypes) and should be supplied as string
Returns - | StatusCode | Meaning | - |------------|--------------------------------| - | 404 | Rate-Limit-Name does not exist | + | StatusCode | Meaning | + |------------|------------------------------------------------| + | 500 | Error parsing RequestType or RequestsPerMinute |
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/RateLimit/` @@ -496,6 +502,10 @@ Returns the current Rate-Limit for the Request-Type. Returns Integer with Requests per Minute. + +| StatusCode | Meaning | +|------------|-----------------------------------------------| +| 404 | Error parsing RequestType | ### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/RateLimit/` @@ -547,7 +557,6 @@ Enables/Disables April-Fools-Mode. | StatusCode | Meaning | |------------|--------------------------------| - | 404 | Rate-Limit-Name does not exist | | 500 | Parsing Error | @@ -568,10 +577,11 @@ Updates the default Download-Location. Returns - | StatusCode | Meaning | - |------------|--------------------------| - | 200 | Successfully changed | - | 500 | Files could not be moved | + | StatusCode | Meaning | + |------------|---------------------------------| + | 200 | Successfully changed | + | 404 | Parameter 'location' is missing | + | 500 | Parsing Error | ## Library Connectors [^top](#top) From 64482931a38a8b457ab7c02491d3231cb3fdab29 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 03:19:56 +0200 Subject: [PATCH 29/95] Implemented GET /v2/LogFile --- Tranga/Server/Server.cs | 2 +- Tranga/Server/v2Miscellaneous.cs | 13 ++++++++++++- docs/API_Calls_v2.md | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index d62b430d..b6b42d0c 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -217,7 +217,7 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon case "jpeg": response.ContentType = "image/jpeg"; break; - case "log": + default: response.ContentType = "text/plain"; break; } diff --git a/Tranga/Server/v2Miscellaneous.cs b/Tranga/Server/v2Miscellaneous.cs index e0ce1a8c..08ff29f2 100644 --- a/Tranga/Server/v2Miscellaneous.cs +++ b/Tranga/Server/v2Miscellaneous.cs @@ -7,7 +7,18 @@ public partial class Server { private ValueTuple GetV2LogFile(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (logger is null || !File.Exists(logger?.logFilePath)) + { + return new ValueTuple(HttpStatusCode.NotFound, "Missing Logfile"); + } + + FileStream logFile = new (logger.logFilePath, FileMode.Open, FileAccess.Read); + FileStream content = new(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 0, FileOptions.DeleteOnClose); + logFile.Position = 0; + logFile.CopyTo(content); + content.Position = 0; + logFile.Dispose(); + return new ValueTuple(HttpStatusCode.OK, content); } private ValueTuple GetV2Ping(GroupCollection groups, Dictionary requestParameters) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index af542e21..b9eaa880 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -878,6 +878,10 @@ Returns the current log-file. Returns The Logfile as Stream. + +| StatusCode | Meaning | +|------------|------------| +| 404 | No Logfile | ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Ping` From 03e90eccd32fbb04b1d55543324844af0bcf30a0 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 04:21:30 +0200 Subject: [PATCH 30/95] No longer require connector name to create job --- Tranga/Jobs/DownloadNewChapters.cs | 6 +-- Tranga/Jobs/JobJsonConverter.cs | 67 +++++++++++---------------- Tranga/Jobs/UpdateMetadata.cs | 2 +- Tranga/Manga.cs | 10 ++-- Tranga/MangaConnectors/Bato.cs | 4 +- Tranga/MangaConnectors/MangaDex.cs | 9 ++-- Tranga/MangaConnectors/MangaKatana.cs | 4 +- Tranga/MangaConnectors/MangaLife.cs | 4 +- Tranga/MangaConnectors/Manganato.cs | 4 +- Tranga/MangaConnectors/Mangasee.cs | 5 +- Tranga/MangaConnectors/Mangaworld.cs | 4 +- Tranga/Server/Server.cs | 4 +- Tranga/Server/v2Jobs.cs | 24 +++------- docs/API_Calls_v2.md | 1 - 14 files changed, 62 insertions(+), 86 deletions(-) diff --git a/Tranga/Jobs/DownloadNewChapters.cs b/Tranga/Jobs/DownloadNewChapters.cs index ebcbde76..a202c492 100644 --- a/Tranga/Jobs/DownloadNewChapters.cs +++ b/Tranga/Jobs/DownloadNewChapters.cs @@ -7,8 +7,8 @@ public class DownloadNewChapters : Job public Manga manga { get; set; } public string translatedLanguage { get; init; } - public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution, - bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, connector, lastExecution, recurring, + public DownloadNewChapters(GlobalBase clone, Manga manga, DateTime lastExecution, + bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, manga.mangaConnector, lastExecution, recurring, recurrence, parentJobId) { this.manga = manga; @@ -43,7 +43,7 @@ protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBos DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id); jobs.Add(downloadChapterJob); } - UpdateMetadata updateMetadataJob = new(this, this.mangaConnector, this.manga, parentJobId: this.id); + UpdateMetadata updateMetadataJob = new(this, this.manga, parentJobId: this.id); jobs.Add(updateMetadataJob); progressToken.Complete(); return jobs; diff --git a/Tranga/Jobs/JobJsonConverter.cs b/Tranga/Jobs/JobJsonConverter.cs index 5b121430..9b372585 100644 --- a/Tranga/Jobs/JobJsonConverter.cs +++ b/Tranga/Jobs/JobJsonConverter.cs @@ -23,53 +23,42 @@ public override bool CanConvert(Type objectType) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { JObject jo = JObject.Load(reader); + + if(!jo.ContainsKey("jobType")) + throw new Exception(); - if (jo.ContainsKey("jobType") && jo["jobType"]!.Value() == (byte)Job.JobType.UpdateMetaDataJob) + return Enum.Parse(jo["jobType"]!.Value().ToString()) switch { - return new UpdateMetadata(this._clone, - jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() + Job.JobType.UpdateMetaDataJob => new UpdateMetadata(_clone, + jo.GetValue("manga")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() { - Converters = - { - this._mangaConnectorJsonConverter - } - }))!, - jo.GetValue("manga")!.ToObject(), - jo.GetValue("parentJobId")!.Value()); - }else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value() == (byte)Job.JobType.DownloadNewChaptersJob) || jo.ContainsKey("translatedLanguage"))//TODO change to jobType - { - DateTime lastExecution = jo.GetValue("lastExecution") is {} le - ? le.ToObject() - : DateTime.UnixEpoch; //TODO do null checks on all variables - return new DownloadNewChapters(this._clone, + Converters = { this._mangaConnectorJsonConverter } + })), + jo.GetValue("parentJobId")!.Value()), + Job.JobType.DownloadChapterJob => new DownloadChapter(this._clone, jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() { - Converters = - { - this._mangaConnectorJsonConverter - } + Converters = { this._mangaConnectorJsonConverter } }))!, - jo.GetValue("manga")!.ToObject(), - lastExecution, - jo.GetValue("recurring")!.Value(), - jo.GetValue("recurrenceTime")!.ToObject(), - jo.GetValue("parentJobId")!.Value()); - }else if ((jo.ContainsKey("jobType") && jo["jobType"]!.Value() == (byte)Job.JobType.DownloadChapterJob) || jo.ContainsKey("chapter"))//TODO change to jobType - { - return new DownloadChapter(this._clone, - jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() + jo.GetValue("chapter")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() { - Converters = - { - this._mangaConnectorJsonConverter - } - }))!, - jo.GetValue("chapter")!.ToObject(), + Converters = { this._mangaConnectorJsonConverter } + })), DateTime.UnixEpoch, - jo.GetValue("parentJobId")!.Value()); - } - - throw new Exception(); + jo.GetValue("parentJobId")!.Value()), + Job.JobType.DownloadNewChaptersJob => new DownloadNewChapters(this._clone, + jo.GetValue("manga")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() + { + Converters = { this._mangaConnectorJsonConverter } + })), + jo.GetValue("lastExecution") is {} le + ? le.ToObject() + : DateTime.UnixEpoch, + jo.GetValue("recurring")!.Value(), + jo.GetValue("recurrenceTime")!.ToObject(), + jo.GetValue("parentJobId")!.Value()), + _ => throw new Exception() + }; } public override bool CanWrite => false; diff --git a/Tranga/Jobs/UpdateMetadata.cs b/Tranga/Jobs/UpdateMetadata.cs index e9107cef..121ce47d 100644 --- a/Tranga/Jobs/UpdateMetadata.cs +++ b/Tranga/Jobs/UpdateMetadata.cs @@ -6,7 +6,7 @@ public class UpdateMetadata : Job { public Manga manga { get; set; } - public UpdateMetadata(GlobalBase clone, MangaConnector connector, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, connector, parentJobId: parentJobId) + public UpdateMetadata(GlobalBase clone, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, manga.mangaConnector, parentJobId: parentJobId) { this.manga = manga; } diff --git a/Tranga/Manga.cs b/Tranga/Manga.cs index a29f2b68..d1a0e859 100644 --- a/Tranga/Manga.cs +++ b/Tranga/Manga.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; +using Tranga.MangaConnectors; using static System.IO.UnixFileMode; namespace Tranga; @@ -26,8 +27,6 @@ public struct Manga // ReSharper disable once MemberCanBePrivate.Global public int? year { get; private set; } public string? originalLanguage { get; } - // ReSharper disable twice MemberCanBePrivate.Global - public string status { get; private set; } public ReleaseStatusByte releaseStatus { get; private set; } public enum ReleaseStatusByte : byte { @@ -43,14 +42,15 @@ public enum ReleaseStatusByte : byte public float ignoreChaptersBelow { get; set; } public float latestChapterDownloaded { get; set; } public float latestChapterAvailable { get; set; } - public string websiteUrl { get; private set; } + public MangaConnector mangaConnector { get; private set; } private static readonly Regex LegalCharacters = new (@"[A-Za-zÀ-ÖØ-öø-ÿ0-9 \.\-,'\'\)\(~!\+]*"); [JsonConstructor] - public Manga(string sortName, List authors, string? description, Dictionary altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0) + public Manga(MangaConnector mangaConnector, string sortName, List authors, string? description, Dictionary altTitles, string[] tags, string? coverUrl, string? coverFileNameInCache, Dictionary? links, int? year, string? originalLanguage, string publicationId, ReleaseStatusByte releaseStatus, string? websiteUrl, string? folderName = null, float? ignoreChaptersBelow = 0) { + this.mangaConnector = mangaConnector; this.sortName = sortName; this.authors = authors; this.description = description; @@ -82,7 +82,6 @@ public void UpdateMetadata(Manga newManga) this.authors = authors.Union(newManga.authors).ToList(); this.altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value); this.tags = tags.Union(newManga.tags).ToArray(); - this.status = newManga.status; this.releaseStatus = newManga.releaseStatus; this.year = newManga.year; } @@ -93,7 +92,6 @@ public override bool Equals(object? obj) return false; return this.description == compareManga.description && this.year == compareManga.year && - this.status == compareManga.status && this.releaseStatus == compareManga.releaseStatus && this.sortName == compareManga.sortName && this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) && diff --git a/Tranga/MangaConnectors/Bato.cs b/Tranga/MangaConnectors/Bato.cs index edc6dc9a..657af007 100644 --- a/Tranga/MangaConnectors/Bato.cs +++ b/Tranga/MangaConnectors/Bato.cs @@ -114,8 +114,8 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi case "pending": releaseStatus = Manga.ReleaseStatusByte.Unreleased; break; } - Manga manga = new (sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary(), - year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); + Manga manga = new (this, sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary(), + year, originalLanguage, publicationId, releaseStatus, websiteUrl); cachedPublications.Add(manga); return manga; } diff --git a/Tranga/MangaConnectors/MangaDex.cs b/Tranga/MangaConnectors/MangaDex.cs index 550e34f7..c764dcd9 100644 --- a/Tranga/MangaConnectors/MangaDex.cs +++ b/Tranga/MangaConnectors/MangaDex.cs @@ -126,10 +126,10 @@ public override Manga[] GetManga(string publicationTitle = "") false => null }; - Manga.ReleaseStatusByte status = Manga.ReleaseStatusByte.Unreleased; + Manga.ReleaseStatusByte releaseStatus = Manga.ReleaseStatusByte.Unreleased; if (attributes.TryGetPropertyValue("status", out JsonNode? statusNode)) { - status = statusNode?.GetValue().ToLower() switch + releaseStatus = statusNode?.GetValue().ToLower() switch { "ongoing" => Manga.ReleaseStatusByte.Continuing, "completed" => Manga.ReleaseStatusByte.Completed, @@ -173,6 +173,7 @@ public override Manga[] GetManga(string publicationTitle = "") } Manga pub = new( + this, title, authors, description, @@ -184,8 +185,8 @@ public override Manga[] GetManga(string publicationTitle = "") year, originalLanguage, publicationId, - status, - websiteUrl: $"https://mangadex.org/title/{publicationId}" + releaseStatus, + $"https://mangadex.org/title/{publicationId}" ); cachedPublications.Add(pub); return pub; diff --git a/Tranga/MangaConnectors/MangaKatana.cs b/Tranga/MangaConnectors/MangaKatana.cs index de590300..9ab937a1 100644 --- a/Tranga/MangaConnectors/MangaKatana.cs +++ b/Tranga/MangaConnectors/MangaKatana.cs @@ -141,8 +141,8 @@ private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId, year = Convert.ToInt32(yearString); } - Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, - year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); + Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, + year, originalLanguage, publicationId, releaseStatus, websiteUrl); cachedPublications.Add(manga); return manga; } diff --git a/Tranga/MangaConnectors/MangaLife.cs b/Tranga/MangaConnectors/MangaLife.cs index 3f87953d..9245ec28 100644 --- a/Tranga/MangaConnectors/MangaLife.cs +++ b/Tranga/MangaConnectors/MangaLife.cs @@ -121,8 +121,8 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi .Descendants("div").First(); string description = descriptionNode.InnerText; - Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, - coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); + Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, + coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); cachedPublications.Add(manga); return manga; } diff --git a/Tranga/MangaConnectors/Manganato.cs b/Tranga/MangaConnectors/Manganato.cs index cbbf0f07..793c0d0c 100644 --- a/Tranga/MangaConnectors/Manganato.cs +++ b/Tranga/MangaConnectors/Manganato.cs @@ -127,8 +127,8 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi .First(s => s.HasClass("chapter-time")).InnerText; int year = Convert.ToInt32(yearString.Split(',')[^1]) + 2000; - Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, - year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); + Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, + year, originalLanguage, publicationId, releaseStatus, websiteUrl); cachedPublications.Add(manga); return manga; } diff --git a/Tranga/MangaConnectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs index f99e1085..935c866b 100644 --- a/Tranga/MangaConnectors/Mangasee.cs +++ b/Tranga/MangaConnectors/Mangasee.cs @@ -176,9 +176,8 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi .Descendants("div").First(); string description = descriptionNode.InnerText; - Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, - coverFileNameInCache, links, - year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); + Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, + coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); cachedPublications.Add(manga); return manga; } diff --git a/Tranga/MangaConnectors/Mangaworld.cs b/Tranga/MangaConnectors/Mangaworld.cs index 0f2e80d5..fbacfaa7 100644 --- a/Tranga/MangaConnectors/Mangaworld.cs +++ b/Tranga/MangaConnectors/Mangaworld.cs @@ -118,8 +118,8 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi string yearString = metadata.SelectSingleNode("//span[text()='Anno di uscita: ']/..").SelectNodes("a").First().InnerText; int year = Convert.ToInt32(yearString); - Manga manga = new (sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, - year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); + Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, + year, originalLanguage, publicationId, releaseStatus, websiteUrl); cachedPublications.Add(manga); return manga; } diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index b6b42d0c..f2d42562 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -32,7 +32,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), new ("Get", @"/v2/Job/Types", GetV2JobTypes), - new ("POST", @"/v2/Job/Create/([a-zA-Z]+)>", PostV2JobsCreateType), + new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobsCreateType), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), @@ -124,7 +124,7 @@ private void HandleRequest(HttpListenerContext context) .ToDictionary(kv => kv.Key, kv => kv.Value); //The actual variable used for the API ValueTuple responseMessage; //Used to respond to the HttpRequest - if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.IsMatch(path, p.RegexStr))) //Check if Request-Path is valid + if (_apiRequestPaths.Any(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length)) //Check if Request-Path is valid { RequestPath requestPath = _apiRequestPaths.First(p => p.HttpMethod == request.HttpMethod && Regex.Match(path, p.RegexStr).Length == path.Length); diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index de590015..fda77a09 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -46,42 +46,32 @@ public partial class Server return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID: '{groups[1].Value}' does not exist."); } - string? connectorStr, mangaId; - MangaConnector? connector; + string? mangaId; Manga? manga; switch (jobType) { case Job.JobType.MonitorManga: - if(!requestParameters.TryGetValue("connector", out connectorStr) || - !_parent.TryGetConnector(connectorStr, out connector) || - connector is null) - return new ValueTuple(HttpStatusCode.NotFound, "Connector Parameter missing, or is not a valid connector."); if(!requestParameters.TryGetValue("internalId", out mangaId) || !_parent.TryGetPublicationById(mangaId, out manga) || manga is null) - return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); + return new ValueTuple(HttpStatusCode.NotFound, "'internalId' Parameter missing, or is not a valid ID."); if(!requestParameters.TryGetValue("interval", out string? intervalStr) || !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) - return new ValueTuple(HttpStatusCode.InternalServerError, "Interval Parameter missing, or is not in correct format."); + return new ValueTuple(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format."); requestParameters.TryGetValue("language", out string? language); - _parent.jobBoss.AddJob(new DownloadNewChapters(this, connector, (Manga)manga, true, interval, language)); - break; + _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, (Manga)manga, true, interval, language)); + return new ValueTuple(HttpStatusCode.OK, null); case Job.JobType.UpdateMetaDataJob: - if(!requestParameters.TryGetValue("connector", out connectorStr) || - !_parent.TryGetConnector(connectorStr, out connector) || - connector is null) - return new ValueTuple(HttpStatusCode.NotFound, "Connector Parameter missing, or is not a valid connector."); if(!requestParameters.TryGetValue("internalId", out mangaId) || !_parent.TryGetPublicationById(mangaId, out manga) || manga is null) return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); - _parent.jobBoss.AddJob(new UpdateMetadata(this, connector, (Manga)manga)); - break; + _parent.jobBoss.AddJob(new UpdateMetadata(this, (Manga)manga)); + return new ValueTuple(HttpStatusCode.OK, null); case Job.JobType.DownloadNewChaptersJob: //TODO case Job.JobType.DownloadChapterJob: //TODO default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported."); } - return new ValueTuple(HttpStatusCode.NotImplemented, "How'd you get here."); } private ValueTuple GetV2JobJobId(GroupCollection groups, Dictionary requestParameters) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index b9eaa880..f5bc8679 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -239,7 +239,6 @@ Creates a Job. | Parameter | Value | |------------|---------------------------------------------------------------------------------------------------| - | connector | Name of the connector to use | | internalId | Manga ID | | *interval* | Interval at which the Job is re-run in HH:MM:SS format
Only for MonitorManga, UpdateMetadata | | *language* | Translated language
Only for MonitorManga, DownloadNewChapters and DownloadChapter | From c3231327f9bdb8e02c827991f7086f08f5b46d9a Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 04:21:39 +0200 Subject: [PATCH 31/95] nullable --- Tranga/Server/v2Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs index 7f7ed5be..5841ee2e 100644 --- a/Tranga/Server/v2Settings.cs +++ b/Tranga/Server/v2Settings.cs @@ -16,7 +16,7 @@ public partial class Server return new ValueTuple(HttpStatusCode.OK, settings.userAgent); } - private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) + private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) { if (!requestParameters.TryGetValue("value", out string? userAgent)) { From ea866e0136aaa16dd1d2f0dd1e2553d0c38800df Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 04:42:10 +0200 Subject: [PATCH 32/95] Added Endpoint /v2/Manga lists all known Manga Implemented /v2/Manga/* --- Tranga/Server/Server.cs | 9 ++++---- Tranga/Server/v2Manga.cs | 44 ++++++++++++++++++++++++++++++++++++---- docs/API_Calls_v2.md | 20 ++++++++++++++++++ 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index f2d42562..863b9d9b 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -23,10 +23,11 @@ public Server(Tranga parent) : base(parent) { new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes), new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga), - new ("GET", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})", GetV2MangaInternalId), - new ("DELETE", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})", DeleteV2MangaInternalId), - new ("GET", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})/Cover", GetV2MangaInternalIdCover), - new ("GET", @"/v2/Manga/([-A-Za-z0-9+/]*={0,3})/Chapters", GetV2MangaInternalIdChapters), + new ("GET", @"/v2/Manga", GetV2Manga), + new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), + new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), + new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), + new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters), new ("GET", @"/v2/Jobs", GetV2Jobs), new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 834cb4f9..b8c3a28d 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -1,27 +1,63 @@ using System.Net; using System.Text.RegularExpressions; +using Tranga.Jobs; namespace Tranga.Server; public partial class Server { + private ValueTuple GetV2Manga(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, cachedPublications.Select(m => m.internalId)); + } + private ValueTuple GetV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + return new ValueTuple(HttpStatusCode.OK, manga); } private ValueTuple DeleteV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + Job[] jobs = _parent.jobBoss.GetJobsLike(publication: manga).ToArray(); + _parent.jobBoss.RemoveJobs(jobs); + return new ValueTuple(HttpStatusCode.OK, null); } private ValueTuple GetV2MangaInternalIdCover(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + string filePath = settings.GetFullCoverPath((Manga)manga!); + if (File.Exists(filePath)) + { + FileStream coverStream = new(filePath, FileMode.Open); + return new ValueTuple(HttpStatusCode.OK, coverStream); + } + return new ValueTuple(HttpStatusCode.NotFound, "Cover-File not found."); } private ValueTuple GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + + Chapter[] chapters = requestParameters.TryGetValue("language", out string? parameter) switch + { + true => manga.Value.mangaConnector.GetChapters((Manga)manga, parameter), + false => manga.Value.mangaConnector.GetChapters((Manga)manga) + }; + return new ValueTuple(HttpStatusCode.OK, chapters); } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index f5bc8679..056a26c4 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -67,6 +67,16 @@ Returns the Manga from the specified Manga Connector. ## Manga [^top](#top) +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga` + +Returns all known Manga. + +
+ Returns + + List of internalIds. +
+ ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/` Returns the specified Manga. @@ -75,6 +85,7 @@ Returns the specified Manga. Request `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid) @@ -97,6 +108,7 @@ Deletes all associated Jobs for the specified Manga Request `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid) @@ -118,6 +130,7 @@ Returns the URL for the Cover of the specified Manga. Request `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid) @@ -140,8 +153,13 @@ Returns the Chapter-list for the specified Manga. Request `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid) + + | Parameter | Value | + |------------|------------------------| + | *language* | Language to search for |
@@ -162,6 +180,7 @@ Returns the latest Chapter of the specified Manga. Request `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid)
@@ -245,6 +264,7 @@ Creates a Job. `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid) From e360037fda8cd460a3998b3a5c9426f7511a7303 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 04:43:08 +0200 Subject: [PATCH 33/95] Add "(?:/?)" to the end of all Regex RequestPaths --- Tranga/Server/RequestPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/RequestPath.cs b/Tranga/Server/RequestPath.cs index c8db596b..cbf4d0d5 100644 --- a/Tranga/Server/RequestPath.cs +++ b/Tranga/Server/RequestPath.cs @@ -13,7 +13,7 @@ public RequestPath(string httpHttpMethod, string regexStr, Func, ValueTuple> method) { this.HttpMethod = httpHttpMethod; - this.RegexStr = regexStr; + this.RegexStr = regexStr + "(?:/?)"; this.Method = method; } } \ No newline at end of file From 8c66bbc89fca9c0064fbf1893dce5d4fa22e4686 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 22 Apr 2024 23:45:51 +0200 Subject: [PATCH 34/95] Use publicationCache to store and update Manga --- Tranga/GlobalBase.cs | 87 ++++++++++++++++++++++++++- Tranga/Jobs/DownloadChapter.cs | 12 ++-- Tranga/Jobs/DownloadNewChapters.cs | 43 ++++++++----- Tranga/Jobs/Job.cs | 11 ++-- Tranga/Jobs/JobBoss.cs | 38 ++++++++---- Tranga/Jobs/JobJsonConverter.cs | 14 +---- Tranga/Jobs/UpdateMetadata.cs | 34 ++++++++--- Tranga/Manga.cs | 23 ++++--- Tranga/MangaConnectors/Bato.cs | 2 +- Tranga/MangaConnectors/MangaDex.cs | 2 +- Tranga/MangaConnectors/MangaKatana.cs | 2 +- Tranga/MangaConnectors/MangaLife.cs | 2 +- Tranga/MangaConnectors/Manganato.cs | 2 +- Tranga/MangaConnectors/Mangasee.cs | 2 +- Tranga/MangaConnectors/Mangaworld.cs | 2 +- Tranga/Server/Server.cs | 2 +- Tranga/Server/v2Jobs.cs | 6 +- Tranga/Server/v2Manga.cs | 3 +- Tranga/Tranga.cs | 8 +-- Tranga/TrangaSettings.cs | 1 + 20 files changed, 207 insertions(+), 89 deletions(-) diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs index ce01a79a..b320ef15 100644 --- a/Tranga/GlobalBase.cs +++ b/Tranga/GlobalBase.cs @@ -1,8 +1,11 @@ using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using Logging; using Newtonsoft.Json; using Tranga.LibraryConnectors; +using Tranga.MangaConnectors; using Tranga.NotificationConnectors; namespace Tranga; @@ -14,7 +17,9 @@ public abstract class GlobalBase protected TrangaSettings settings { get; init; } protected HashSet notificationConnectors { get; init; } protected HashSet libraryConnectors { get; init; } - protected List cachedPublications { get; init; } + private Dictionary cachedPublications { get; init; } + + protected HashSet _connectors; public static readonly NumberFormatInfo numberFormatDecimalPoint = new (){ NumberDecimalSeparator = "." }; protected static readonly Regex baseUrlRex = new(@"https?:\/\/[0-9A-z\.-]+(:[0-9]+)?"); @@ -25,6 +30,7 @@ protected GlobalBase(GlobalBase clone) this.notificationConnectors = clone.notificationConnectors; this.libraryConnectors = clone.libraryConnectors; this.cachedPublications = clone.cachedPublications; + this._connectors = clone._connectors; } protected GlobalBase(Logger? logger, TrangaSettings settings) @@ -34,6 +40,85 @@ protected GlobalBase(Logger? logger, TrangaSettings settings) this.notificationConnectors = settings.LoadNotificationConnectors(this); this.libraryConnectors = settings.LoadLibraryConnectors(this); this.cachedPublications = new(); + this._connectors = new(); + } + + protected Manga? GetCachedManga(string internalId) + { + return cachedPublications.TryGetValue(internalId, out Manga manga) switch + { + true => manga, + _ => null + }; + } + + protected IEnumerable GetAllCachedManga() => cachedPublications.Values; + + protected void AddMangaToCache(Manga manga) + { + if (!cachedPublications.TryAdd(manga.internalId, manga)) + { + Log($"Overwriting Manga {manga.internalId}"); + cachedPublications[manga.internalId] = manga; + } + ExportManga(); + } + + protected void RemoveMangaFromCache(Manga manga) => RemoveMangaFromCache(manga.internalId); + + protected void RemoveMangaFromCache(string internalId) + { + cachedPublications.Remove(internalId); + ExportManga(); + } + + internal void ImportManga() + { + string folder = settings.mangaCacheFolderPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + Directory.CreateDirectory(folder, + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | + UnixFileMode.UserRead | UnixFileMode.UserWrite); + else + Directory.CreateDirectory(folder); + + foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles()) + { + string content = File.ReadAllText(fileInfo.FullName); + try + { + Manga m = JsonConvert.DeserializeObject(content, new MangaConnectorJsonConverter(this, _connectors)); + this.cachedPublications.TryAdd(m.internalId, m); + } + catch (JsonException e) + { + Log($"Error parsing Manga {fileInfo.Name}:\n{e.Message}"); + } + } + + } + + private void ExportManga() + { + string folder = settings.mangaCacheFolderPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + Directory.CreateDirectory(folder, + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | + UnixFileMode.UserRead | UnixFileMode.UserWrite); + else + Directory.CreateDirectory(folder); + foreach (Manga manga in cachedPublications.Values) + { + string content = JsonConvert.SerializeObject(manga, Formatting.Indented); + string filePath = Path.Combine(folder, $"{manga.internalId}.json"); + File.WriteAllText(filePath, content, Encoding.UTF8); + } + + foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles()) + { + if(!cachedPublications.Keys.Any(key => fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf('.')).Equals(key))) + fileInfo.Delete(); + } } protected void Log(string message) diff --git a/Tranga/Jobs/DownloadChapter.cs b/Tranga/Jobs/DownloadChapter.cs index 434736e3..7e1de356 100644 --- a/Tranga/Jobs/DownloadChapter.cs +++ b/Tranga/Jobs/DownloadChapter.cs @@ -7,12 +7,12 @@ public class DownloadChapter : Job { public Chapter chapter { get; init; } - public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, lastExecution, parentJobId: parentJobId) + public DownloadChapter(GlobalBase clone, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, lastExecution, parentJobId: parentJobId) { this.chapter = chapter; } - public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, connector, parentJobId: parentJobId) + public DownloadChapter(GlobalBase clone, Chapter chapter, string? parentJobId = null) : base(clone, JobType.DownloadChapterJob, parentJobId: parentJobId) { this.chapter = chapter; } @@ -44,11 +44,15 @@ protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBos return Array.Empty(); } + protected override MangaConnector GetMangaConnector() + { + return chapter.parentManga.mangaConnector; + } + public override bool Equals(object? obj) { if (obj is not DownloadChapter otherJob) return false; - return otherJob.mangaConnector == this.mangaConnector && - otherJob.chapter.Equals(this.chapter); + return otherJob.chapter.Equals(this.chapter); } } \ No newline at end of file diff --git a/Tranga/Jobs/DownloadNewChapters.cs b/Tranga/Jobs/DownloadNewChapters.cs index a202c492..da8c519a 100644 --- a/Tranga/Jobs/DownloadNewChapters.cs +++ b/Tranga/Jobs/DownloadNewChapters.cs @@ -1,29 +1,29 @@ -using Tranga.MangaConnectors; +using Newtonsoft.Json; +using Tranga.MangaConnectors; namespace Tranga.Jobs; public class DownloadNewChapters : Job { - public Manga manga { get; set; } + public string mangaInternalId { get; set; } + [JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId); public string translatedLanguage { get; init; } - public DownloadNewChapters(GlobalBase clone, Manga manga, DateTime lastExecution, - bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, manga.mangaConnector, lastExecution, recurring, - recurrence, parentJobId) + public DownloadNewChapters(GlobalBase clone, string mangaInternalId, DateTime lastExecution, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base(clone, JobType.DownloadNewChaptersJob, lastExecution, recurring, recurrence, parentJobId) { - this.manga = manga; + this.mangaInternalId = mangaInternalId; this.translatedLanguage = translatedLanguage; } - public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, connector, recurring, recurrence, parentJobId) + public DownloadNewChapters(GlobalBase clone, MangaConnector connector, string mangaInternalId, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null, string translatedLanguage = "en") : base (clone, JobType.DownloadNewChaptersJob, recurring, recurrence, parentJobId) { - this.manga = manga; + this.mangaInternalId = mangaInternalId; this.translatedLanguage = translatedLanguage; } protected override string GetId() { - return $"{GetType()}-{manga.internalId}"; + return $"{GetType()}-{mangaInternalId}"; } public override string ToString() @@ -33,27 +33,38 @@ public override string ToString() protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBoss) { - manga.SaveSeriesInfoJson(settings.downloadLocation); - Chapter[] chapters = mangaConnector.GetNewChapters(manga, this.translatedLanguage); + if (manga is null) + { + Log($"Manga {mangaInternalId} is missing! Can not execute job."); + return Array.Empty(); + } + manga.Value.SaveSeriesInfoJson(settings.downloadLocation); + Chapter[] chapters = manga.Value.mangaConnector.GetNewChapters(manga.Value, this.translatedLanguage); this.progressToken.increments = chapters.Length; List jobs = new(); - mangaConnector.CopyCoverFromCacheToDownloadLocation(manga); + manga.Value.mangaConnector.CopyCoverFromCacheToDownloadLocation(manga.Value); foreach (Chapter chapter in chapters) { - DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id); + DownloadChapter downloadChapterJob = new(this, chapter, parentJobId: this.id); jobs.Add(downloadChapterJob); } - UpdateMetadata updateMetadataJob = new(this, this.manga, parentJobId: this.id); + UpdateMetadata updateMetadataJob = new(this, mangaInternalId, parentJobId: this.id); jobs.Add(updateMetadataJob); progressToken.Complete(); return jobs; } + protected override MangaConnector GetMangaConnector() + { + if (manga is null) + throw new Exception($"Missing Manga {mangaInternalId}"); + return manga.Value.mangaConnector; + } + public override bool Equals(object? obj) { if (obj is not DownloadNewChapters otherJob) return false; - return otherJob.mangaConnector == this.mangaConnector && - otherJob.manga.Equals(this.manga); + return otherJob.manga.Equals(this.manga); } } \ No newline at end of file diff --git a/Tranga/Jobs/Job.cs b/Tranga/Jobs/Job.cs index 8ce37be4..71dbc24b 100644 --- a/Tranga/Jobs/Job.cs +++ b/Tranga/Jobs/Job.cs @@ -4,7 +4,6 @@ namespace Tranga.Jobs; public abstract class Job : GlobalBase { - public MangaConnector mangaConnector { get; init; } public ProgressToken progressToken { get; private set; } public bool recurring { get; init; } public TimeSpan? recurrenceTime { get; set; } @@ -15,12 +14,13 @@ public abstract class Job : GlobalBase public string? parentJobId { get; init; } public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob, MonitorManga } + public MangaConnector mangaConnector => GetMangaConnector(); + public JobType jobType; - internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone) + internal Job(GlobalBase clone, JobType jobType, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone) { this.jobType = jobType; - this.mangaConnector = connector; this.progressToken = new ProgressToken(0); this.recurring = recurring; if (recurring && recurrenceTime is null) @@ -31,11 +31,10 @@ internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, bool r this.parentJobId = parentJobId; } - internal Job(GlobalBase clone, JobType jobType, MangaConnector connector, DateTime lastExecution, bool recurring = false, + internal Job(GlobalBase clone, JobType jobType, DateTime lastExecution, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone) { this.jobType = jobType; - this.mangaConnector = connector; this.progressToken = new ProgressToken(0); this.recurring = recurring; if (recurring && recurrenceTime is null) @@ -95,4 +94,6 @@ public IEnumerable ExecuteReturnSubTasks(JobBoss jobBoss) } protected abstract IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBoss); + + protected abstract MangaConnector GetMangaConnector(); } \ No newline at end of file diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs index 99670b4f..85a27a4c 100644 --- a/Tranga/Jobs/JobBoss.cs +++ b/Tranga/Jobs/JobBoss.cs @@ -65,11 +65,9 @@ public void RemoveJobs(IEnumerable jobsToRemove) RemoveJob(job); } - public IEnumerable GetJobsLike(string? connectorName = null, string? internalId = null, string? chapterNumber = null) + public IEnumerable GetJobsLike(string? internalId = null, string? chapterNumber = null) { IEnumerable ret = this.jobs; - if (connectorName is not null) - ret = ret.Where(job => job.mangaConnector.name == connectorName); if (internalId is not null && chapterNumber is not null) ret = ret.Where(jjob => @@ -84,18 +82,18 @@ public IEnumerable GetJobsLike(string? connectorName = null, string? intern { if (jjob is not DownloadNewChapters job) return false; - return job.manga.internalId == internalId; + return job.mangaInternalId == internalId; }); return ret; } - public IEnumerable GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null, + public IEnumerable GetJobsLike(Manga? publication = null, Chapter? chapter = null) { if (chapter is not null) - return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter.Value.chapterNumber); + return GetJobsLike(chapter.Value.parentManga.internalId, chapter.Value.chapterNumber); else - return GetJobsLike(mangaConnector?.name, publication?.internalId); + return GetJobsLike(publication?.internalId); } public Job? GetJobById(string jobId) @@ -154,16 +152,28 @@ private void LoadJobsList(HashSet connectors) new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)))!; this.jobs.Add(job); } + + //Load Manga-Files + ImportManga(); - //Connect jobs to parent-jobs and add Publications to cache + //Connect jobs to parent-jobs foreach (Job job in this.jobs) { this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId)?.AddSubJob(job); - if (job is DownloadNewChapters dncJob) - cachedPublications.Add(dncJob.manga); } - HashSet coverFileNames = cachedPublications.Select(manga => manga.coverFileNameInCache!).ToHashSet(); + string[] jobMangaInternalIds = this.jobs.Where(job => job is DownloadNewChapters) + .Select(dnc => ((DownloadNewChapters)dnc).mangaInternalId).ToArray(); + jobMangaInternalIds = jobMangaInternalIds.Concat( + this.jobs.Where(job => job is UpdateMetadata) + .Select(dnc => ((UpdateMetadata)dnc).mangaInternalId)).ToArray(); + string[] internalIds = GetAllCachedManga().Select(m => m.internalId).ToArray(); + + string[] extraneousIds = internalIds.Except(jobMangaInternalIds).ToArray(); + foreach (string internalId in extraneousIds) + RemoveMangaFromCache(internalId); + + HashSet coverFileNames = GetAllCachedManga().Select(manga => manga.coverFileNameInCache!).ToHashSet(); foreach (string fileName in Directory.GetFiles(settings.coverImageCache)) //Cleanup Unused Covers { if(!coverFileNames.Any(existingManga => fileName.Contains(existingManga))) @@ -171,7 +181,7 @@ private void LoadJobsList(HashSet connectors) } } - private void UpdateJobFile(Job job) + internal void UpdateJobFile(Job job) { string jobFilePath = Path.Join(settings.jobsFolderPath, $"{job.id}.json"); @@ -245,7 +255,9 @@ public void CheckJobs() Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}"); }else if (queueHead.progressToken.state is ProgressToken.State.Standby) { - Job[] subJobs = jobQueue.Peek().ExecuteReturnSubTasks(this).ToArray(); + Job eJob = jobQueue.Peek(); + Job[] subJobs = eJob.ExecuteReturnSubTasks(this).ToArray(); + UpdateJobFile(eJob); AddJobs(subJobs); AddJobsToQueue(subJobs); }else if (queueHead.progressToken.state is ProgressToken.State.Running && DateTime.Now.Subtract(queueHead.progressToken.lastUpdate) > TimeSpan.FromMinutes(5)) diff --git a/Tranga/Jobs/JobJsonConverter.cs b/Tranga/Jobs/JobJsonConverter.cs index 9b372585..26a1a2a1 100644 --- a/Tranga/Jobs/JobJsonConverter.cs +++ b/Tranga/Jobs/JobJsonConverter.cs @@ -30,16 +30,9 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis return Enum.Parse(jo["jobType"]!.Value().ToString()) switch { Job.JobType.UpdateMetaDataJob => new UpdateMetadata(_clone, - jo.GetValue("manga")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() - { - Converters = { this._mangaConnectorJsonConverter } - })), + jo.GetValue("mangaInternalId")!.Value()!, jo.GetValue("parentJobId")!.Value()), Job.JobType.DownloadChapterJob => new DownloadChapter(this._clone, - jo.GetValue("mangaConnector")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() - { - Converters = { this._mangaConnectorJsonConverter } - }))!, jo.GetValue("chapter")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() { Converters = { this._mangaConnectorJsonConverter } @@ -47,10 +40,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis DateTime.UnixEpoch, jo.GetValue("parentJobId")!.Value()), Job.JobType.DownloadNewChaptersJob => new DownloadNewChapters(this._clone, - jo.GetValue("manga")!.ToObject(JsonSerializer.Create(new JsonSerializerSettings() - { - Converters = { this._mangaConnectorJsonConverter } - })), + jo.GetValue("mangaInternalId")!.Value()!, jo.GetValue("lastExecution") is {} le ? le.ToObject() : DateTime.UnixEpoch, diff --git a/Tranga/Jobs/UpdateMetadata.cs b/Tranga/Jobs/UpdateMetadata.cs index 121ce47d..38902f35 100644 --- a/Tranga/Jobs/UpdateMetadata.cs +++ b/Tranga/Jobs/UpdateMetadata.cs @@ -1,19 +1,21 @@ -using Tranga.MangaConnectors; +using System.Text.Json.Serialization; +using Tranga.MangaConnectors; namespace Tranga.Jobs; public class UpdateMetadata : Job { - public Manga manga { get; set; } + public string mangaInternalId { get; set; } + [JsonIgnore] private Manga? manga => GetCachedManga(mangaInternalId); - public UpdateMetadata(GlobalBase clone, Manga manga, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, manga.mangaConnector, parentJobId: parentJobId) + public UpdateMetadata(GlobalBase clone, string mangaInternalId, string? parentJobId = null) : base(clone, JobType.UpdateMetaDataJob, parentJobId: parentJobId) { - this.manga = manga; + this.mangaInternalId = mangaInternalId; } protected override string GetId() { - return $"{GetType()}-{manga.internalId}"; + return $"{GetType()}-{mangaInternalId}"; } public override string ToString() @@ -23,8 +25,14 @@ public override string ToString() protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBoss) { + if (manga is null) + { + Log($"Manga {mangaInternalId} is missing! Can not execute job."); + return Array.Empty(); + } + //Retrieve new Metadata - Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.publicationId); + Manga? possibleUpdatedManga = mangaConnector.GetMangaFromId(manga.Value.publicationId); if (possibleUpdatedManga is { } updatedManga) { if (updatedManga.Equals(this.manga)) //Check if anything changed @@ -33,8 +41,8 @@ protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBos return Array.Empty(); } - this.manga.UpdateMetadata(updatedManga); - this.manga.SaveSeriesInfoJson(settings.downloadLocation, true); + AddMangaToCache(manga.Value.WithMetadata(updatedManga)); + this.manga.Value.SaveSeriesInfoJson(settings.downloadLocation, true); this.progressToken.Complete(); } else @@ -47,12 +55,18 @@ protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBos return Array.Empty(); } + protected override MangaConnector GetMangaConnector() + { + if (manga is null) + throw new Exception($"Missing Manga {mangaInternalId}"); + return manga.Value.mangaConnector; + } + public override bool Equals(object? obj) { if (obj is not UpdateMetadata otherJob) return false; - return otherJob.mangaConnector == this.mangaConnector && - otherJob.manga.Equals(this.manga); + return otherJob.manga.Equals(this.manga); } } \ No newline at end of file diff --git a/Tranga/Manga.cs b/Tranga/Manga.cs index d1a0e859..139b9c56 100644 --- a/Tranga/Manga.cs +++ b/Tranga/Manga.cs @@ -74,16 +74,20 @@ public Manga(MangaConnector mangaConnector, string sortName, List author this.websiteUrl = websiteUrl; } - public void UpdateMetadata(Manga newManga) + public Manga WithMetadata(Manga newManga) { - this.sortName = newManga.sortName; - this.description = newManga.description; - this.coverUrl = newManga.coverUrl; - this.authors = authors.Union(newManga.authors).ToList(); - this.altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value); - this.tags = tags.Union(newManga.tags).ToArray(); - this.releaseStatus = newManga.releaseStatus; - this.year = newManga.year; + return this with + { + sortName = newManga.sortName, + description = newManga.description, + coverUrl = newManga.coverUrl, + authors = authors.Union(newManga.authors).ToList(), + altTitles = altTitles.UnionBy(newManga.altTitles, kv => kv.Key).ToDictionary(x => x.Key, x => x.Value), + tags = tags.Union(newManga.tags).ToArray(), + releaseStatus = newManga.releaseStatus, + year = newManga.year, + websiteUrl = newManga.websiteUrl + }; } public override bool Equals(object? obj) @@ -96,6 +100,7 @@ public override bool Equals(object? obj) this.sortName == compareManga.sortName && this.latestChapterAvailable.Equals(compareManga.latestChapterAvailable) && this.authors.All(a => compareManga.authors.Contains(a)) && + this.websiteUrl.Equals(compareManga.websiteUrl) && this.tags.All(t => compareManga.tags.Contains(t)); } diff --git a/Tranga/MangaConnectors/Bato.cs b/Tranga/MangaConnectors/Bato.cs index 657af007..b7257cc1 100644 --- a/Tranga/MangaConnectors/Bato.cs +++ b/Tranga/MangaConnectors/Bato.cs @@ -116,7 +116,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi Manga manga = new (this, sortName, authors, description, altTitles, tags, posterUrl, coverFileNameInCache, new Dictionary(), year, originalLanguage, publicationId, releaseStatus, websiteUrl); - cachedPublications.Add(manga); + AddMangaToCache(manga); return manga; } diff --git a/Tranga/MangaConnectors/MangaDex.cs b/Tranga/MangaConnectors/MangaDex.cs index c764dcd9..69a43852 100644 --- a/Tranga/MangaConnectors/MangaDex.cs +++ b/Tranga/MangaConnectors/MangaDex.cs @@ -188,7 +188,7 @@ public override Manga[] GetManga(string publicationTitle = "") releaseStatus, $"https://mangadex.org/title/{publicationId}" ); - cachedPublications.Add(pub); + AddMangaToCache(pub); return pub; } diff --git a/Tranga/MangaConnectors/MangaKatana.cs b/Tranga/MangaConnectors/MangaKatana.cs index 9ab937a1..50881080 100644 --- a/Tranga/MangaConnectors/MangaKatana.cs +++ b/Tranga/MangaConnectors/MangaKatana.cs @@ -143,7 +143,7 @@ private Manga ParseSinglePublicationFromHtml(Stream html, string publicationId, Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); - cachedPublications.Add(manga); + AddMangaToCache(manga); return manga; } diff --git a/Tranga/MangaConnectors/MangaLife.cs b/Tranga/MangaConnectors/MangaLife.cs index 9245ec28..cb8f1c68 100644 --- a/Tranga/MangaConnectors/MangaLife.cs +++ b/Tranga/MangaConnectors/MangaLife.cs @@ -123,7 +123,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); - cachedPublications.Add(manga); + AddMangaToCache(manga); return manga; } diff --git a/Tranga/MangaConnectors/Manganato.cs b/Tranga/MangaConnectors/Manganato.cs index 793c0d0c..2377c7e1 100644 --- a/Tranga/MangaConnectors/Manganato.cs +++ b/Tranga/MangaConnectors/Manganato.cs @@ -129,7 +129,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); - cachedPublications.Add(manga); + AddMangaToCache(manga); return manga; } diff --git a/Tranga/MangaConnectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs index 935c866b..bd443079 100644 --- a/Tranga/MangaConnectors/Mangasee.cs +++ b/Tranga/MangaConnectors/Mangasee.cs @@ -178,7 +178,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); - cachedPublications.Add(manga); + AddMangaToCache(manga); return manga; } diff --git a/Tranga/MangaConnectors/Mangaworld.cs b/Tranga/MangaConnectors/Mangaworld.cs index fbacfaa7..9f4ca3e1 100644 --- a/Tranga/MangaConnectors/Mangaworld.cs +++ b/Tranga/MangaConnectors/Mangaworld.cs @@ -120,7 +120,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi Manga manga = new (this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); - cachedPublications.Add(manga); + AddMangaToCache(manga); return manga; } diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 863b9d9b..decd1ed1 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -33,7 +33,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), new ("Get", @"/v2/Job/Types", GetV2JobTypes), - new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobsCreateType), + new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index fda77a09..e40d9a93 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -38,7 +38,7 @@ public partial class Server return new ValueTuple(HttpStatusCode.OK, Enum.GetNames(typeof(Job.JobType))); } - private ValueTuple PostV2JobsCreateType(GroupCollection groups, Dictionary requestParameters) + private ValueTuple PostV2JobCreateType(GroupCollection groups, Dictionary requestParameters) { if (groups.Count < 1 || !Enum.TryParse(groups[1].Value, true, out Job.JobType jobType)) @@ -59,14 +59,14 @@ public partial class Server !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) return new ValueTuple(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format."); requestParameters.TryGetValue("language", out string? language); - _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, (Manga)manga, true, interval, language)); + _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, ((Manga)manga).internalId, true, interval, language)); return new ValueTuple(HttpStatusCode.OK, null); case Job.JobType.UpdateMetaDataJob: if(!requestParameters.TryGetValue("internalId", out mangaId) || !_parent.TryGetPublicationById(mangaId, out manga) || manga is null) return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); - _parent.jobBoss.AddJob(new UpdateMetadata(this, (Manga)manga)); + _parent.jobBoss.AddJob(new UpdateMetadata(this, ((Manga)manga).internalId)); return new ValueTuple(HttpStatusCode.OK, null); case Job.JobType.DownloadNewChaptersJob: //TODO case Job.JobType.DownloadChapterJob: //TODO diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index b8c3a28d..91c79944 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -8,7 +8,7 @@ public partial class Server { private ValueTuple GetV2Manga(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, cachedPublications.Select(m => m.internalId)); + return new ValueTuple(HttpStatusCode.OK, GetAllCachedManga().Select(m => m.internalId)); } private ValueTuple GetV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) @@ -28,6 +28,7 @@ public partial class Server return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); Job[] jobs = _parent.jobBoss.GetJobsLike(publication: manga).ToArray(); _parent.jobBoss.RemoveJobs(jobs); + RemoveMangaFromCache(groups[1].Value); return new ValueTuple(HttpStatusCode.OK, null); } diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index 4f286703..dc264111 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -9,7 +9,6 @@ public partial class Tranga : GlobalBase public bool keepRunning; public JobBoss jobBoss; private Server.Server _server; - private HashSet _connectors; public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings) { @@ -54,12 +53,7 @@ public IEnumerable GetConnectors() return _connectors.Select(c => c.name); } - public Manga? GetPublicationById(string internalId) - { - if (cachedPublications.Exists(publication => publication.internalId == internalId)) - return cachedPublications.First(publication => publication.internalId == internalId); - return null; - } + public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId); public bool TryGetPublicationById(string internalId, out Manga? manga) { diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index 2ad9492f..70470069 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -19,6 +19,7 @@ public class TrangaSettings [JsonIgnore] public string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); [JsonIgnore] public string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json"); [JsonIgnore] public string jobsFolderPath => Path.Join(workingDirectory, "jobs"); + [JsonIgnore] public string mangaCacheFolderPath => Path.Join(workingDirectory, "manga"); [JsonIgnore] public string coverImageCache => Path.Join(workingDirectory, "imageCache"); [JsonIgnore] internal static readonly string DefaultUserAgent = $"Tranga ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")}) / 1.0"; public ushort? version { get; } = 2; From a56555eee45fd156299527f87837e7cf641937e3 Mon Sep 17 00:00:00 2001 From: Glax Date: Tue, 23 Apr 2024 00:48:08 +0200 Subject: [PATCH 35/95] Add LibraryConnector.Test to see if requests can be made to endpoint. --- Tranga/LibraryConnectors/Kavita.cs | 10 +++++++++- Tranga/LibraryConnectors/Komga.cs | 8 ++++++++ Tranga/LibraryConnectors/LibraryConnector.cs | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Tranga/LibraryConnectors/Kavita.cs b/Tranga/LibraryConnectors/Kavita.cs index 3fb89838..4dfb02b6 100644 --- a/Tranga/LibraryConnectors/Kavita.cs +++ b/Tranga/LibraryConnectors/Kavita.cs @@ -67,7 +67,15 @@ public override void UpdateLibrary() foreach (KavitaLibrary lib in GetLibraries()) NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger); } - + + internal override bool Test() + { + foreach (KavitaLibrary lib in GetLibraries()) + if (NetClient.MakePost($"{baseUrl}/api/Library/scan?libraryId={lib.id}", "Bearer", auth, logger)) + return true; + return false; + } + /// /// Fetches all libraries available to the user /// diff --git a/Tranga/LibraryConnectors/Komga.cs b/Tranga/LibraryConnectors/Komga.cs index 7a0c4854..557d934c 100644 --- a/Tranga/LibraryConnectors/Komga.cs +++ b/Tranga/LibraryConnectors/Komga.cs @@ -32,6 +32,14 @@ public override void UpdateLibrary() NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger); } + internal override bool Test() + { + foreach (KomgaLibrary lib in GetLibraries()) + if (NetClient.MakePost($"{baseUrl}/api/v1/libraries/{lib.id}/scan", "Basic", auth, logger)) + return true; + return false; + } + /// /// Fetches all libraries available to the user /// diff --git a/Tranga/LibraryConnectors/LibraryConnector.cs b/Tranga/LibraryConnectors/LibraryConnector.cs index 0a66890b..80dd5b28 100644 --- a/Tranga/LibraryConnectors/LibraryConnector.cs +++ b/Tranga/LibraryConnectors/LibraryConnector.cs @@ -30,6 +30,7 @@ protected LibraryConnector(GlobalBase clone, string baseUrl, string auth, Librar this.libraryType = libraryType; } public abstract void UpdateLibrary(); + internal abstract bool Test(); protected static class NetClient { From 0ced3a7dd981ef4d870acae0f53e8c66e0abfdb4 Mon Sep 17 00:00:00 2001 From: Glax Date: Tue, 23 Apr 2024 00:48:25 +0200 Subject: [PATCH 36/95] Implement /v2/LibraryConnector/* --- Tranga/Server/v2Jobs.cs | 5 +- Tranga/Server/v2LibraryConnectors.cs | 95 ++++++++++++++++++++++++++-- docs/API_Calls_v2.md | 20 +++--- 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index e40d9a93..6581fb14 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -35,7 +35,8 @@ public partial class Server private ValueTuple GetV2JobTypes(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, Enum.GetNames(typeof(Job.JobType))); + return new ValueTuple(HttpStatusCode.OK, + Enum.GetValues().ToDictionary(b => (byte)b, b => Enum.GetName(b))); } private ValueTuple PostV2JobCreateType(GroupCollection groups, Dictionary requestParameters) @@ -43,7 +44,7 @@ public partial class Server if (groups.Count < 1 || !Enum.TryParse(groups[1].Value, true, out Job.JobType jobType)) { - return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID: '{groups[1].Value}' does not exist."); + return new ValueTuple(HttpStatusCode.NotFound, $"JobType {groups[1].Value} does not exist."); } string? mangaId; diff --git a/Tranga/Server/v2LibraryConnectors.cs b/Tranga/Server/v2LibraryConnectors.cs index 5ee6287b..f989edf8 100644 --- a/Tranga/Server/v2LibraryConnectors.cs +++ b/Tranga/Server/v2LibraryConnectors.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.RegularExpressions; +using Tranga.LibraryConnectors; namespace Tranga.Server; @@ -7,31 +8,113 @@ public partial class Server { private ValueTuple GetV2LibraryConnector(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, libraryConnectors); } private ValueTuple GetV2LibraryConnectorTypes(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, + Enum.GetValues().ToDictionary(b => (byte)b, b => Enum.GetName(b))); } private ValueTuple GetV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist."); + } + + if(libraryConnectors.All(lc => lc.libraryType != libraryType)) + return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured."); + else + return new ValueTuple(HttpStatusCode.OK, libraryConnectors.First(lc => lc.libraryType == libraryType)); } private ValueTuple PostV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist."); + } + + if(!requestParameters.TryGetValue("URL", out string? url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + + switch (libraryType) + { + case LibraryConnector.LibraryType.Kavita: + if(!requestParameters.TryGetValue("username", out string? username)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); + if(!requestParameters.TryGetValue("password", out string? password)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); + Kavita kavita = new (this, url, username, password); + libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Kavita); + libraryConnectors.Add(kavita); + return new ValueTuple(HttpStatusCode.OK, kavita); + case LibraryConnector.LibraryType.Komga: + if(!requestParameters.TryGetValue("auth", out string? auth)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); + Komga komga = new (this, url, auth); + libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Komga); + libraryConnectors.Add(komga); + return new ValueTuple(HttpStatusCode.OK, komga); + default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported."); + } } private ValueTuple PostV2LibraryConnectorTypeTest(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist."); + } + + if(!requestParameters.TryGetValue("URL", out string? url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + + switch (libraryType) + { + case LibraryConnector.LibraryType.Kavita: + if(!requestParameters.TryGetValue("username", out string? username)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); + if(!requestParameters.TryGetValue("password", out string? password)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); + Kavita kavita = new (this, url, username, password); + return kavita.Test() switch + { + true => new ValueTuple(HttpStatusCode.OK, kavita), + _ => new ValueTuple(HttpStatusCode.FailedDependency, kavita) + }; + case LibraryConnector.LibraryType.Komga: + if(!requestParameters.TryGetValue("auth", out string? auth)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); + Komga komga = new (this, url, auth); + return komga.Test() switch + { + true => new ValueTuple(HttpStatusCode.OK, komga), + _ => new ValueTuple(HttpStatusCode.FailedDependency, komga) + }; + default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"LibraryType {Enum.GetName(libraryType)} is not supported."); + } } private ValueTuple DeleteV2LibraryConnectorType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out LibraryConnector.LibraryType libraryType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist."); + } + + if(libraryConnectors.All(lc => lc.libraryType != libraryType)) + return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {Enum.GetName(libraryType)} not configured."); + else + { + libraryConnectors.Remove(libraryConnectors.First(lc => lc.libraryType == libraryType)); + return new ValueTuple(HttpStatusCode.OK, null); + } } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 056a26c4..17c7c17b 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -640,9 +640,9 @@ Returns the Library-Connector for the specified Type. [LibraryConnector](Types.md#libraryconnector) - | StatusCode | Meaning | - |------------|---------------------------------------| - | 404 | Library Connector Type does not exist | + | StatusCode | Meaning | + |------------|----------------------------------------------------| + | 404 | Library Connector of specified Type does not exist | ### ![POST](https://img.shields.io/badge/POST-00f) `/v2/LibraryConnector/` @@ -681,6 +681,7 @@ Creates a Library-Connector of the specified Type. | StatusCode | Meaning | |------------|----------------------------------| | 404 | Library Connector does not exist | + | 406 | Missing Parameter | | 500 | Parsing Error | @@ -715,12 +716,13 @@ Tests a Library-Connector of the specified Type.
Returns - | StatusCode | Meaning | - |------------|---------------------------------------| - | 200 | Test successful | - | 404 | Library Connector Type does not exist | - | 408 | Test failed | - | 500 | Parsing Error | + | StatusCode | Meaning | + |------------|------------------------------------| + | 200 | Test successful | + | 404 | Library Connector does not exist | + | 406 | Missing Parameter | + | 424 | Test failed | + | 500 | Parsing Error |
### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/LibraryConnector/` From 2651a0c53ba0b7ceff6ea6bc6dabe682198218a0 Mon Sep 17 00:00:00 2001 From: Glax Date: Tue, 23 Apr 2024 00:58:19 +0200 Subject: [PATCH 37/95] Implemented /v2/NotificationConnector/* --- Tranga/Server/v2NotificationConnectors.cs | 107 ++++++++++++++++++++-- docs/API_Calls_v2.md | 4 +- 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/Tranga/Server/v2NotificationConnectors.cs b/Tranga/Server/v2NotificationConnectors.cs index 7d7cb731..741b2d6c 100644 --- a/Tranga/Server/v2NotificationConnectors.cs +++ b/Tranga/Server/v2NotificationConnectors.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.RegularExpressions; +using Tranga.NotificationConnectors; namespace Tranga.Server; @@ -7,31 +8,125 @@ public partial class Server { private ValueTuple GetV2NotificationConnector(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, notificationConnectors); } private ValueTuple GetV2NotificationConnectorTypes(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + return new ValueTuple(HttpStatusCode.OK, + Enum.GetValues().ToDictionary(b => (byte)b, b => Enum.GetName(b))); } private ValueTuple GetV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist."); + } + + if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType)) + return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured."); + else + return new ValueTuple(HttpStatusCode.OK, notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType)); } private ValueTuple PostV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist."); + } + + string? url; + switch (notificationConnectorType) + { + case NotificationConnector.NotificationConnectorType.Gotify: + if(!requestParameters.TryGetValue("url", out url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("appToken", out string? appToken)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing."); + Gotify gotify = new (this, url, appToken); + this.notificationConnectors.RemoveWhere(nc => + nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Gotify); + this.notificationConnectors.Add(gotify); + return new ValueTuple(HttpStatusCode.OK, gotify); + case NotificationConnector.NotificationConnectorType.LunaSea: + if(!requestParameters.TryGetValue("webhook", out string? webhook)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing."); + LunaSea lunaSea = new (this, webhook); + this.notificationConnectors.RemoveWhere(nc => + nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.LunaSea); + this.notificationConnectors.Add(lunaSea); + return new ValueTuple(HttpStatusCode.OK, lunaSea); + case NotificationConnector.NotificationConnectorType.Ntfy: + if(!requestParameters.TryGetValue("url", out url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("auth", out string? auth)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); + Ntfy ntfy = new(this, url, auth); + this.notificationConnectors.RemoveWhere(nc => + nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Ntfy); + this.notificationConnectors.Add(ntfy); + return new ValueTuple(HttpStatusCode.OK, ntfy); + default: + return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported."); + } } private ValueTuple PostV2NotificationConnectorTypeTest(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist."); + } + + string? url; + switch (notificationConnectorType) + { + case NotificationConnector.NotificationConnectorType.Gotify: + if(!requestParameters.TryGetValue("url", out url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("appToken", out string? appToken)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'appToken' missing."); + Gotify gotify = new (this, url, appToken); + gotify.SendNotification("Tranga Test", "It was successful :3"); + return new ValueTuple(HttpStatusCode.OK, gotify); + case NotificationConnector.NotificationConnectorType.LunaSea: + if(!requestParameters.TryGetValue("webhook", out string? webhook)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'webhook' missing."); + LunaSea lunaSea = new (this, webhook); + lunaSea.SendNotification("Tranga Test", "It was successful :3"); + return new ValueTuple(HttpStatusCode.OK, lunaSea); + case NotificationConnector.NotificationConnectorType.Ntfy: + if(!requestParameters.TryGetValue("url", out url)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("auth", out string? auth)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); + Ntfy ntfy = new(this, url, auth); + ntfy.SendNotification("Tranga Test", "It was successful :3"); + return new ValueTuple(HttpStatusCode.OK, ntfy); + default: + return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"NotificationType {Enum.GetName(notificationConnectorType)} is not supported."); + } } private ValueTuple DeleteV2NotificationConnectorType(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.NotImplemented, "Not Implemented"); + if (groups.Count < 1 || + !Enum.TryParse(groups[1].Value, true, out NotificationConnector.NotificationConnectorType notificationConnectorType)) + { + return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {groups[1].Value} does not exist."); + } + + if(notificationConnectors.All(nc => nc.notificationConnectorType != notificationConnectorType)) + return new ValueTuple(HttpStatusCode.NotFound, $"NotificationType {Enum.GetName(notificationConnectorType)} not configured."); + else + { + notificationConnectors.Remove(notificationConnectors.First(nc => nc.notificationConnectorType != notificationConnectorType)); + return new ValueTuple(HttpStatusCode.OK, null); + } } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 17c7c17b..3948cf96 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -784,6 +784,7 @@ Returns the configured Notification-Connector of the specified Type. | StatusCode | Meaning | |------------|---------------------------------------| | 404 | Library Connector Type does not exist | + | 500 | Parsing Error | ### ![POST](https://img.shields.io/badge/POST-00f) `/v2/NotificationConnector/` @@ -825,6 +826,7 @@ Creates a Notification-Connector of the specified Type. | StatusCode | Meaning | |------------|--------------------------------------------| | 404 | Notification Connector Type does not exist | + | 406 | Missing Parameter | | 500 | Parsing Error | @@ -866,7 +868,7 @@ Tests a Notification-Connector of the specified Type. |------------|--------------------------------------------| | 200 | Test successful | | 404 | Notification Connector Type does not exist | - | 408 | Test failed | + | 406 | Missing Parameter | | 500 | Parsing Error | From 28a0efe4886c577249c4d5f36585efd7ff5a96b9 Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 25 Apr 2024 23:45:13 +0200 Subject: [PATCH 38/95] Add Endpoint /v2/Manga/internalId/Chapters/Latest --- Tranga/Server/Server.cs | 1 + Tranga/Server/v2Manga.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index decd1ed1..4916b37f 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -28,6 +28,7 @@ public Server(Tranga parent) : base(parent) new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters), + new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest), new ("GET", @"/v2/Jobs", GetV2Jobs), new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 91c79944..d661ae88 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -61,4 +61,19 @@ public partial class Server }; return new ValueTuple(HttpStatusCode.OK, chapters); } + + private ValueTuple GetV2MangaInternalIdChaptersLatest(GroupCollection groups, Dictionary requestParameters) + { + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + + float latest = requestParameters.TryGetValue("language", out string? parameter) switch + { + true => float.Parse(manga.Value.mangaConnector.GetChapters(manga.Value, parameter).Max().chapterNumber), + false => float.Parse(manga.Value.mangaConnector.GetChapters(manga.Value).Max().chapterNumber) + }; + return new ValueTuple(HttpStatusCode.OK, latest); + } } \ No newline at end of file From 80dc8fbe6541741bfc2db89e6005a22ef4c591e7 Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 25 Apr 2024 23:50:06 +0200 Subject: [PATCH 39/95] Resolves #176 Return 409 conflict if job already exists. --- Tranga/Jobs/JobBoss.cs | 4 +++- Tranga/Server/v2Jobs.cs | 15 +++++++++++---- docs/API_Calls_v2.md | 2 ++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs index 72d0c529..0ef6996a 100644 --- a/Tranga/Jobs/JobBoss.cs +++ b/Tranga/Jobs/JobBoss.cs @@ -17,17 +17,19 @@ public JobBoss(GlobalBase clone, HashSet connectors) : base(clon Log($"Next job in {jobs.MinBy(job => job.nextExecution)?.nextExecution.Subtract(DateTime.Now)} {jobs.MinBy(job => job.nextExecution)?.id}"); } - public void AddJob(Job job) + public bool AddJob(Job job) { if (ContainsJobLike(job)) { Log($"Already Contains Job {job}"); + return false; } else { Log($"Added {job}"); this.jobs.Add(job); UpdateJobFile(job); + return true; } } diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index 6581fb14..244347a5 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -60,15 +60,22 @@ public partial class Server !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) return new ValueTuple(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format."); requestParameters.TryGetValue("language", out string? language); - _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, ((Manga)manga).internalId, true, interval, language)); - return new ValueTuple(HttpStatusCode.OK, null); + return _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, + ((Manga)manga).internalId, true, interval, language)) switch + { + true => new ValueTuple(HttpStatusCode.OK, null), + false => new ValueTuple(HttpStatusCode.Conflict, "Job already exists."), + }; case Job.JobType.UpdateMetaDataJob: if(!requestParameters.TryGetValue("internalId", out mangaId) || !_parent.TryGetPublicationById(mangaId, out manga) || manga is null) return new ValueTuple(HttpStatusCode.NotFound, "InternalId Parameter missing, or is not a valid ID."); - _parent.jobBoss.AddJob(new UpdateMetadata(this, ((Manga)manga).internalId)); - return new ValueTuple(HttpStatusCode.OK, null); + return _parent.jobBoss.AddJob(new UpdateMetadata(this, ((Manga)manga).internalId)) switch + { + true => new ValueTuple(HttpStatusCode.OK, null), + false => new ValueTuple(HttpStatusCode.Conflict, "Job already exists."), + }; case Job.JobType.DownloadNewChaptersJob: //TODO case Job.JobType.DownloadChapterJob: //TODO default: return new ValueTuple(HttpStatusCode.MethodNotAllowed, $"JobType {Enum.GetName(jobType)} is not supported."); diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 3948cf96..fbbdfbc8 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -276,7 +276,9 @@ Creates a Job. | StatusCode | Meaning | |------------|------------------------------------------| + | 200 | Job created. | | 404 | Parameter missing or could not be found. | + | 409 | Job already exists | | 500 | Error parsing interval | From 061da1b4bf7153ad7a14b47431f56f2c4424381f Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 25 Apr 2024 23:55:31 +0200 Subject: [PATCH 40/95] Add field customFolder and startChapter to CreateJob Endpoint https://github.com/C9Glax/tranga/pull/167#issuecomment-2077909075 #167 --- Tranga/Server/v2Jobs.cs | 9 +++++++++ docs/API_Calls_v2.md | 12 +++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index 244347a5..cc4e7386 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -60,6 +60,15 @@ public partial class Server !TimeSpan.TryParse(intervalStr, out TimeSpan interval)) return new ValueTuple(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format."); requestParameters.TryGetValue("language", out string? language); + if (requestParameters.TryGetValue("customFolder", out string? folder)) + manga.Value.MovePublicationFolder(settings.downloadLocation, folder); + if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) && + float.TryParse(startChapterStr, out float startChapter)) + { + Manga manga1 = manga.Value; + manga1.ignoreChaptersBelow = startChapter; + } + return _parent.jobBoss.AddJob(new DownloadNewChapters(this, ((Manga)manga).mangaConnector, ((Manga)manga).internalId, true, interval, language)) switch { diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index fbbdfbc8..df9c9a79 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -256,11 +256,13 @@ Creates a Job. `Type` is returned in the response of [GET /v2/Job/Types](#-v2jobtypes) - | Parameter | Value | - |------------|---------------------------------------------------------------------------------------------------| - | internalId | Manga ID | - | *interval* | Interval at which the Job is re-run in HH:MM:SS format
Only for MonitorManga, UpdateMetadata | - | *language* | Translated language
Only for MonitorManga, DownloadNewChapters and DownloadChapter | + | Parameter | Value | + |----------------|---------------------------------------------------------------------------------------------------| + | internalId | Manga ID | + | *customFolder* | Custom folder location
Only for MonitorManga, DownloadNewChapters and DownloadChapter | + | *startChapter* | Chapter to start downloading at
Only for MonitorManga, DownloadNewChapters | + | *interval* | Interval at which the Job is re-run in HH:MM:SS format
Only for MonitorManga, UpdateMetadata | + | *language* | Translated language
Only for MonitorManga, DownloadNewChapters and DownloadChapter | `internalId` is returned in the response of From 8887cea718ef308d7fe0a8e7cb78f5537b77683b Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:00:57 +0200 Subject: [PATCH 41/95] Add Endpoint POST /v2/Manga/internalId/ignoreChaptersBelow #167 --- Tranga/Server/Server.cs | 1 + Tranga/Server/v2Manga.cs | 16 ++++++++++++++++ docs/API_Calls_v2.md | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 4916b37f..cde7bd9b 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -29,6 +29,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest), + new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/ignoreChaptersBelow", PostV2MangaInternalIdIgnoreChaptersBelow), new ("GET", @"/v2/Jobs", GetV2Jobs), new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index d661ae88..cee3dfa2 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -76,4 +76,20 @@ public partial class Server }; return new ValueTuple(HttpStatusCode.OK, latest); } + + private ValueTuple PostV2MangaInternalIdIgnoreChaptersBelow(GroupCollection groups, Dictionary requestParameters) + { + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) && + float.TryParse(startChapterStr, out float startChapter)) + { + Manga manga1 = manga.Value; + manga1.ignoreChaptersBelow = startChapter; + return new ValueTuple(HttpStatusCode.OK, null); + }else + return new ValueTuple(HttpStatusCode.InternalServerError, "Parameter 'startChapter' missing, or failed to parse."); + } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index df9c9a79..867b3fa7 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -195,6 +195,30 @@ Returns the latest Chapter of the specified Manga. | 404 | Manga with `internalId` could not be found | +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Manga//ignoreChaptersBelow` + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) + + | Parameter | Value | + |--------------|----------------------------| + | startChapter | Chapter-number to start at | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 404 | Manga with `internalId` could not be found | + | 500 | Parsing Error | +
+ ## Jobs [^top](#top) ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs` From 2e1f633f405a2e9ac6ca38bc851e09674672b41d Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:05:48 +0200 Subject: [PATCH 42/95] Add Endpoint POST /v2/Manga/internalId/moveFolder #167 --- Tranga/Server/Server.cs | 1 + Tranga/Server/v2Manga.cs | 13 +++++++++++++ docs/API_Calls_v2.md | 25 +++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index cde7bd9b..cc8db46a 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -30,6 +30,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest), new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/ignoreChaptersBelow", PostV2MangaInternalIdIgnoreChaptersBelow), + new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/moveFolder", PostV2MangaInternalIdMoveFolder), new ("GET", @"/v2/Jobs", GetV2Jobs), new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index cee3dfa2..947d039c 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -92,4 +92,17 @@ public partial class Server }else return new ValueTuple(HttpStatusCode.InternalServerError, "Parameter 'startChapter' missing, or failed to parse."); } + + private ValueTuple PostV2MangaInternalIdMoveFolder(GroupCollection groups, Dictionary requestParameters) + { + + if(groups.Count < 1 || + !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || + manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); + if(!requestParameters.TryGetValue("location", out string? newFolder)) + return new ValueTuple(HttpStatusCode.BadRequest, "Parameter 'location' missing."); + manga.Value.MovePublicationFolder(settings.downloadLocation, newFolder); + return new ValueTuple(HttpStatusCode.OK, null); + } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 867b3fa7..e42a1f41 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -219,6 +219,31 @@ Returns the latest Chapter of the specified Manga. | 500 | Parsing Error | +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Manga//moveFolder` + +
+ Request + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) + + | Parameter | Value | + |-----------|-------------------------------------| + | location | New location (relative to root dir) | + +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 400 | Parameter missing | + | 404 | Manga with `internalId` could not be found | +
+ ## Jobs [^top](#top) ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs` From 5b22246c41d1a4adc48f0d7e23ead4a67b43f2ad Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:14:46 +0200 Subject: [PATCH 43/95] Add Endpoint GET /v2/Job returns list of jobs specified by jobid --- Tranga/Server/Server.cs | 1 + Tranga/Server/v2Jobs.cs | 17 ++++++++++++++++- docs/API_Calls_v2.md | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index cc8db46a..f842e1f6 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -37,6 +37,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), new ("Get", @"/v2/Job/Types", GetV2JobTypes), new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType), + new ("GET", @"/v2/Job", GetV2Job), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index cc4e7386..adf6fdbd 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -150,5 +150,20 @@ public partial class Server job.Cancel(); return new ValueTuple(HttpStatusCode.OK, null); } - + + private ValueTuple GetV2Job(GroupCollection groups, Dictionary requestParameters) + { + if(!requestParameters.TryGetValue("jobIds", out string? jobIdListStr)) + return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'jobIds'."); + string[] jobIdList = jobIdListStr.Split(','); + List ret = new(); + foreach (string jobId in jobIdList) + { + if(!_parent.jobBoss.TryGetJobById(jobId, out Job? job) || job is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Job with id '{jobId}' not found."); + ret.Add(job); + } + + return new ValueTuple(HttpStatusCode.OK, ret); + } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index e42a1f41..1587e2a2 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -333,6 +333,35 @@ Creates a Job. | 500 | Error parsing interval | +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` + +Returns the list of Jobs requested. + +
+ Request + + | Parameter | Value | + |------------|--------------------------------| + | jobIds | Comma-Seperated list of jobIds | + + `jobId` is returned in the response of + * [GET /v2/Jobs](#-v2jobs) + * [GET /v2/Jobs/Running](#-v2jobsrunning) + * [GET /v2/Jobs/Waiting](#-v2jobswaiting) + * [GET /v2/Jobs/Monitoring](#-v2jobsmonitoring) +
+ +
+ Returns + + List of [Jobs](Types.md#job) + + | StatusCode | Meaning | + |------------|---------------------------------------| + | 400 | Missing Parameter | + | 404 | Manga with `jobId` could not be found | +
+ ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` Returns the specified Job. From 0735e2c588159e4cd98dcc9b4de843ae48a64cb3 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:16:28 +0200 Subject: [PATCH 44/95] Change GET /v2/Manga to /v2/Mangas --- Tranga/Server/Server.cs | 2 +- Tranga/Server/v2Manga.cs | 2 +- docs/API_Calls_v2.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index f842e1f6..bb3d6eaf 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -23,7 +23,7 @@ public Server(Tranga parent) : base(parent) { new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes), new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga), - new ("GET", @"/v2/Manga", GetV2Manga), + new ("GET", @"/v2/Mangas", GetV2Mangas), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 947d039c..9717e94f 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -6,7 +6,7 @@ namespace Tranga.Server; public partial class Server { - private ValueTuple GetV2Manga(GroupCollection groups, Dictionary requestParameters) + private ValueTuple GetV2Mangas(GroupCollection groups, Dictionary requestParameters) { return new ValueTuple(HttpStatusCode.OK, GetAllCachedManga().Select(m => m.internalId)); } diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 1587e2a2..18d1c2a6 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -67,7 +67,7 @@ Returns the Manga from the specified Manga Connector. ## Manga [^top](#top) -### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga` +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Mangas` Returns all known Manga. From 49a9b7ccb0a397fab7ad8d5a5697d7169260946a Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:19:38 +0200 Subject: [PATCH 45/95] Corrected Job->Manga in return --- docs/API_Calls_v2.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 18d1c2a6..6773089d 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -356,10 +356,10 @@ Returns the list of Jobs requested. List of [Jobs](Types.md#job) - | StatusCode | Meaning | - |------------|---------------------------------------| - | 400 | Missing Parameter | - | 404 | Manga with `jobId` could not be found | + | StatusCode | Meaning | + |------------|-------------------------------------| + | 400 | Missing Parameter | + | 404 | Job with `jobId` could not be found | ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Job/` From 7f95ab94393675e206c45edc684b938381274270 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:22:17 +0200 Subject: [PATCH 46/95] Add Endpoint GET /v2/Manga to request multiple Manga from internalIds #167 --- Tranga/Server/Server.cs | 1 + Tranga/Server/v2Manga.cs | 16 ++++++++++++++++ docs/API_Calls_v2.md | 32 +++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index bb3d6eaf..d2a8640a 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -24,6 +24,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes), new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga), new ("GET", @"/v2/Mangas", GetV2Mangas), + new ("GET", @"/v2/Manga", GetV2Manga), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 9717e94f..f983ec09 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -11,6 +11,22 @@ public partial class Server return new ValueTuple(HttpStatusCode.OK, GetAllCachedManga().Select(m => m.internalId)); } + private ValueTuple GetV2Manga(GroupCollection groups, Dictionary requestParameters) + { + if(!requestParameters.TryGetValue("mangaIds", out string? mangaIdListStr)) + return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'mangaIds'."); + string[] mangaIdList = mangaIdListStr.Split(','); + List ret = new(); + foreach (string mangaId in mangaIdList) + { + if(!_parent.TryGetPublicationById(mangaId, out Manga? manga) || manga is null) + return new ValueTuple(HttpStatusCode.NotFound, $"Manga with id '{mangaId}' not found."); + ret.Add(manga.Value); + } + + return new ValueTuple(HttpStatusCode.OK, ret); + } + private ValueTuple GetV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) { if(groups.Count < 1 || diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 6773089d..f928ca6e 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -100,6 +100,34 @@ Returns the specified Manga. | 404 | Manga with `internalId` could not be found | +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/` + +Returns the list of Mangas requested. + +
+ Request + + | Parameter | Value | + |-----------|--------------------------------------| + | mangaIds | Comma-Seperated list of `internalId` | + + `internalId` is returned in the response of + * [GET /v2/Manga](#-v2manga) + * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) + * [GET /v2/Job/*jobId*](#-v2jobjobid) +
+ +
+ Returns + + List of [Manga](Types.md#manga) + + | StatusCode | Meaning | + |------------|--------------------------------------------| + | 400 | Missing Parameter | + | 404 | Manga with `internalId` could not be found | +
+ ### ![DELETE](https://img.shields.io/badge/DELETE-f00) `/v2/Manga/` Deletes all associated Jobs for the specified Manga @@ -115,10 +143,12 @@ Deletes all associated Jobs for the specified Manga
Returns + + [Manga](Types.md#manga) | StatusCode | Meaning | |------------|--------------------------------------------| - | 200 | Jobs were deleted | + | 200 | Manga was deleted | | 404 | Manga with `internalId` could not be found |
From 40212378882b37db08915193d5ae9fe4699fb2b5 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 00:51:18 +0200 Subject: [PATCH 47/95] Add Endpoint GET /v2/Manga/Search GlobalSearch Resolves #124 #167 --- Tranga/Server/Server.cs | 2 ++ Tranga/Server/v2Manga.cs | 22 ++++++++++++++++++++++ docs/API_Calls_v2.md | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index d2a8640a..43732d24 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -24,7 +24,9 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Connector/Types", GetV2ConnectorTypes), new ("GET", @"/v2/Connector/([a-zA-Z]+)/GetManga", GetV2ConnectorConnectorNameGetManga), new ("GET", @"/v2/Mangas", GetV2Mangas), + new ("GET", @"/v2/Manga/Search", GetV2MangaSearch), new ("GET", @"/v2/Manga", GetV2Manga), + new ("GET", @"/v2/Manga/sear", GetV2Manga), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index f983ec09..44ff843a 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -1,6 +1,7 @@ using System.Net; using System.Text.RegularExpressions; using Tranga.Jobs; +using Tranga.MangaConnectors; namespace Tranga.Server; @@ -26,6 +27,27 @@ public partial class Server return new ValueTuple(HttpStatusCode.OK, ret); } + + private ValueTuple GetV2MangaSearch(GroupCollection groups, Dictionary requestParameters) + { + if(!requestParameters.TryGetValue("title", out string? title)) + return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'title'."); + List ret = new(); + List threads = new(); + foreach (MangaConnector mangaConnector in _connectors) + { + Thread t = new (() => + { + ret.AddRange(mangaConnector.GetManga(title)); + }); + t.Start(); + threads.Add(t); + } + while(threads.Any(t => t.ThreadState is ThreadState.Running or ThreadState.WaitSleepJoin)) + Thread.Sleep(10); + + return new ValueTuple(HttpStatusCode.OK, ret); + } private ValueTuple GetV2MangaInternalId(GroupCollection groups, Dictionary requestParameters) { diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index f928ca6e..545e1d42 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -77,6 +77,29 @@ Returns all known Manga. List of internalIds. +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/Search` + +Initiates a search for a Manga on all Connectors. + +
+ Request + + + | Parameter | Value | + |-----------|-------------------------------------------------| + | title | Search Term | +
+ +
+ Returns + + List of [Manga](Types.md#Manga) + + | StatusCode | Meaning | + |------------|-------------------| + | 400 | Parameter missing | +
+ ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Manga/` Returns the specified Manga. From 017f31ca83911cf2b01dc19f63742d6256dcd481 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 26 Apr 2024 16:39:39 +0200 Subject: [PATCH 48/95] Clean --- Tranga/Server/Server.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 43732d24..13eb5422 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -26,7 +26,6 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Mangas", GetV2Mangas), new ("GET", @"/v2/Manga/Search", GetV2MangaSearch), new ("GET", @"/v2/Manga", GetV2Manga), - new ("GET", @"/v2/Manga/sear", GetV2Manga), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), From fd20b9febf1058b916dc48678a2222d1ac60ccbb Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 15 Jun 2024 21:26:23 +0200 Subject: [PATCH 49/95] NTFY use Username and Password https://github.com/C9Glax/tranga/issues/187 --- Tranga/Server/v2NotificationConnectors.cs | 16 ++++++++++------ docs/API_Calls_v2.md | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Tranga/Server/v2NotificationConnectors.cs b/Tranga/Server/v2NotificationConnectors.cs index 741b2d6c..d6917fb1 100644 --- a/Tranga/Server/v2NotificationConnectors.cs +++ b/Tranga/Server/v2NotificationConnectors.cs @@ -63,9 +63,11 @@ public partial class Server case NotificationConnector.NotificationConnectorType.Ntfy: if(!requestParameters.TryGetValue("url", out url)) return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); - if(!requestParameters.TryGetValue("auth", out string? auth)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); - Ntfy ntfy = new(this, url, auth); + if(!requestParameters.TryGetValue("username", out string? username)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); + if(!requestParameters.TryGetValue("password", out string? password)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); + Ntfy ntfy = new(this, url, username, password, null); this.notificationConnectors.RemoveWhere(nc => nc.notificationConnectorType == NotificationConnector.NotificationConnectorType.Ntfy); this.notificationConnectors.Add(ntfy); @@ -103,9 +105,11 @@ public partial class Server case NotificationConnector.NotificationConnectorType.Ntfy: if(!requestParameters.TryGetValue("url", out url)) return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); - if(!requestParameters.TryGetValue("auth", out string? auth)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); - Ntfy ntfy = new(this, url, auth); + if(!requestParameters.TryGetValue("username", out string? username)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); + if(!requestParameters.TryGetValue("password", out string? password)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); + Ntfy ntfy = new(this, url, username, password, null); ntfy.SendNotification("Tranga Test", "It was successful :3"); return new ValueTuple(HttpStatusCode.OK, ntfy); default: diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 545e1d42..3ff89242 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -950,7 +950,8 @@ Creates a Notification-Connector of the specified Type. | Parameter | Value | |-----------|--------------------------| | url | URL of the Ntfy Instance | - | auth | Auth-String | + | username | Username | + | password | Password |
From fab30dc5a75bace6ba21cfe26ae044210d6cc4d4 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 15 Jun 2024 21:27:24 +0200 Subject: [PATCH 50/95] Documentation https://github.com/C9Glax/tranga/issues/187 --- docs/API_Calls_v2.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 3ff89242..5e32b618 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -994,7 +994,8 @@ Tests a Notification-Connector of the specified Type. | Parameter | Value | |-----------|--------------------------| | url | URL of the Ntfy Instance | - | auth | Auth-String | + | username | Username | + | password | Password |
From 8607bd2c897d9930d21c1fae813462e22347cf0a Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 15 Jun 2024 21:39:53 +0200 Subject: [PATCH 51/95] #187 NTFY JsonConverter --- .../NotificationConnectors/NotificationManagerJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs b/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs index c5963c21..6f310e11 100644 --- a/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs +++ b/Tranga/NotificationConnectors/NotificationManagerJsonConverter.cs @@ -28,7 +28,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis case (byte)NotificationConnector.NotificationConnectorType.LunaSea: return new LunaSea(this._clone, jo.GetValue("id")!.Value()!); case (byte)NotificationConnector.NotificationConnectorType.Ntfy: - return new Ntfy(this._clone, jo.GetValue("endpoint")!.Value()!, jo.GetValue("auth")!.Value()!); + return new Ntfy(this._clone, jo.GetValue("endpoint")!.Value()!, jo.GetValue("topic")!.Value()!, jo.GetValue("auth")!.Value()!); } throw new Exception(); From 9dd52178b939dad6429f7e3d285a29b6f6b5d2e2 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 17:34:45 +0200 Subject: [PATCH 52/95] Update MangaHere bad ManhuaPlus to v2 architecture --- Tranga/MangaConnectors/MangaHere.cs | 2 +- Tranga/MangaConnectors/ManhuaPlus.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tranga/MangaConnectors/MangaHere.cs b/Tranga/MangaConnectors/MangaHere.cs index fc9406e5..49a0d559 100644 --- a/Tranga/MangaConnectors/MangaHere.cs +++ b/Tranga/MangaConnectors/MangaHere.cs @@ -101,7 +101,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi .SelectSingleNode("//p[contains(concat(' ',normalize-space(@class),' '),' fullcontent ')]"); string description = descriptionNode.InnerText; - Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, + Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, null, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); AddMangaToCache(manga); diff --git a/Tranga/MangaConnectors/ManhuaPlus.cs b/Tranga/MangaConnectors/ManhuaPlus.cs index 3fe392ca..8ae36419 100644 --- a/Tranga/MangaConnectors/ManhuaPlus.cs +++ b/Tranga/MangaConnectors/ManhuaPlus.cs @@ -112,7 +112,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi .SelectSingleNode("//div[@id='syn-target']"); string description = descriptionNode.InnerText; - Manga manga = new(sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, + Manga manga = new(this, sortName, authors.ToList(), description, altTitles, tags.ToArray(), posterUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl: websiteUrl); AddMangaToCache(manga); From 8145abb744836f0381a0686c2a5340198a21f78d Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 18:51:58 +0200 Subject: [PATCH 53/95] Fix workign Directory in TrangaArgsMain --- Tranga/TrangaArgs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tranga/TrangaArgs.cs b/Tranga/TrangaArgs.cs index e95ad912..c1743a6a 100644 --- a/Tranga/TrangaArgs.cs +++ b/Tranga/TrangaArgs.cs @@ -38,7 +38,7 @@ public static void Main(string[] args) TrangaSettings? settings = null; bool dlp = fetched.TryGetValue(downloadLocation, out string[]? downloadLocationPath); - bool wdp = fetched.TryGetValue(downloadLocation, out string[]? workingDirectoryPath); + bool wdp = fetched.TryGetValue(workingDirectory, out string[]? workingDirectoryPath); if (dlp && wdp) { @@ -52,7 +52,7 @@ public static void Main(string[] args) }else if (wdp) { if (settings is null) - settings = new TrangaSettings(downloadLocation: workingDirectoryPath![0]); + settings = new TrangaSettings(workingDirectory: workingDirectoryPath![0]); else settings = new TrangaSettings(settings.downloadLocation, workingDirectoryPath![0]); } From b18f8e4059effee307357d44845f97755a4f5492 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 21:00:38 +0200 Subject: [PATCH 54/95] Fix GET /v2/Job/Types --- Tranga/Server/Server.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 13eb5422..28a1ba30 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -37,7 +37,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), - new ("Get", @"/v2/Job/Types", GetV2JobTypes), + new ("GET", @"/v2/Job/Types", GetV2JobTypes), new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType), new ("GET", @"/v2/Job", GetV2Job), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), From 2f36701fefc5ce98422f81a15adf8e5ed7187671 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 21:25:24 +0200 Subject: [PATCH 55/95] Reduce Logspam --- Tranga/Server/Server.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 28a1ba30..416eb27c 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -165,7 +165,7 @@ private Dictionary GetRequestBody(HttpListenerRequest request) { if (!request.HasEntityBody) { - Log("No request body"); + //Nospam Log("No request body"); return new Dictionary(); } Stream body = request.InputStream; From 1ee9b644aa1e5634ecbb29070b137926f1042b1d Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 21:37:43 +0200 Subject: [PATCH 56/95] Fix Permissions for manga-directory --- Tranga/GlobalBase.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs index c4f78ce1..0843e327 100644 --- a/Tranga/GlobalBase.cs +++ b/Tranga/GlobalBase.cs @@ -74,12 +74,7 @@ protected void RemoveMangaFromCache(string internalId) internal void ImportManga() { string folder = settings.mangaCacheFolderPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - Directory.CreateDirectory(folder, - UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | - UnixFileMode.UserRead | UnixFileMode.UserWrite); - else - Directory.CreateDirectory(folder); + Directory.CreateDirectory(folder); foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles()) { @@ -100,12 +95,7 @@ internal void ImportManga() private void ExportManga() { string folder = settings.mangaCacheFolderPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - Directory.CreateDirectory(folder, - UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | - UnixFileMode.UserRead | UnixFileMode.UserWrite); - else - Directory.CreateDirectory(folder); + Directory.CreateDirectory(folder); foreach (Manga manga in cachedPublications.Values) { string content = JsonConvert.SerializeObject(manga, Formatting.Indented); From 6520aebcdf7466ad450db0baa7c73a2577a46a45 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 21:42:09 +0200 Subject: [PATCH 57/95] Cleanup MangaCache --- Tranga/Jobs/JobBoss.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs index 26ea8daa..c288c144 100644 --- a/Tranga/Jobs/JobBoss.cs +++ b/Tranga/Jobs/JobBoss.cs @@ -195,6 +195,9 @@ private void LoadJobsList(HashSet connectors) string[] coverFiles = Directory.GetFiles(settings.coverImageCache); foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName))) File.Delete(fileName); + string[] mangaFiles = Directory.GetFiles(settings.mangaCacheFolderPath); + foreach(string fileName in mangaFiles.Where(fileName => !GetAllCachedManga().Any(manga => fileName.Split('.')[0] == manga.internalId))) + File.Delete(fileName); } internal void UpdateJobFile(Job job, string? oldFile = null) From 960d3f7c62f9fa77e16012eb0ceaa31fdd1d279d Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 21:45:47 +0200 Subject: [PATCH 58/95] Fix Cover location --- Tranga/Server/v2Manga.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 44ff843a..502b88f1 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -76,7 +76,7 @@ public partial class Server !_parent.TryGetPublicationById(groups[1].Value, out Manga? manga) || manga is null) return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); - string filePath = settings.GetFullCoverPath((Manga)manga!); + string filePath = manga.Value.coverFileNameInCache!; if (File.Exists(filePath)) { FileStream coverStream = new(filePath, FileMode.Open); From fc884adc9fd3294335e9385db5eef21779036582 Mon Sep 17 00:00:00 2001 From: Glax Date: Sat, 10 Aug 2024 21:52:14 +0200 Subject: [PATCH 59/95] Fix HandleRequest trying to send more than one response --- Tranga/Server/Server.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 416eb27c..0f23f73c 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -114,9 +114,15 @@ private void HandleRequest(HttpListenerContext context) HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; if (request.HttpMethod == "OPTIONS") - SendResponse(HttpStatusCode.OK, context.Response); //Response always contains all valid Request-Methods + { + SendResponse(HttpStatusCode.NoContent, response);//Response always contains all valid Request-Methods + return; + } if (request.Url!.LocalPath.Contains("favicon")) + { SendResponse(HttpStatusCode.NoContent, response); + return; + } string path = Regex.Match(request.Url.LocalPath, @"\/[a-zA-Z0-9\.+/=-]+(\/[a-zA-Z0-9\.+/=-]+)*").Value; //Local Path if (!Regex.IsMatch(path, "/v2(/.*)?")) //Use only v2 API From 3c3f7bb95a9cf43a46b3963965109377e7e43d66 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 26 Aug 2024 19:08:59 +0200 Subject: [PATCH 60/95] Merge recent changes to TrangaSettings backend --- Tranga/GlobalBase.cs | 4 ++-- Tranga/Jobs/DownloadNewChapters.cs | 2 +- Tranga/Jobs/JobBoss.cs | 2 +- Tranga/Jobs/UpdateMetadata.cs | 2 +- Tranga/Server/Server.cs | 4 ++-- Tranga/Server/v2Jobs.cs | 2 +- Tranga/Server/v2Manga.cs | 2 +- Tranga/Server/v2Settings.cs | 25 ++++++++++++------------- Tranga/TrangaSettings.cs | 1 + 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs index 2fc3dbe2..ed5e9981 100644 --- a/Tranga/GlobalBase.cs +++ b/Tranga/GlobalBase.cs @@ -70,7 +70,7 @@ protected void RemoveMangaFromCache(string internalId) internal void ImportManga() { - string folder = settings.mangaCacheFolderPath; + string folder = TrangaSettings.mangaCacheFolderPath; Directory.CreateDirectory(folder); foreach (FileInfo fileInfo in new DirectoryInfo(folder).GetFiles()) @@ -91,7 +91,7 @@ internal void ImportManga() private void ExportManga() { - string folder = settings.mangaCacheFolderPath; + string folder = TrangaSettings.mangaCacheFolderPath; Directory.CreateDirectory(folder); foreach (Manga manga in cachedPublications.Values) { diff --git a/Tranga/Jobs/DownloadNewChapters.cs b/Tranga/Jobs/DownloadNewChapters.cs index da8c519a..bab79f13 100644 --- a/Tranga/Jobs/DownloadNewChapters.cs +++ b/Tranga/Jobs/DownloadNewChapters.cs @@ -38,7 +38,7 @@ protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBos Log($"Manga {mangaInternalId} is missing! Can not execute job."); return Array.Empty(); } - manga.Value.SaveSeriesInfoJson(settings.downloadLocation); + manga.Value.SaveSeriesInfoJson(); Chapter[] chapters = manga.Value.mangaConnector.GetNewChapters(manga.Value, this.translatedLanguage); this.progressToken.increments = chapters.Length; List jobs = new(); diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs index e0acf100..c997d598 100644 --- a/Tranga/Jobs/JobBoss.cs +++ b/Tranga/Jobs/JobBoss.cs @@ -195,7 +195,7 @@ private void LoadJobsList(HashSet connectors) string[] coverFiles = Directory.GetFiles(TrangaSettings.coverImageCache); foreach(string fileName in coverFiles.Where(fileName => !GetAllCachedManga().Any(manga => manga.coverFileNameInCache == fileName))) File.Delete(fileName); - string[] mangaFiles = Directory.GetFiles(settings.mangaCacheFolderPath); + string[] mangaFiles = Directory.GetFiles(TrangaSettings.mangaCacheFolderPath); foreach(string fileName in mangaFiles.Where(fileName => !GetAllCachedManga().Any(manga => fileName.Split('.')[0] == manga.internalId))) File.Delete(fileName); } diff --git a/Tranga/Jobs/UpdateMetadata.cs b/Tranga/Jobs/UpdateMetadata.cs index a468d958..8396e02e 100644 --- a/Tranga/Jobs/UpdateMetadata.cs +++ b/Tranga/Jobs/UpdateMetadata.cs @@ -42,7 +42,7 @@ protected override IEnumerable ExecuteReturnSubTasksInternal(JobBoss jobBos } AddMangaToCache(manga.Value.WithMetadata(updatedManga)); - this.manga.Value.SaveSeriesInfoJson(settings.downloadLocation, true); + this.manga.Value.SaveSeriesInfoJson(true); this.mangaConnector.CopyCoverFromCacheToDownloadLocation((Manga)manga); this.progressToken.Complete(); } diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 0f23f73c..2386e1e1 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -75,9 +75,9 @@ public Server(Tranga parent) : base(parent) this._parent = parent; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - this._listener.Prefixes.Add($"http://*:{settings.apiPortNumber}/"); + this._listener.Prefixes.Add($"http://*:{TrangaSettings.apiPortNumber}/"); else - this._listener.Prefixes.Add($"http://localhost:{settings.apiPortNumber}/"); + this._listener.Prefixes.Add($"http://localhost:{TrangaSettings.apiPortNumber}/"); Thread listenThread = new(Listen); listenThread.Start(); while(_parent.keepRunning && _running) diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index adf6fdbd..1146612e 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -61,7 +61,7 @@ public partial class Server return new ValueTuple(HttpStatusCode.InternalServerError, "'interval' Parameter missing, or is not in correct format."); requestParameters.TryGetValue("language", out string? language); if (requestParameters.TryGetValue("customFolder", out string? folder)) - manga.Value.MovePublicationFolder(settings.downloadLocation, folder); + manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, folder); if (requestParameters.TryGetValue("startChapter", out string? startChapterStr) && float.TryParse(startChapterStr, out float startChapter)) { diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 502b88f1..749e95eb 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -140,7 +140,7 @@ public partial class Server return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); if(!requestParameters.TryGetValue("location", out string? newFolder)) return new ValueTuple(HttpStatusCode.BadRequest, "Parameter 'location' missing."); - manga.Value.MovePublicationFolder(settings.downloadLocation, newFolder); + manga.Value.MovePublicationFolder(TrangaSettings.downloadLocation, newFolder); return new ValueTuple(HttpStatusCode.OK, null); } } \ No newline at end of file diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs index 5841ee2e..f6c1d2c2 100644 --- a/Tranga/Server/v2Settings.cs +++ b/Tranga/Server/v2Settings.cs @@ -8,24 +8,24 @@ public partial class Server { private ValueTuple GetV2Settings(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, settings); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.AsJObject()); } private ValueTuple GetV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, settings.userAgent); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.userAgent); } private ValueTuple PostV2SettingsUserAgent(GroupCollection groups, Dictionary requestParameters) { if (!requestParameters.TryGetValue("value", out string? userAgent)) { - settings.UpdateUserAgent(null); + TrangaSettings.UpdateUserAgent(null); return new ValueTuple(HttpStatusCode.Accepted, null); } else { - settings.UpdateUserAgent(userAgent); + TrangaSettings.UpdateUserAgent(userAgent); return new ValueTuple(HttpStatusCode.OK, null); } } @@ -37,7 +37,7 @@ public partial class Server private ValueTuple GetV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, settings.requestLimits); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.requestLimits); } private ValueTuple PostV2SettingsRateLimit(GroupCollection groups, Dictionary requestParameters) @@ -47,10 +47,9 @@ public partial class Server if(!Enum.TryParse(kv.Key, out RequestType requestType) || !int.TryParse(kv.Value, out int requestsPerMinute)) return new ValueTuple(HttpStatusCode.InternalServerError, null); - settings.requestLimits[requestType] = requestsPerMinute; - settings.ExportSettings(); + TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute); } - return new ValueTuple(HttpStatusCode.OK, settings.requestLimits); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.requestLimits); } private ValueTuple GetV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) @@ -58,7 +57,7 @@ public partial class Server if(groups.Count < 1 || !Enum.TryParse(groups[1].Value, out RequestType requestType)) return new ValueTuple(HttpStatusCode.NotFound, $"RequestType {groups[1].Value}"); - return new ValueTuple(HttpStatusCode.OK, settings.requestLimits[requestType]); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.requestLimits[requestType]); } private ValueTuple PostV2SettingsRateLimitType(GroupCollection groups, Dictionary requestParameters) @@ -69,13 +68,13 @@ public partial class Server if (!requestParameters.TryGetValue("value", out string? requestsPerMinuteStr) || !int.TryParse(requestsPerMinuteStr, out int requestsPerMinute)) return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing requestsPerMinute"); - settings.requestLimits[requestType] = requestsPerMinute; + TrangaSettings.UpdateRateLimit(requestType, requestsPerMinute); return new ValueTuple(HttpStatusCode.OK, null); } private ValueTuple GetV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, settings.aprilFoolsMode); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.aprilFoolsMode); } private ValueTuple PostV2SettingsAprilFoolsMode(GroupCollection groups, Dictionary requestParameters) @@ -83,7 +82,7 @@ public partial class Server if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || !bool.TryParse(trueFalseStr, out bool trueFalse)) return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); - settings.UpdateAprilFoolsMode(trueFalse); + TrangaSettings.UpdateAprilFoolsMode(trueFalse); return new ValueTuple(HttpStatusCode.OK, null); } @@ -98,7 +97,7 @@ public partial class Server false => true, true => bool.Parse(moveFilesStr!) }; - settings.UpdateDownloadLocation(folderPath, moveFiles); + TrangaSettings.UpdateDownloadLocation(folderPath, moveFiles); return new ValueTuple(HttpStatusCode.OK, null); } catch (FormatException) diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index b945fe58..54d4d930 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -20,6 +20,7 @@ public static class TrangaSettings [JsonIgnore] public static string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json"); [JsonIgnore] public static string jobsFolderPath => Path.Join(workingDirectory, "jobs"); [JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache"); + [JsonIgnore] public static string mangaCacheFolderPath => Path.Join(workingDirectory, "mangaCache"); public static ushort? version { get; } = 2; public static bool aprilFoolsMode { get; private set; } = true; [JsonIgnore]internal static readonly Dictionary DefaultRequestLimits = new () From e95eb0497cec02d5366d75f25a2b42c5a2c7fa5d Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 26 Aug 2024 19:34:29 +0200 Subject: [PATCH 61/95] #229 Resize cover Images if requested --- Tranga/Server/Server.cs | 46 +++++++++++++++------------------------- Tranga/Server/v2Manga.cs | 35 ++++++++++++++++++++++++++---- Tranga/Tranga.csproj | 1 + docs/API_Calls_v2.md | 4 ++++ 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 2386e1e1..dba7a57f 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -200,43 +200,31 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon response.AddHeader("Access-Control-Max-Age", "1728000"); response.AppendHeader("Access-Control-Allow-Origin", "*"); - if (content is not Stream) + + try { - response.ContentType = "application/json"; - try + if (content is Stream stream) { - response.OutputStream.Write(content is not null - ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) - : Array.Empty()); + response.ContentType = "image/jpeg"; + response.AddHeader("Cache-Control", "max-age=600"); + stream.CopyTo(response.OutputStream); response.OutputStream.Close(); + stream.Close(); } - catch (HttpListenerException e) + else { - Log(e.ToString()); + response.ContentType = "application/json"; + response.AddHeader("Cache-Control", "no-store"); + response.OutputStream.Write(content is not null + ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) + : Array.Empty()); } + + response.OutputStream.Close(); } - else if(content is FileStream stream) + catch (HttpListenerException e) { - string contentType = stream.Name.Split('.')[^1]; - switch (contentType.ToLower()) - { - case "gif": - response.ContentType = "image/gif"; - break; - case "png": - response.ContentType = "image/png"; - break; - case "jpg": - case "jpeg": - response.ContentType = "image/jpeg"; - break; - default: - response.ContentType = "text/plain"; - break; - } - stream.CopyTo(response.OutputStream); - response.OutputStream.Close(); - stream.Close(); + Log(e.ToString()); } } diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 749e95eb..2528cbaa 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -1,4 +1,6 @@ -using System.Net; +using System.Drawing; +using System.Drawing.Imaging; +using System.Net; using System.Text.RegularExpressions; using Tranga.Jobs; using Tranga.MangaConnectors; @@ -77,12 +79,37 @@ public partial class Server manga is null) return new ValueTuple(HttpStatusCode.NotFound, $"Manga with ID '{groups[1].Value} could not be found.'"); string filePath = manga.Value.coverFileNameInCache!; - if (File.Exists(filePath)) + if(!File.Exists(filePath)) + return new ValueTuple(HttpStatusCode.NotFound, "Cover-File not found."); + + Bitmap bitmap; + if (requestParameters.TryGetValue("dimensions", out string? dimensionsStr)) + { + Regex dimensionsRex = new(@"([0-9]+)x([0-9]+)"); + if(!dimensionsRex.IsMatch(dimensionsStr)) + return new ValueTuple(HttpStatusCode.BadRequest, "Requested dimensions not in required format."); + Match m = dimensionsRex.Match(dimensionsStr); + int width = int.Parse(m.Groups[1].Value); + int height = int.Parse(m.Groups[2].Value); + double aspectRequested = (double)width / (double)height; + + using Image coverImage = Image.FromFile(filePath); + double aspectCover = (double)coverImage.Width / (double)coverImage.Height; + + Size newSize = aspectRequested > aspectCover + ? new Size(width, (width / coverImage.Width) * coverImage.Height) + : new Size((height / coverImage.Height) * coverImage.Width, height); + + bitmap = new(coverImage, newSize); + } + else { FileStream coverStream = new(filePath, FileMode.Open); - return new ValueTuple(HttpStatusCode.OK, coverStream); + bitmap = new(coverStream); } - return new ValueTuple(HttpStatusCode.NotFound, "Cover-File not found."); + using MemoryStream ret = new(); + bitmap.Save(ret, ImageFormat.Jpeg); + return new ValueTuple(HttpStatusCode.OK, ret); } private ValueTuple GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary requestParameters) diff --git a/Tranga/Tranga.csproj b/Tranga/Tranga.csproj index 58cb6de1..cc66d5aa 100644 --- a/Tranga/Tranga.csproj +++ b/Tranga/Tranga.csproj @@ -13,6 +13,7 @@ + diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 5e32b618..4acbf83b 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -186,12 +186,16 @@ Returns the URL for the Cover of the specified Manga. * [GET /v2/Manga](#-v2manga) * [GET /v2/Connector/*ConnectorName*/GetManga](#-v2connectorconnectornamegetmanga) * [GET /v2/Job/*jobId*](#-v2jobjobid) + + Optional: `dimensions=x` replace width and height with requested dimensions in pixels. + Fitting will cover requested area.
Returns String with the url. +If `dimensions=x` was not requested, returns full sized image. | StatusCode | Meaning | |------------|--------------------------------------------| From 190fa8cba76f8e55efa48e72837fe0f559b64d17 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 9 Sep 2024 09:54:09 +0200 Subject: [PATCH 62/95] Fix #239 multiple enumeration on Export --- Tranga/GlobalBase.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Tranga/GlobalBase.cs b/Tranga/GlobalBase.cs index ed5e9981..4b36ed5f 100644 --- a/Tranga/GlobalBase.cs +++ b/Tranga/GlobalBase.cs @@ -89,11 +89,17 @@ internal void ImportManga() } + private static bool ExportRunning = false; private void ExportManga() { + while (ExportRunning) + Thread.Sleep(1); + ExportRunning = true; string folder = TrangaSettings.mangaCacheFolderPath; Directory.CreateDirectory(folder); - foreach (Manga manga in cachedPublications.Values) + Manga[] copy = new Manga[cachedPublications.Values.Count]; + cachedPublications.Values.CopyTo(copy, 0); + foreach (Manga manga in copy) { string content = JsonConvert.SerializeObject(manga, Formatting.Indented); string filePath = Path.Combine(folder, $"{manga.internalId}.json"); @@ -105,6 +111,8 @@ private void ExportManga() if(!cachedPublications.Keys.Any(key => fileInfo.Name.Substring(0, fileInfo.Name.LastIndexOf('.')).Equals(key))) fileInfo.Delete(); } + + ExportRunning = false; } protected void Log(string message) From 77bb309dfa9e71c282ab165115d6b2b200365cdf Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 16 Sep 2024 19:58:26 +0200 Subject: [PATCH 63/95] Fix #248 double closing OutputStream in response --- Tranga/Server/Server.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index dba7a57f..3011a59d 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -208,7 +208,6 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon response.ContentType = "image/jpeg"; response.AddHeader("Cache-Control", "max-age=600"); stream.CopyTo(response.OutputStream); - response.OutputStream.Close(); stream.Close(); } else From 99df9a9dfd838603e5b47016f29c1b58d6ce9be9 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 16 Sep 2024 20:10:38 +0200 Subject: [PATCH 64/95] Fix #248 Move contents of old DownloadLocation and WorkingDirectory to new paths. Overwrite existing files, and add from oldPath. --- Tranga/TrangaSettings.cs | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index 54d4d930..99925592 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -98,14 +98,13 @@ public static void UpdateAprilFoolsMode(bool enabled) public static void UpdateDownloadLocation(string newPath, bool moveFiles = true) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - Directory.CreateDirectory(newPath, - GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); + Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); else Directory.CreateDirectory(newPath); - if (moveFiles && Directory.Exists(TrangaSettings.downloadLocation)) - Directory.Move(TrangaSettings.downloadLocation, newPath); - + if (moveFiles) + MoveContentsOfDirectoryTo(TrangaSettings.downloadLocation, newPath); + TrangaSettings.downloadLocation = newPath; ExportSettings(); } @@ -113,15 +112,38 @@ public static void UpdateDownloadLocation(string newPath, bool moveFiles = true) public static void UpdateWorkingDirectory(string newPath) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - Directory.CreateDirectory(newPath, - GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); + Directory.CreateDirectory(newPath, GroupRead | GroupWrite | None | OtherRead | OtherWrite | UserRead | UserWrite); else Directory.CreateDirectory(newPath); - Directory.Move(TrangaSettings.workingDirectory, newPath); + + MoveContentsOfDirectoryTo(TrangaSettings.workingDirectory, newPath); + TrangaSettings.workingDirectory = newPath; ExportSettings(); } + private static void MoveContentsOfDirectoryTo(string oldDir, string newDir) + { + string[] directoryPaths = Directory.GetDirectories(oldDir); + string[] filePaths = Directory.GetFiles(oldDir); + foreach (string file in filePaths) + { + string newPath = Path.Join(newDir, Path.GetFileName(file)); + File.Move(file, newPath, true); + } + foreach(string directory in directoryPaths) + { + string? dirName = Path.GetDirectoryName(directory); + if(dirName is null) + continue; + string newPath = Path.Join(newDir, dirName); + if(Directory.Exists(newPath)) + MoveContentsOfDirectoryTo(directory, newPath); + else + Directory.Move(directory, newPath); + } + } + public static void UpdateUserAgent(string? customUserAgent) { TrangaSettings.userAgent = customUserAgent ?? DefaultUserAgent; From 18edcef1c3159013af86fab722e6721d64e0178a Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 16 Sep 2024 21:25:08 +0200 Subject: [PATCH 65/95] Resolve #247 Modify API call: `/v2/Connector/Types` Returns: Dictionary with Connector-Names and supported languages. --- Tranga/Server/v2Connector.cs | 2 +- Tranga/Tranga.cs | 4 ++-- docs/API_Calls_v2.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tranga/Server/v2Connector.cs b/Tranga/Server/v2Connector.cs index 29d45779..f84d039d 100644 --- a/Tranga/Server/v2Connector.cs +++ b/Tranga/Server/v2Connector.cs @@ -14,7 +14,7 @@ public partial class Server private ValueTuple GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary requestParameters) { if(groups.Count < 1 || - !_parent.GetConnectors().Contains(groups[1].Value) || + !_parent.GetConnectors().Keys.Contains(groups[1].Value) || !_parent.TryGetConnector(groups[1].Value, out MangaConnector? connector) || connector is null) return new ValueTuple(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist."); diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index 94933119..e6b6c3c4 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -49,9 +49,9 @@ public bool TryGetConnector(string name, out MangaConnector? connector) return connector is not null; } - public IEnumerable GetConnectors() + public Dictionary GetConnectors() { - return _connectors.Select(c => c.name); + return _connectors.ToDictionary(c => c.name, c => c.SupportedLanguages); } public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId); diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 4acbf83b..ee949ecd 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -34,7 +34,7 @@ Returns available Manga Connectors (Scanlation sites)
Returns - List of strings with Names. + Dictionary with Connector-Names and supported languages.
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector//GetManga` From d80fcd9039171460ac8e205d74d412d84b6ac517 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 30 Sep 2024 23:19:17 +0200 Subject: [PATCH 66/95] Manga website url nullable --- Tranga/Manga.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Manga.cs b/Tranga/Manga.cs index c03e403e..ea7298a4 100644 --- a/Tranga/Manga.cs +++ b/Tranga/Manga.cs @@ -72,7 +72,7 @@ public Manga(MangaConnector mangaConnector, string sortName, List author this.latestChapterDownloaded = 0; this.latestChapterAvailable = 0; this.releaseStatus = releaseStatus; - this.websiteUrl = websiteUrl; + this.websiteUrl = websiteUrl??""; } public Manga WithMetadata(Manga newManga) From 225db8bedabcfdf6db404d2634786904c5b25f9e Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 17 Oct 2024 21:03:37 +0200 Subject: [PATCH 67/95] Change return type of api request to get Connectors to get connector-list instead of dictionary --- Tranga/Server/v2Connector.cs | 2 +- Tranga/Tranga.cs | 4 ++-- docs/API_Calls_v2.md | 2 +- docs/Types.md | 26 ++++++++++++++++++-------- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Tranga/Server/v2Connector.cs b/Tranga/Server/v2Connector.cs index f84d039d..58fd3549 100644 --- a/Tranga/Server/v2Connector.cs +++ b/Tranga/Server/v2Connector.cs @@ -14,7 +14,7 @@ public partial class Server private ValueTuple GetV2ConnectorConnectorNameGetManga(GroupCollection groups, Dictionary requestParameters) { if(groups.Count < 1 || - !_parent.GetConnectors().Keys.Contains(groups[1].Value) || + !_parent.GetConnectors().Any(mangaConnector => mangaConnector.name == groups[1].Value)|| !_parent.TryGetConnector(groups[1].Value, out MangaConnector? connector) || connector is null) return new ValueTuple(HttpStatusCode.BadRequest, $"Connector '{groups[1].Value}' does not exist."); diff --git a/Tranga/Tranga.cs b/Tranga/Tranga.cs index 7c3271da..a463877b 100644 --- a/Tranga/Tranga.cs +++ b/Tranga/Tranga.cs @@ -50,9 +50,9 @@ public bool TryGetConnector(string name, out MangaConnector? connector) return connector is not null; } - public Dictionary GetConnectors() + public List GetConnectors() { - return _connectors.ToDictionary(c => c.name, c => c.SupportedLanguages); + return _connectors.ToList(); } public Manga? GetPublicationById(string internalId) => GetCachedManga(internalId); diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index ee949ecd..559acca0 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -34,7 +34,7 @@ Returns available Manga Connectors (Scanlation sites)
Returns - Dictionary with Connector-Names and supported languages. + List of [Connectors](Types.md#Connector)
### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Connector//GetManga` diff --git a/docs/Types.md b/docs/Types.md index 9a7ae3df..293cf1d2 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -1,41 +1,51 @@ -## Manga -```json +## Connector + +``` +{ + "name": string, + "SupportedLanguages": string[] +} +``` + +## Manga +``` { + } ``` ## Chapter -```json +``` { } ``` ## Job -```json +``` { } ``` ## ProgressToken -```json +``` { } ``` ## Settings -```json +``` { } ``` ## LibraryConnector -```json +``` { } ``` ## NotificationConnector -```json +``` { } ``` \ No newline at end of file From 6a4d454a08a567a9463d01838978182816531a3d Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 00:29:29 +0200 Subject: [PATCH 68/95] Extend Types.md documentation --- docs/Types.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/Types.md b/docs/Types.md index 293cf1d2..02fd79fe 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -10,7 +10,25 @@ ## Manga ``` { - + "sortName": string, + "authors": string[], + "altTitles": string[][], + "description": string, + "tags": string[], + "coverUrl": string, + "coverFileNameInCache": string, + "links": string[][], + "year": int, + "originalLanguage": string, + "releaseStatus": int, + "folderName": string, + "publicationId": string, + "internalId": string, + "ignoreChaptersBelow": decimal, + "latestChapterDownloaded": decimal, + "latestChapterAvailable": decimal, + "websiteUrl": string, + "mangaConnector": Connector } ``` From d4af068f0e240a4486affa5010a99f17fa18745c Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 00:48:46 +0200 Subject: [PATCH 69/95] Add BaseUris: string[] field to MangaConnector, to match Connector to uri --- Tranga/MangaConnectors/Bato.cs | 2 +- Tranga/MangaConnectors/MangaConnector.cs | 4 +++- Tranga/MangaConnectors/MangaDex.cs | 2 +- Tranga/MangaConnectors/MangaHere.cs | 2 +- Tranga/MangaConnectors/MangaKatana.cs | 2 +- Tranga/MangaConnectors/MangaLife.cs | 2 +- Tranga/MangaConnectors/Manganato.cs | 2 +- Tranga/MangaConnectors/Mangasee.cs | 2 +- Tranga/MangaConnectors/Mangaworld.cs | 2 +- Tranga/MangaConnectors/ManhuaPlus.cs | 2 +- docs/Types.md | 3 ++- 11 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Tranga/MangaConnectors/Bato.cs b/Tranga/MangaConnectors/Bato.cs index 8b21e888..161789f3 100644 --- a/Tranga/MangaConnectors/Bato.cs +++ b/Tranga/MangaConnectors/Bato.cs @@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors; public class Bato : MangaConnector { - public Bato(GlobalBase clone) : base(clone, "Bato", ["en"]) + public Bato(GlobalBase clone) : base(clone, "Bato", ["en"], ["bato.to"]) { this.downloadClient = new HttpDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/MangaConnector.cs b/Tranga/MangaConnectors/MangaConnector.cs index 0a4adc0c..34cd7173 100644 --- a/Tranga/MangaConnectors/MangaConnector.cs +++ b/Tranga/MangaConnectors/MangaConnector.cs @@ -15,11 +15,13 @@ public abstract class MangaConnector : GlobalBase { internal DownloadClient downloadClient { get; init; } = null!; public string[] SupportedLanguages; + public string[] BaseUris; - protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages) : base(clone) + protected MangaConnector(GlobalBase clone, string name, string[] supportedLanguages, string[] baseUris) : base(clone) { this.name = name; this.SupportedLanguages = supportedLanguages; + this.BaseUris = baseUris; Directory.CreateDirectory(TrangaSettings.coverImageCache); } diff --git a/Tranga/MangaConnectors/MangaDex.cs b/Tranga/MangaConnectors/MangaDex.cs index 9ea9d484..56982295 100644 --- a/Tranga/MangaConnectors/MangaDex.cs +++ b/Tranga/MangaConnectors/MangaDex.cs @@ -10,7 +10,7 @@ public class MangaDex : MangaConnector //https://api.mangadex.org/docs/3-enumerations/#language-codes--localization //https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes //https://gist.github.com/Josantonius/b455e315bc7f790d14b136d61d9ae469 - public MangaDex(GlobalBase clone) : base(clone, "MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"]) + public MangaDex(GlobalBase clone) : base(clone, "MangaDex", ["en","pt","pt-br","it","de","ru","aa","ab","ae","af","ak","am","an","ar-ae","ar-bh","ar-dz","ar-eg","ar-iq","ar-jo","ar-kw","ar-lb","ar-ly","ar-ma","ar-om","ar-qa","ar-sa","ar-sy","ar-tn","ar-ye","ar","as","av","ay","az","ba","be","bg","bh","bi","bm","bn","bo","br","bs","ca","ce","ch","co","cr","cs","cu","cv","cy","da","de-at","de-ch","de-de","de-li","de-lu","div","dv","dz","ee","el","en-au","en-bz","en-ca","en-cb","en-gb","en-ie","en-jm","en-nz","en-ph","en-tt","en-us","en-za","en-zw","eo","es-ar","es-bo","es-cl","es-co","es-cr","es-do","es-ec","es-es","es-gt","es-hn","es-la","es-mx","es-ni","es-pa","es-pe","es-pr","es-py","es-sv","es-us","es-uy","es-ve","es","et","eu","fa","ff","fi","fj","fo","fr-be","fr-ca","fr-ch","fr-fr","fr-lu","fr-mc","fr","fy","ga","gd","gl","gn","gu","gv","ha","he","hi","ho","hr-ba","hr-hr","hr","ht","hu","hy","hz","ia","id","ie","ig","ii","ik","in","io","is","it-ch","it-it","iu","iw","ja","ja-ro","ji","jv","jw","ka","kg","ki","kj","kk","kl","km","kn","ko","ko-ro","kr","ks","ku","kv","kw","ky","kz","la","lb","lg","li","ln","lo","ls","lt","lu","lv","mg","mh","mi","mk","ml","mn","mo","mr","ms-bn","ms-my","ms","mt","my","na","nb","nd","ne","ng","nl-be","nl-nl","nl","nn","no","nr","ns","nv","ny","oc","oj","om","or","os","pa","pi","pl","ps","pt-pt","qu-bo","qu-ec","qu-pe","qu","rm","rn","ro","rw","sa","sb","sc","sd","se-fi","se-no","se-se","se","sg","sh","si","sk","sl","sm","sn","so","sq","sr-ba","sr-sp","sr","ss","st","su","sv-fi","sv-se","sv","sw","sx","syr","ta","te","tg","th","ti","tk","tl","tn","to","tr","ts","tt","tw","ty","ug","uk","ur","us","uz","ve","vi","vo","wa","wo","xh","yi","yo","za","zh-cn","zh-hk","zh-mo","zh-ro","zh-sg","zh-tw","zh","zu"], ["mangadex.org"]) { this.downloadClient = new HttpDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/MangaHere.cs b/Tranga/MangaConnectors/MangaHere.cs index b68eb9c4..a680e3d0 100644 --- a/Tranga/MangaConnectors/MangaHere.cs +++ b/Tranga/MangaConnectors/MangaHere.cs @@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors; public class MangaHere : MangaConnector { - public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"]) + public MangaHere(GlobalBase clone) : base(clone, "MangaHere", ["en"], ["www.mangahere.cc"]) { this.downloadClient = new ChromiumDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/MangaKatana.cs b/Tranga/MangaConnectors/MangaKatana.cs index f3b8bb70..37d98057 100644 --- a/Tranga/MangaConnectors/MangaKatana.cs +++ b/Tranga/MangaConnectors/MangaKatana.cs @@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors; public class MangaKatana : MangaConnector { - public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"]) + public MangaKatana(GlobalBase clone) : base(clone, "MangaKatana", ["en"], ["mangakatana.com"]) { this.downloadClient = new HttpDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/MangaLife.cs b/Tranga/MangaConnectors/MangaLife.cs index 11f4c318..02ff605c 100644 --- a/Tranga/MangaConnectors/MangaLife.cs +++ b/Tranga/MangaConnectors/MangaLife.cs @@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors; public class MangaLife : MangaConnector { - public MangaLife(GlobalBase clone) : base(clone, "Manga4Life", ["en"]) + public MangaLife(GlobalBase clone) : base(clone, "Manga4Life", ["en"], ["manga4life.com"]) { this.downloadClient = new ChromiumDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/Manganato.cs b/Tranga/MangaConnectors/Manganato.cs index 1c040126..3ab24a4f 100644 --- a/Tranga/MangaConnectors/Manganato.cs +++ b/Tranga/MangaConnectors/Manganato.cs @@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors; public class Manganato : MangaConnector { - public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"]) + public Manganato(GlobalBase clone) : base(clone, "Manganato", ["en"], ["manganato.com"]) { this.downloadClient = new HttpDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/Mangasee.cs b/Tranga/MangaConnectors/Mangasee.cs index f7983c5f..fd5de869 100644 --- a/Tranga/MangaConnectors/Mangasee.cs +++ b/Tranga/MangaConnectors/Mangasee.cs @@ -11,7 +11,7 @@ namespace Tranga.MangaConnectors; public class Mangasee : MangaConnector { - public Mangasee(GlobalBase clone) : base(clone, "Mangasee", ["en"]) + public Mangasee(GlobalBase clone) : base(clone, "Mangasee", ["en"], ["mangasee123.com"]) { this.downloadClient = new ChromiumDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/Mangaworld.cs b/Tranga/MangaConnectors/Mangaworld.cs index b560c276..245e87c6 100644 --- a/Tranga/MangaConnectors/Mangaworld.cs +++ b/Tranga/MangaConnectors/Mangaworld.cs @@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors; public class Mangaworld: MangaConnector { - public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"]) + public Mangaworld(GlobalBase clone) : base(clone, "Mangaworld", ["it"], ["www.mangaworld.ac"]) { this.downloadClient = new HttpDownloadClient(clone); } diff --git a/Tranga/MangaConnectors/ManhuaPlus.cs b/Tranga/MangaConnectors/ManhuaPlus.cs index ffa31796..41ce7ce5 100644 --- a/Tranga/MangaConnectors/ManhuaPlus.cs +++ b/Tranga/MangaConnectors/ManhuaPlus.cs @@ -7,7 +7,7 @@ namespace Tranga.MangaConnectors; public class ManhuaPlus : MangaConnector { - public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"]) + public ManhuaPlus(GlobalBase clone) : base(clone, "ManhuaPlus", ["en"], ["manhuaplus.org"]) { this.downloadClient = new ChromiumDownloadClient(clone); } diff --git a/docs/Types.md b/docs/Types.md index 02fd79fe..d3626891 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -3,7 +3,8 @@ ``` { "name": string, - "SupportedLanguages": string[] + "SupportedLanguages": string[], + "BaseUris": stirng[] } ``` From 575fb739cc01b331fdd27d9ef12a3bfc2f4fcc64 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 00:48:58 +0200 Subject: [PATCH 70/95] typo --- docs/Types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Types.md b/docs/Types.md index d3626891..360a8c69 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -4,7 +4,7 @@ { "name": string, "SupportedLanguages": string[], - "BaseUris": stirng[] + "BaseUris": string[] } ``` From 2c9bd2532e43bd69c07abf93b8fd1a37a353698c Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 17:51:37 +0200 Subject: [PATCH 71/95] Fix order of RequestPaths --- Tranga/Server/Server.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 3011a59d..6a0e0570 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -40,11 +40,11 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Job/Types", GetV2JobTypes), new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType), new ("GET", @"/v2/Job", GetV2Job), - new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), - new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Progress", GetV2JobJobIdProgress), new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/StartNow", PostV2JobJobIdStartNow), new ("POST", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)/Cancel", PostV2JobJobIdCancel), + new ("GET", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", GetV2JobJobId), + new ("DELETE", @"/v2/Job/([a-zA-Z\.]+-[-A-Za-z0-9+/]*={0,3}(?:-[0-9]+)?)", DeleteV2JobJobId), new ("GET", @"/v2/Settings", GetV2Settings), new ("GET", @"/v2/Settings/UserAgent", GetV2SettingsUserAgent), new ("POST", @"/v2/Settings/UserAgent", PostV2SettingsUserAgent), From 8670863810e10dd85bc75f6973fb7a28aa22c1bc Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 17:51:53 +0200 Subject: [PATCH 72/95] Add Job and ProgressToken Types to docs --- docs/API_Calls_v2.md | 8 ++++---- docs/Types.md | 25 ++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 559acca0..3f167c5a 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -305,7 +305,7 @@ Returns the latest Chapter of the specified Manga. ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs` -Returns all configured Jobs. +Returns all configured Jobs as IDs.
Returns @@ -315,7 +315,7 @@ Returns all configured Jobs. ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Running` -Returns all Running Jobs. +Returns all Running Jobs as IDs.
Returns @@ -325,7 +325,7 @@ Returns all Running Jobs. ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Waiting` -Returns all Waiting Jobs. +Returns all Waiting Jobs as IDs.
Returns @@ -335,7 +335,7 @@ Returns all Waiting Jobs. ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` -Returns all Monitoring Jobs. +Returns all Monitoring Jobs as IDs.
Returns diff --git a/docs/Types.md b/docs/Types.md index 360a8c69..8ae0c0cd 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -25,9 +25,9 @@ "folderName": string, "publicationId": string, "internalId": string, - "ignoreChaptersBelow": decimal, - "latestChapterDownloaded": decimal, - "latestChapterAvailable": decimal, + "ignoreChaptersBelow": number, + "latestChapterDownloaded": number, + "latestChapterAvailable": number, "websiteUrl": string, "mangaConnector": Connector } @@ -42,12 +42,31 @@ ## Job ``` { + jobType: number, + mangaInternalId: string, + translatedLanguage: string, + progressToken: ProgressToken, + recurring: boolean, + recurrenceTime: string, + lastExecution: Date, + nextExecution: Date, + id: string, + parentJobId: string | null, + mangaConnector: Connector } ``` ## ProgressToken ``` { + cancellationRequested: boolean, + increments: number, + incrementsCompleted: number, + progress: number, + lastUpdate: Date, + executionStarted: Date, + timeRemaining: Date, + state: number } ``` From 00c4f0533fd53be7c9ff26c8048acdd75792cb52 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 17:57:59 +0200 Subject: [PATCH 73/95] Update documentation --- docs/API_Calls_v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 3f167c5a..e5048e24 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -536,7 +536,7 @@ Cancels the specified Job, or dequeues it. | 404 | Manga with `jobId` could not be found |
-### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//SetInterval` +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Job//SetInterval` NOT YET IMPLEMENTED Edits the specified Job. From 1a631362c9a69193ec12919c770e148bdd60a461 Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 19:30:57 +0200 Subject: [PATCH 74/95] Use Sixlabors.Imagesharp for resizing coverimages. --- README.md | 1 + Tranga/Server/Server.cs | 15 +++++++++++---- Tranga/Server/v2Manga.cs | 26 +++++++++----------------- Tranga/Tranga.csproj | 1 + 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 011927cd..f10ff848 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ That is why I wanted to create my own project, in a language I understand, and t - [PuppeteerSharp](https://www.puppeteersharp.com/) - [Html Agility Pack (HAP)](https://html-agility-pack.net/) - [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch) +- [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license) - 💙 Blåhaj 🦈

(back to top)

diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 6a0e0570..818a6c91 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -3,6 +3,8 @@ using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; namespace Tranga.Server; @@ -26,13 +28,13 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Mangas", GetV2Mangas), new ("GET", @"/v2/Manga/Search", GetV2MangaSearch), new ("GET", @"/v2/Manga", GetV2Manga), - new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), - new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Cover", GetV2MangaInternalIdCover), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters", GetV2MangaInternalIdChapters), new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/Chapters/Latest", GetV2MangaInternalIdChaptersLatest), new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/ignoreChaptersBelow", PostV2MangaInternalIdIgnoreChaptersBelow), new ("POST", @"/v2/Manga/([-A-Za-z0-9]*={0,3})/moveFolder", PostV2MangaInternalIdMoveFolder), + new ("GET", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", GetV2MangaInternalId), + new ("DELETE", @"/v2/Manga/([-A-Za-z0-9]*={0,3})", DeleteV2MangaInternalId), new ("GET", @"/v2/Jobs", GetV2Jobs), new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), @@ -205,10 +207,15 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon { if (content is Stream stream) { - response.ContentType = "image/jpeg"; - response.AddHeader("Cache-Control", "max-age=600"); + response.ContentType = "text/plain"; stream.CopyTo(response.OutputStream); stream.Close(); + }else if (content is Image image) + { + response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType; + response.AddHeader("Cache-Control", "max-age=600"); + image.Save(response.OutputStream, image.Metadata.DecodedImageFormat ?? PngFormat.Instance); + image.Dispose(); } else { diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 2528cbaa..9753907d 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -1,5 +1,5 @@ -using System.Drawing; -using System.Drawing.Imaging; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; using System.Net; using System.Text.RegularExpressions; using Tranga.Jobs; @@ -82,7 +82,7 @@ public partial class Server if(!File.Exists(filePath)) return new ValueTuple(HttpStatusCode.NotFound, "Cover-File not found."); - Bitmap bitmap; + Image image = Image.Load(filePath); if (requestParameters.TryGetValue("dimensions", out string? dimensionsStr)) { Regex dimensionsRex = new(@"([0-9]+)x([0-9]+)"); @@ -93,23 +93,15 @@ public partial class Server int height = int.Parse(m.Groups[2].Value); double aspectRequested = (double)width / (double)height; - using Image coverImage = Image.FromFile(filePath); - double aspectCover = (double)coverImage.Width / (double)coverImage.Height; + double aspectCover = (double)image.Width / (double)image.Height; Size newSize = aspectRequested > aspectCover - ? new Size(width, (width / coverImage.Width) * coverImage.Height) - : new Size((height / coverImage.Height) * coverImage.Width, height); - - bitmap = new(coverImage, newSize); - } - else - { - FileStream coverStream = new(filePath, FileMode.Open); - bitmap = new(coverStream); + ? new Size(width, (width / image.Width) * image.Height) + : new Size((height / image.Height) * image.Width, height); + + image.Mutate(x => x.Resize(newSize)); } - using MemoryStream ret = new(); - bitmap.Save(ret, ImageFormat.Jpeg); - return new ValueTuple(HttpStatusCode.OK, ret); + return new ValueTuple(HttpStatusCode.OK, image); } private ValueTuple GetV2MangaInternalIdChapters(GroupCollection groups, Dictionary requestParameters) diff --git a/Tranga/Tranga.csproj b/Tranga/Tranga.csproj index 17a31335..70fa5ef6 100644 --- a/Tranga/Tranga.csproj +++ b/Tranga/Tranga.csproj @@ -13,6 +13,7 @@ + From 70993a692a1d1446cab6574ac99806063b2ecb8c Mon Sep 17 00:00:00 2001 From: Glax Date: Fri, 18 Oct 2024 19:31:09 +0200 Subject: [PATCH 75/95] Add ReleaseStatus to docs/types.md --- docs/Types.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/Types.md b/docs/Types.md index 8ae0c0cd..c04bb43a 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -21,7 +21,7 @@ "links": string[][], "year": int, "originalLanguage": string, - "releaseStatus": int, + "releaseStatus": ReleaseStatus, "folderName": string, "publicationId": string, "internalId": string, @@ -39,6 +39,17 @@ } ``` +## ReleaseStatus +``` +{ + Continuing = 0, + Completed = 1, + OnHiatus = 2, + Cancelled = 3, + Unreleased = 4 +} +``` + ## Job ``` { From 27f823cfeb762fd4bfe5e9a78b327cc269e0f811 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 01:06:24 +0200 Subject: [PATCH 76/95] GET V2Manga with internalIds return distinct array. --- Tranga/Server/v2Manga.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 9753907d..80da384d 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -18,7 +18,7 @@ public partial class Server { if(!requestParameters.TryGetValue("mangaIds", out string? mangaIdListStr)) return new ValueTuple(HttpStatusCode.BadRequest, "Missing parameter 'mangaIds'."); - string[] mangaIdList = mangaIdListStr.Split(','); + string[] mangaIdList = mangaIdListStr.Split(',').Distinct().ToArray(); List ret = new(); foreach (string mangaId in mangaIdList) { From a88b85e599cfd6fbc7e20d385fc0322ea143df03 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 01:08:22 +0200 Subject: [PATCH 77/95] Add numbers to JobTypes (and type documentation) --- Tranga/Jobs/Job.cs | 2 +- docs/Types.md | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Tranga/Jobs/Job.cs b/Tranga/Jobs/Job.cs index 71dbc24b..ac48b391 100644 --- a/Tranga/Jobs/Job.cs +++ b/Tranga/Jobs/Job.cs @@ -12,7 +12,7 @@ public abstract class Job : GlobalBase public string id => GetId(); internal IEnumerable? subJobs { get; private set; } public string? parentJobId { get; init; } - public enum JobType : byte { DownloadChapterJob, DownloadNewChaptersJob, UpdateMetaDataJob, MonitorManga } + public enum JobType : byte { DownloadChapterJob = 0, DownloadNewChaptersJob = 1, UpdateMetaDataJob = 2, MonitorManga = 3 } public MangaConnector mangaConnector => GetMangaConnector(); diff --git a/docs/Types.md b/docs/Types.md index c04bb43a..3e2c45ba 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -21,7 +21,7 @@ "links": string[][], "year": int, "originalLanguage": string, - "releaseStatus": ReleaseStatus, + "releaseStatus": ReleaseStatus, see ReleaseStatus "folderName": string, "publicationId": string, "internalId": string, @@ -53,7 +53,7 @@ ## Job ``` { - jobType: number, + jobType: number, see JobType mangaInternalId: string, translatedLanguage: string, progressToken: ProgressToken, @@ -67,6 +67,16 @@ } ``` +## JobType +``` +{ + DownloadChapterJob = 0, + DownloadNewChaptersJob = 1, + UpdateMetaDataJob = 2, + MonitorManga = 3 +} +``` + ## ProgressToken ``` { From 26b291000080f7846eae2524a2276e108e559267 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 01:30:50 +0200 Subject: [PATCH 78/95] Add GET /v2/Jobs/Standby --- Tranga/Server/Server.cs | 1 + Tranga/Server/v2Jobs.cs | 7 +++++++ docs/API_Calls_v2.md | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 818a6c91..acc56447 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -39,6 +39,7 @@ public Server(Tranga parent) : base(parent) new ("GET", @"/v2/Jobs/Running", GetV2JobsRunning), new ("GET", @"/v2/Jobs/Waiting", GetV2JobsWaiting), new ("GET", @"/v2/Jobs/Monitoring", GetV2JobsMonitoring), + new ("GET", @"/v2/Jobs/Standby", GetV2JobsStandby), new ("GET", @"/v2/Job/Types", GetV2JobTypes), new ("POST", @"/v2/Job/Create/([a-zA-Z]+)", PostV2JobCreateType), new ("GET", @"/v2/Job", GetV2Job), diff --git a/Tranga/Server/v2Jobs.cs b/Tranga/Server/v2Jobs.cs index 1146612e..a0eb9b4a 100644 --- a/Tranga/Server/v2Jobs.cs +++ b/Tranga/Server/v2Jobs.cs @@ -26,6 +26,13 @@ public partial class Server .Select(job => job.id)); } + private ValueTuple GetV2JobsStandby(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs + .Where(job => job.progressToken.state is ProgressToken.State.Standby) + .Select(job => job.id)); + } + private ValueTuple GetV2JobsMonitoring(GroupCollection groups, Dictionary requestParameters) { return new ValueTuple(HttpStatusCode.OK, _parent.jobBoss.jobs diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index e5048e24..bdd3cdeb 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -333,6 +333,16 @@ Returns all Waiting Jobs as IDs. List of JobIds.
+### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Standby` + +Returns all Standby Jobs as IDs. + +
+ Returns + +List of JobIds. +
+ ### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Jobs/Monitoring` Returns all Monitoring Jobs as IDs. From 240af81fa912989152a8e5de1a006f5a64f5fcfb Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 02:16:22 +0200 Subject: [PATCH 79/95] Add doc types chapter --- docs/Types.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/Types.md b/docs/Types.md index 3e2c45ba..3cd40e7c 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -36,6 +36,12 @@ ## Chapter ``` { + parentManga: IManga, + name: string | undefined, + volumeNumber: string, + chapterNumber: string, + url: string, + fileName: string } ``` @@ -53,17 +59,18 @@ ## Job ``` { - jobType: number, see JobType - mangaInternalId: string, - translatedLanguage: string, - progressToken: ProgressToken, + progressToken: IProgressToken, recurring: boolean, recurrenceTime: string, lastExecution: Date, nextExecution: Date, id: string, + jobType: number, //see JobType parentJobId: string | null, - mangaConnector: Connector + mangaConnector: IMangaConnector, + mangaInternalId: string | undefined, //only on DownloadNewChapters + translatedLanguage: string | undefined, //only on DownloadNewChapters + chapter: IChapter | undefined, //only on DownloadChapter } ``` From f9a30f2587e6e636265c7c7aad917c19e2ee2eb9 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 18:32:49 +0200 Subject: [PATCH 80/95] Types documentation add quotation marks --- docs/Types.md | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/Types.md b/docs/Types.md index 3cd40e7c..8f0379de 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -36,12 +36,12 @@ ## Chapter ``` { - parentManga: IManga, - name: string | undefined, - volumeNumber: string, - chapterNumber: string, - url: string, - fileName: string + "parentManga": IManga, + "name": string | undefined, + "volumeNumber": string, + "chapterNumber": string, + "url": string, + "fileName": string } ``` @@ -59,18 +59,18 @@ ## Job ``` { - progressToken: IProgressToken, - recurring: boolean, - recurrenceTime: string, - lastExecution: Date, - nextExecution: Date, - id: string, - jobType: number, //see JobType - parentJobId: string | null, - mangaConnector: IMangaConnector, - mangaInternalId: string | undefined, //only on DownloadNewChapters - translatedLanguage: string | undefined, //only on DownloadNewChapters - chapter: IChapter | undefined, //only on DownloadChapter + "progressToken": IProgressToken, + "recurring": boolean, + "recurrenceTime": string, + "lastExecution": Date, + "nextExecution": Date, + "id": string, + "jobType": number, //see JobType + "parentJobId": string | null, + "mangaConnector": IMangaConnector, + "mangaInternalId": string | undefined, //only on DownloadNewChapters + "translatedLanguage": string | undefined, //only on DownloadNewChapters + "chapter": IChapter | undefined, //only on DownloadChapter } ``` @@ -87,14 +87,14 @@ ## ProgressToken ``` { - cancellationRequested: boolean, - increments: number, - incrementsCompleted: number, - progress: number, - lastUpdate: Date, - executionStarted: Date, - timeRemaining: Date, - state: number + "cancellationRequested": boolean, + "increments": number, + "incrementsCompleted": number, + "progress": number, + "lastUpdate": Date, + "executionStarted": Date, + "timeRemaining": Date, + "state": number } ``` From f57667bc8fafc7677915ca9427b469f513f6730c Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 18:32:57 +0200 Subject: [PATCH 81/95] add documentation types settings --- docs/Types.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/Types.md b/docs/Types.md index 8f0379de..42f0850b 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -101,6 +101,22 @@ ## Settings ``` { + "downloadLocation": string, + "workingDirectory": string, + "apiPortNumber": number, + "userAgent": string, + "bufferLibraryUpdates": boolean, + "bufferNotifications": boolean, + "version": number, + "aprilFoolsMode": boolean, + "requestLimits": { + "MangaInfo": number, + "MangaDexFeed": number, + "MangaDexImage": number, + "MangaImage": number, + "MangaCover": number, + "Default": number + } } ``` From 582b3af89cac7074ff5e42bb70a66dd871af529b Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 18:53:03 +0200 Subject: [PATCH 82/95] Add docs types LibraryConnector --- docs/Types.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/Types.md b/docs/Types.md index 42f0850b..b3e7517a 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -123,6 +123,17 @@ ## LibraryConnector ``` { + "libraryType": number, //see LibraryType + "baseUrl": string, + "auth": string +} +``` + +## LibraryType +``` +{ + Komga = 0, + Kavita = 1 } ``` From 93696fbac1a238948486c114d1ddecc3fedab2e0 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 20:43:40 +0200 Subject: [PATCH 83/95] docs Types documentation NotificationConnector --- docs/Types.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/Types.md b/docs/Types.md index b3e7517a..34717092 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -140,5 +140,20 @@ ## NotificationConnector ``` { + "notificationConnectorType": number, //see NotificationConnectorType + "endpoint": string, //only on Ntfy, Gotify + "appToken": string, //only on Gotify + "auth": string, //only on Ntfy + "topic": string, //only on Ntfy + "id": string, //only on LunaSea +} +``` + +## NotificationConnectorType +``` +{ + Gotify = 0, + LunaSea = 1, + Ntfy = 2 } ``` \ No newline at end of file From 9d47445339efe0806688104c5a573ab32d7cbf9d Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 20 Oct 2024 20:58:59 +0200 Subject: [PATCH 84/95] Assign numbers to ProgressToken.State Update type docs --- Tranga/Jobs/ProgressToken.cs | 2 +- docs/Types.md | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Tranga/Jobs/ProgressToken.cs b/Tranga/Jobs/ProgressToken.cs index e718c7da..3823ac2b 100644 --- a/Tranga/Jobs/ProgressToken.cs +++ b/Tranga/Jobs/ProgressToken.cs @@ -10,7 +10,7 @@ public class ProgressToken public DateTime executionStarted { get; private set; } public TimeSpan timeRemaining => GetTimeRemaining(); - public enum State { Running, Complete, Standby, Cancelled, Waiting } + public enum State : byte { Running = 0, Complete = 1, Standby = 2, Cancelled = 3, Waiting = 4 } public State state { get; private set; } public ProgressToken(int increments) diff --git a/docs/Types.md b/docs/Types.md index 34717092..bfc3cb52 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -45,7 +45,7 @@ } ``` -## ReleaseStatus +### ReleaseStatus ``` { Continuing = 0, @@ -74,7 +74,7 @@ } ``` -## JobType +### JobType ``` { DownloadChapterJob = 0, @@ -94,7 +94,18 @@ "lastUpdate": Date, "executionStarted": Date, "timeRemaining": Date, - "state": number + "state": number //see ProgressState +} +``` + +### ProgressState +``` +{ + Running = 0, + Complete = 1, + Standby = 2, + Cancelled = 3, + Waiting = 4 } ``` @@ -129,7 +140,7 @@ } ``` -## LibraryType +### LibraryType ``` { Komga = 0, @@ -149,7 +160,7 @@ } ``` -## NotificationConnectorType +### NotificationConnectorType ``` { Gotify = 0, From 96b5921ed6bce2853aed4fa647c44e82150aac44 Mon Sep 17 00:00:00 2001 From: Glax Date: Wed, 23 Oct 2024 02:29:08 +0200 Subject: [PATCH 85/95] GET LibraryTypes Create and Test set url to lowercase Set Komga to also require username and password --- Tranga/Server/v2LibraryConnectors.cs | 28 ++++++++---------- docs/API_Calls_v2.md | 44 +++++++--------------------- 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/Tranga/Server/v2LibraryConnectors.cs b/Tranga/Server/v2LibraryConnectors.cs index f989edf8..85c2ee07 100644 --- a/Tranga/Server/v2LibraryConnectors.cs +++ b/Tranga/Server/v2LibraryConnectors.cs @@ -39,24 +39,22 @@ public partial class Server return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist."); } - if(!requestParameters.TryGetValue("URL", out string? url)) + if(!requestParameters.TryGetValue("url", out string? url)) return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("username", out string? username)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); + if(!requestParameters.TryGetValue("password", out string? password)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); switch (libraryType) { case LibraryConnector.LibraryType.Kavita: - if(!requestParameters.TryGetValue("username", out string? username)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); - if(!requestParameters.TryGetValue("password", out string? password)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); Kavita kavita = new (this, url, username, password); libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Kavita); libraryConnectors.Add(kavita); return new ValueTuple(HttpStatusCode.OK, kavita); case LibraryConnector.LibraryType.Komga: - if(!requestParameters.TryGetValue("auth", out string? auth)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); - Komga komga = new (this, url, auth); + Komga komga = new (this, url, username, password); libraryConnectors.RemoveWhere(lc => lc.libraryType == LibraryConnector.LibraryType.Komga); libraryConnectors.Add(komga); return new ValueTuple(HttpStatusCode.OK, komga); @@ -72,16 +70,16 @@ public partial class Server return new ValueTuple(HttpStatusCode.NotFound, $"LibraryType {groups[1].Value} does not exist."); } - if(!requestParameters.TryGetValue("URL", out string? url)) + if(!requestParameters.TryGetValue("url", out string? url)) return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'url' missing."); + if(!requestParameters.TryGetValue("username", out string? username)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); + if(!requestParameters.TryGetValue("password", out string? password)) + return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); switch (libraryType) { case LibraryConnector.LibraryType.Kavita: - if(!requestParameters.TryGetValue("username", out string? username)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'username' missing."); - if(!requestParameters.TryGetValue("password", out string? password)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'password' missing."); Kavita kavita = new (this, url, username, password); return kavita.Test() switch { @@ -89,9 +87,7 @@ public partial class Server _ => new ValueTuple(HttpStatusCode.FailedDependency, kavita) }; case LibraryConnector.LibraryType.Komga: - if(!requestParameters.TryGetValue("auth", out string? auth)) - return new ValueTuple(HttpStatusCode.NotAcceptable, "Parameter 'auth' missing."); - Komga komga = new (this, url, auth); + Komga komga = new (this, url, username, password); return komga.Test() switch { true => new ValueTuple(HttpStatusCode.OK, komga), diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index bdd3cdeb..5f4f77ff 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -803,23 +803,11 @@ Creates a Library-Connector of the specified Type. `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) - | Parameter | Value | - |-------------|--------------------| - | URL | URL of the Library | - - #### Type specific Parameters (must be included for each) - * Komga - - | Parameter | Value | - |-----------|-------------------------------------------------------------------------------------------------------------------| - | auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | - - * Kavita - - | Parameter | Value | - |-----------|-----------------| - | username | Kavita Username | - | password | Kavita Password | + | Parameter | Value | + |-----------|--------------------| + | url | URL of the Library | + | username | Username | + | password | Password |
@@ -843,23 +831,11 @@ Tests a Library-Connector of the specified Type. `Type` is returned by [GET /v2/LibraryConnector/Types](#-v2libraryconnectortypes) - | Parameter | Value | - |-------------|--------------------| - | URL | URL of the Library | - - #### Type specific Parameters (must be included for each) - * Komga - - | Parameter | Value | - |-----------|-------------------------------------------------------------------------------------------------------------------| - | auth | [Base64 encoded Basic-Authentication-String](https://datatracker.ietf.org/doc/html/rfc7617) (`username:password`) | - - * Kavita - - | Parameter | Value | - |-----------|-----------------| - | username | Kavita Username | - | password | Kavita Password | + | Parameter | Value | + |-----------|--------------------| + | url | URL of the Library | + | username | Username | + | password | Password |
From fb7ed21d82a3387d9575f93a9561d8b9c563c68d Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 27 Oct 2024 03:39:40 +0100 Subject: [PATCH 86/95] Update Types doc with last merge for Chapters --- docs/Types.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Types.md b/docs/Types.md index bfc3cb52..8ca9d051 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -41,7 +41,8 @@ "volumeNumber": string, "chapterNumber": string, "url": string, - "fileName": string + "fileName": string, + "id": string? } ``` From febce6b92a5a1ab1ba8e937dc6990eb97808e067 Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 27 Oct 2024 03:40:07 +0100 Subject: [PATCH 87/95] Downloaded Image processing: - Compression - B/W threshold --- Tranga/MangaConnectors/MangaConnector.cs | 27 ++++++++++- Tranga/Server/Server.cs | 4 ++ Tranga/Server/v2Settings.cs | 28 +++++++++++ Tranga/TrangaSettings.cs | 26 +++++++++- docs/API_Calls_v2.md | 61 ++++++++++++++++++++++++ docs/Types.md | 2 + 6 files changed, 145 insertions(+), 3 deletions(-) diff --git a/Tranga/MangaConnectors/MangaConnector.cs b/Tranga/MangaConnectors/MangaConnector.cs index 607b7a60..20b8472c 100644 --- a/Tranga/MangaConnectors/MangaConnector.cs +++ b/Tranga/MangaConnectors/MangaConnector.cs @@ -2,6 +2,10 @@ using System.Net; using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Binarization; using Tranga.Jobs; using static System.IO.UnixFileMode; @@ -216,6 +220,22 @@ private HttpStatusCode DownloadImage(string imageUrl, string fullPath, RequestTy return requestResult.statusCode; } + private void ProcessImage(string imagePath) + { + if (!TrangaSettings.bwImages && !TrangaSettings.compressImages) + return; + DateTime start = DateTime.Now; + using Image image = Image.Load(imagePath); + File.Delete(imagePath); + if(TrangaSettings.bwImages) + image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor())); + image.SaveAsJpeg(imagePath, new JpegEncoder() + { + Quality = TrangaSettings.compressImages ? 30 : 75 + }); + Log($"Image processing took {DateTime.Now.Subtract(start):s\\.fff} B/W:{TrangaSettings.bwImages} Compress:{TrangaSettings.compressImages}"); + } + protected HttpStatusCode DownloadChapterImages(string[] imageUrls, Chapter chapter, RequestType requestType, string? referrer = null, ProgressToken? progressToken = null) { string saveArchiveFilePath = chapter.GetArchiveFilePath(); @@ -254,11 +274,14 @@ protected HttpStatusCode DownloadChapterImages(string[] imageUrls, Chapter chapt progressToken?.Complete(); return HttpStatusCode.NoContent; } + foreach (string imageUrl in imageUrls) { string extension = imageUrl.Split('.')[^1].Split('?')[0]; - Log($"Downloading image {chapterNum + 1:000}/{imageUrls.Length:000}"); //TODO - HttpStatusCode status = DownloadImage(imageUrl, Path.Join(tempFolder, $"{chapterNum++}.{extension}"), requestType, referrer); + Log($"Downloading image {chapterNum + 1:000}/{imageUrls.Length:000}"); + string imagePath = Path.Join(tempFolder, $"{chapterNum++}.{extension}"); + HttpStatusCode status = DownloadImage(imageUrl, imagePath, requestType, referrer); + ProcessImage(imagePath); Log($"{saveArchiveFilePath} {chapterNum + 1:000}/{imageUrls.Length:000} {status}"); if ((int)status < 200 || (int)status >= 300) { diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index acc56447..68e326c9 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -58,6 +58,10 @@ public Server(Tranga parent) : base(parent) new ("POST", @"/v2/Settings/RateLimit/([a-zA-Z]+)", PostV2SettingsRateLimitType), new ("GET", @"/v2/Settings/AprilFoolsMode", GetV2SettingsAprilFoolsMode), new ("POST", @"/v2/Settings/AprilFoolsMode", PostV2SettingsAprilFoolsMode), + new ("GET", @"/v2/Settings/CompressImages", GetV2SettingsCompressImages), + new ("POST", @"/v2/Settings/CompressImages", PostV2SettingsCompressImages), + new ("GET", @"/v2/Settings/BWImages", GetV2SettingsBwImages), + new ("POST", @"/v2/Settings/BWImages", PostV2SettingsBwImages), new ("POST", @"/v2/Settings/DownloadLocation", PostV2SettingsDownloadLocation), new ("GET", @"/v2/LibraryConnector", GetV2LibraryConnector), new ("GET", @"/v2/LibraryConnector/Types", GetV2LibraryConnectorTypes), diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs index f6c1d2c2..045312fb 100644 --- a/Tranga/Server/v2Settings.cs +++ b/Tranga/Server/v2Settings.cs @@ -86,6 +86,34 @@ public partial class Server return new ValueTuple(HttpStatusCode.OK, null); } + private ValueTuple GetV2SettingsCompressImages(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.compressImages); + } + + private ValueTuple PostV2SettingsCompressImages(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || + !bool.TryParse(trueFalseStr, out bool trueFalse)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); + TrangaSettings.UpdateCompressImages(trueFalse); + return new ValueTuple(HttpStatusCode.OK, null); + } + + private ValueTuple GetV2SettingsBwImages(GroupCollection groups, Dictionary requestParameters) + { + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.bwImages); + } + + private ValueTuple PostV2SettingsBwImages(GroupCollection groups, Dictionary requestParameters) + { + if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || + !bool.TryParse(trueFalseStr, out bool trueFalse)) + return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); + TrangaSettings.UpdateBwImages(trueFalse); + return new ValueTuple(HttpStatusCode.OK, null); + } + private ValueTuple PostV2SettingsDownloadLocation(GroupCollection groups, Dictionary requestParameters) { if (!requestParameters.TryGetValue("location", out string? folderPath)) diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index bf4ee96c..16919c87 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -17,6 +17,8 @@ public static class TrangaSettings public static string userAgent { get; private set; } = DefaultUserAgent; public static bool bufferLibraryUpdates { get; private set; } = false; public static bool bufferNotifications { get; private set; } = false; + public static bool compressImages { get; private set; } = true; + public static bool bwImages { get; private set; } = false; [JsonIgnore] public static string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore] public static string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); [JsonIgnore] public static string notificationConnectorsFilePath => Path.Join(workingDirectory, "notificationConnectors.json"); @@ -49,7 +51,9 @@ public static void LoadFromWorkingDirectory(string directory) ExportSettings(); } - public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null) + public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, + int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, + bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null, bool? pCompressImages = null, bool? pbwImages = null) { if(pWorkingDirectory is null && File.Exists(settingsFilePath)) LoadFromWorkingDirectory(workingDirectory); @@ -60,6 +64,8 @@ public static void CreateOrUpdate(string? downloadDirectory = null, string? pWor aprilFoolsMode = pAprilFoolsMode ?? aprilFoolsMode; bufferLibraryUpdates = pBufferLibraryUpdates ?? bufferLibraryUpdates; bufferNotifications = pBufferNotifications ?? bufferNotifications; + compressImages = pCompressImages ?? compressImages; + bwImages = pbwImages ?? bwImages; Directory.CreateDirectory(downloadLocation); Directory.CreateDirectory(workingDirectory); ExportSettings(); @@ -99,6 +105,18 @@ public static void UpdateAprilFoolsMode(bool enabled) ExportSettings(); } + public static void UpdateCompressImages(bool enabled) + { + compressImages = enabled; + ExportSettings(); + } + + public static void UpdateBwImages(bool enabled) + { + bwImages = enabled; + ExportSettings(); + } + public static void UpdateDownloadLocation(string newPath, bool moveFiles = true) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -190,6 +208,8 @@ public static JObject AsJObject() jobj.Add("requestLimits", JToken.FromObject(requestLimits)); jobj.Add("bufferLibraryUpdates", JToken.FromObject(bufferLibraryUpdates)); jobj.Add("bufferNotifications", JToken.FromObject(bufferNotifications)); + jobj.Add("compressImages", JToken.FromObject(compressImages)); + jobj.Add("bwImages", JToken.FromObject(bwImages)); return jobj; } @@ -214,5 +234,9 @@ public static void Deserialize(string serialized) bufferLibraryUpdates = blu.Value()!; if (jobj.TryGetValue("bufferNotifications", out JToken? bn)) bufferNotifications = bn.Value()!; + if (jobj.TryGetValue("compressImages", out JToken? ci)) + compressImages = ci.Value()!; + if (jobj.TryGetValue("bwImages", out JToken? bwi)) + bwImages = bwi.Value()!; } } \ No newline at end of file diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 5f4f77ff..34bbce0b 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -728,6 +728,67 @@ Enables/Disables April-Fools-Mode. | 500 | Parsing Error |
+### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/CompressImages` + +Returns the current state of the Image-compression setting. + +
+ Returns + + Boolean +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/CompressImages` + +Enables/Disables Imagecompression. + +
+ Request + + | Parameter | Value | + |-----------|------------| + | value | true/false | +
+ +
+ Returns + + | StatusCode | Meaning | + |------------|--------------------------------| + | 500 | Parsing Error | +
+ +### ![GET](https://img.shields.io/badge/GET-0f0) `/v2/Settings/BWImages` + +Returns the current state of the Black/White Image setting. + +
+ Returns + +Boolean +
+ +### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/BWImages` + +Enables/Disables converting Images to Black and White. + +
+ Request + + | Parameter | Value | + |-----------|------------| + | value | true/false | +
+ +
+ Returns + +| StatusCode | Meaning | + |------------|--------------------------------| +| 500 | Parsing Error | +
+ + ### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/DownloadLocation` Updates the default Download-Location. diff --git a/docs/Types.md b/docs/Types.md index 8ca9d051..5543fed2 100644 --- a/docs/Types.md +++ b/docs/Types.md @@ -121,6 +121,8 @@ "bufferNotifications": boolean, "version": number, "aprilFoolsMode": boolean, + "compressImages": boolean, + "bwImages": boolean, "requestLimits": { "MangaInfo": number, "MangaDexFeed": number, From 585d7e338015dc76a014b9887ebda2117487f6ec Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 27 Oct 2024 03:42:50 +0100 Subject: [PATCH 88/95] Fix order of startup: Load Manga first, the jobs --- Tranga/Jobs/JobBoss.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tranga/Jobs/JobBoss.cs b/Tranga/Jobs/JobBoss.cs index 025baf39..8c4542dc 100644 --- a/Tranga/Jobs/JobBoss.cs +++ b/Tranga/Jobs/JobBoss.cs @@ -146,6 +146,9 @@ private void LoadJobsList(HashSet connectors) return; } Regex idRex = new (@"(.*)\.json"); + + //Load Manga-Files + ImportManga(); //Load json-job-files foreach (FileInfo file in new DirectoryInfo(TrangaSettings.jobsFolderPath).EnumerateFiles().Where(fileInfo => idRex.IsMatch(fileInfo.Name))) @@ -166,9 +169,6 @@ private void LoadJobsList(HashSet connectors) UpdateJobFile(job, file.Name); } } - - //Load Manga-Files - ImportManga(); //Connect jobs to parent-jobs and add Publications to cache foreach (Job job in this.jobs) From 07c6081c03300baa85f7ff50a2ed16905bbb18ce Mon Sep 17 00:00:00 2001 From: Glax Date: Sun, 27 Oct 2024 03:49:55 +0100 Subject: [PATCH 89/95] #236 --- Tranga/MangaConnectors/MangaConnector.cs | 6 +++--- Tranga/Server/v2Settings.cs | 9 +++++---- Tranga/TrangaSettings.cs | 16 ++++++++-------- docs/API_Calls_v2.md | 10 +++++----- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Tranga/MangaConnectors/MangaConnector.cs b/Tranga/MangaConnectors/MangaConnector.cs index 20b8472c..f87e4bf1 100644 --- a/Tranga/MangaConnectors/MangaConnector.cs +++ b/Tranga/MangaConnectors/MangaConnector.cs @@ -222,7 +222,7 @@ private HttpStatusCode DownloadImage(string imageUrl, string fullPath, RequestTy private void ProcessImage(string imagePath) { - if (!TrangaSettings.bwImages && !TrangaSettings.compressImages) + if (!TrangaSettings.bwImages && TrangaSettings.compression == 100) return; DateTime start = DateTime.Now; using Image image = Image.Load(imagePath); @@ -231,9 +231,9 @@ private void ProcessImage(string imagePath) image.Mutate(i => i.ApplyProcessor(new AdaptiveThresholdProcessor())); image.SaveAsJpeg(imagePath, new JpegEncoder() { - Quality = TrangaSettings.compressImages ? 30 : 75 + Quality = TrangaSettings.compression }); - Log($"Image processing took {DateTime.Now.Subtract(start):s\\.fff} B/W:{TrangaSettings.bwImages} Compress:{TrangaSettings.compressImages}"); + Log($"Image processing took {DateTime.Now.Subtract(start):s\\.fff} B/W:{TrangaSettings.bwImages} Compression: {TrangaSettings.compression}"); } protected HttpStatusCode DownloadChapterImages(string[] imageUrls, Chapter chapter, RequestType requestType, string? referrer = null, ProgressToken? progressToken = null) diff --git a/Tranga/Server/v2Settings.cs b/Tranga/Server/v2Settings.cs index 045312fb..4e0212ec 100644 --- a/Tranga/Server/v2Settings.cs +++ b/Tranga/Server/v2Settings.cs @@ -88,15 +88,16 @@ public partial class Server private ValueTuple GetV2SettingsCompressImages(GroupCollection groups, Dictionary requestParameters) { - return new ValueTuple(HttpStatusCode.OK, TrangaSettings.compressImages); + return new ValueTuple(HttpStatusCode.OK, TrangaSettings.compression); } private ValueTuple PostV2SettingsCompressImages(GroupCollection groups, Dictionary requestParameters) { - if (!requestParameters.TryGetValue("value", out string? trueFalseStr) || - !bool.TryParse(trueFalseStr, out bool trueFalse)) + if (!requestParameters.TryGetValue("value", out string? valueStr) || + !int.TryParse(valueStr, out int value) + || value != int.Clamp(value, 1, 100)) return new ValueTuple(HttpStatusCode.InternalServerError, "Errors parsing 'value'"); - TrangaSettings.UpdateCompressImages(trueFalse); + TrangaSettings.UpdateCompressImages(value); return new ValueTuple(HttpStatusCode.OK, null); } diff --git a/Tranga/TrangaSettings.cs b/Tranga/TrangaSettings.cs index 16919c87..dfa15902 100644 --- a/Tranga/TrangaSettings.cs +++ b/Tranga/TrangaSettings.cs @@ -17,7 +17,7 @@ public static class TrangaSettings public static string userAgent { get; private set; } = DefaultUserAgent; public static bool bufferLibraryUpdates { get; private set; } = false; public static bool bufferNotifications { get; private set; } = false; - public static bool compressImages { get; private set; } = true; + public static int compression{ get; private set; } = 40; public static bool bwImages { get; private set; } = false; [JsonIgnore] public static string settingsFilePath => Path.Join(workingDirectory, "settings.json"); [JsonIgnore] public static string libraryConnectorsFilePath => Path.Join(workingDirectory, "libraryConnectors.json"); @@ -53,7 +53,7 @@ public static void LoadFromWorkingDirectory(string directory) public static void CreateOrUpdate(string? downloadDirectory = null, string? pWorkingDirectory = null, int? pApiPortNumber = null, string? pUserAgent = null, bool? pAprilFoolsMode = null, - bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null, bool? pCompressImages = null, bool? pbwImages = null) + bool? pBufferLibraryUpdates = null, bool? pBufferNotifications = null, int? pCompression = null, bool? pbwImages = null) { if(pWorkingDirectory is null && File.Exists(settingsFilePath)) LoadFromWorkingDirectory(workingDirectory); @@ -64,7 +64,7 @@ public static void CreateOrUpdate(string? downloadDirectory = null, string? pWor aprilFoolsMode = pAprilFoolsMode ?? aprilFoolsMode; bufferLibraryUpdates = pBufferLibraryUpdates ?? bufferLibraryUpdates; bufferNotifications = pBufferNotifications ?? bufferNotifications; - compressImages = pCompressImages ?? compressImages; + compression = pCompression ?? compression; bwImages = pbwImages ?? bwImages; Directory.CreateDirectory(downloadLocation); Directory.CreateDirectory(workingDirectory); @@ -105,9 +105,9 @@ public static void UpdateAprilFoolsMode(bool enabled) ExportSettings(); } - public static void UpdateCompressImages(bool enabled) + public static void UpdateCompressImages(int value) { - compressImages = enabled; + compression = int.Clamp(value, 1, 100); ExportSettings(); } @@ -208,7 +208,7 @@ public static JObject AsJObject() jobj.Add("requestLimits", JToken.FromObject(requestLimits)); jobj.Add("bufferLibraryUpdates", JToken.FromObject(bufferLibraryUpdates)); jobj.Add("bufferNotifications", JToken.FromObject(bufferNotifications)); - jobj.Add("compressImages", JToken.FromObject(compressImages)); + jobj.Add("compression", JToken.FromObject(compression)); jobj.Add("bwImages", JToken.FromObject(bwImages)); return jobj; } @@ -234,8 +234,8 @@ public static void Deserialize(string serialized) bufferLibraryUpdates = blu.Value()!; if (jobj.TryGetValue("bufferNotifications", out JToken? bn)) bufferNotifications = bn.Value()!; - if (jobj.TryGetValue("compressImages", out JToken? ci)) - compressImages = ci.Value()!; + if (jobj.TryGetValue("compression", out JToken? ci)) + compression = ci.Value()!; if (jobj.TryGetValue("bwImages", out JToken? bwi)) bwImages = bwi.Value()!; } diff --git a/docs/API_Calls_v2.md b/docs/API_Calls_v2.md index 34bbce0b..e47bcd4e 100644 --- a/docs/API_Calls_v2.md +++ b/docs/API_Calls_v2.md @@ -735,19 +735,19 @@ Returns the current state of the Image-compression setting.
Returns - Boolean + number
### ![POST](https://img.shields.io/badge/POST-00f) `/v2/Settings/CompressImages` -Enables/Disables Imagecompression. +Set the quality of the compression.
Request - | Parameter | Value | - |-----------|------------| - | value | true/false | + | Parameter | Value | + |-----------|-------| + | value | 1-100 |
From f7daacf0d4d056e148ced4c8e8e6250d7de49f8a Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 31 Oct 2024 21:50:46 +0100 Subject: [PATCH 90/95] Use Robidoux algorithm for resizing covers --- Tranga/Server/v2Manga.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index 80da384d..b40150fa 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -2,6 +2,7 @@ using SixLabors.ImageSharp.Processing; using System.Net; using System.Text.RegularExpressions; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using Tranga.Jobs; using Tranga.MangaConnectors; @@ -99,7 +100,7 @@ public partial class Server ? new Size(width, (width / image.Width) * image.Height) : new Size((height / image.Height) * image.Width, height); - image.Mutate(x => x.Resize(newSize)); + image.Mutate(x => x.Resize(newSize, CubicResampler.Robidoux, true)); } return new ValueTuple(HttpStatusCode.OK, image); } From b7bc04a0453ca409ae7ba50f051748be40b251e4 Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 31 Oct 2024 22:16:18 +0100 Subject: [PATCH 91/95] Add zstd compression to all API Traffic --- README.md | 1 + Tranga/Server/Server.cs | 21 +++++++++++++-------- Tranga/Tranga.csproj | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f10ff848..fe0010b4 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ That is why I wanted to create my own project, in a language I understand, and t - [Html Agility Pack (HAP)](https://html-agility-pack.net/) - [Soenneker.Utils.String.NeedlemanWunsch](https://github.com/soenneker/soenneker.utils.string.needlemanwunsch) - [Sixlabors.ImageSharp](https://docs-v2.sixlabors.com/articles/imagesharp/index.html#license) +- [zstd-wrapper](https://github.com/oleg-st/ZstdSharp) [zstd](https://github.com/facebook/zstd) - 💙 Blåhaj 🦈

(back to top)

diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 68e326c9..37fc5a4f 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; +using ZstdSharp; namespace Tranga.Server; @@ -205,32 +206,36 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE"); response.AddHeader("Access-Control-Max-Age", "1728000"); - response.AppendHeader("Access-Control-Allow-Origin", "*"); - - + response.AddHeader("Access-Control-Allow-Origin", "*"); + response.AddHeader("Content-Encoding", "zstd"); + + using CompressionStream compressor = new (response.OutputStream, 5); try { if (content is Stream stream) { response.ContentType = "text/plain"; - stream.CopyTo(response.OutputStream); + response.AddHeader("Cache-Control", "no-store"); + stream.CopyTo(compressor); stream.Close(); }else if (content is Image image) { response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType; response.AddHeader("Cache-Control", "max-age=600"); - image.Save(response.OutputStream, image.Metadata.DecodedImageFormat ?? PngFormat.Instance); + image.Save(compressor, image.Metadata.DecodedImageFormat ?? PngFormat.Instance); image.Dispose(); } else { response.ContentType = "application/json"; response.AddHeader("Cache-Control", "no-store"); - response.OutputStream.Write(content is not null - ? Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)) - : Array.Empty()); + if(content is not null) + new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))).CopyTo(compressor); + else + compressor.Write(Array.Empty()); } + compressor.Flush(); response.OutputStream.Close(); } catch (HttpListenerException e) diff --git a/Tranga/Tranga.csproj b/Tranga/Tranga.csproj index 70fa5ef6..10fb726e 100644 --- a/Tranga/Tranga.csproj +++ b/Tranga/Tranga.csproj @@ -16,6 +16,7 @@ + From 3f37eefe72f9858a36338eca91933971b7165ea7 Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 31 Oct 2024 22:53:05 +0100 Subject: [PATCH 92/95] Include modified date in image responses for cachecontrol --- Tranga/Server/Server.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 37fc5a4f..3c82afdb 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -5,6 +5,8 @@ using Newtonsoft.Json; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using ZstdSharp; namespace Tranga.Server; @@ -215,20 +217,33 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon if (content is Stream stream) { response.ContentType = "text/plain"; - response.AddHeader("Cache-Control", "no-store"); + response.AddHeader("Cache-Control", "private, no-store"); stream.CopyTo(compressor); stream.Close(); }else if (content is Image image) { response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType; - response.AddHeader("Cache-Control", "max-age=600"); + response.AddHeader("Cache-Control", "public, max-age=3600"); + string lastModifiedStr = ""; + if (image.Metadata.IptcProfile is not null) + { + DateTime date = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedDate).First().Value, "yyyyMMdd",null); + DateTime time = DateTime.ParseExact(image.Metadata.IptcProfile.GetValues(IptcTag.CreatedTime).First().Value, "HHmmssK",null); + lastModifiedStr = $"{date:ddd\\,\\ dd\\ MMM\\ yyyy} {time:HH\\:mm\\:ss} GMT"; + }else if (image.Metadata.ExifProfile is not null) + { + DateTime datetime = DateTime.ParseExact(image.Metadata.ExifProfile.Values.FirstOrDefault(value => value.Tag == ExifTag.DateTime)?.ToString() ?? "2000:01:01 01:01:01", "yyyy:MM:dd HH:mm:ss", null); + lastModifiedStr = $"{datetime:ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT"; + } + if(lastModifiedStr.Length>0) + response.AddHeader("Last-Modified", lastModifiedStr); image.Save(compressor, image.Metadata.DecodedImageFormat ?? PngFormat.Instance); image.Dispose(); } else { response.ContentType = "application/json"; - response.AddHeader("Cache-Control", "no-store"); + response.AddHeader("Cache-Control", "private, no-store"); if(content is not null) new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content))).CopyTo(compressor); else From c7dc5e75f2e405b235ea45554c766d2f27a9c429 Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 31 Oct 2024 23:00:33 +0100 Subject: [PATCH 93/95] Add "Expires" Header to image responses --- Tranga/Server/Server.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Tranga/Server/Server.cs b/Tranga/Server/Server.cs index 3c82afdb..b7ce0f87 100644 --- a/Tranga/Server/Server.cs +++ b/Tranga/Server/Server.cs @@ -224,6 +224,7 @@ private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse respon { response.ContentType = image.Metadata.DecodedImageFormat?.DefaultMimeType ?? PngFormat.Instance.DefaultMimeType; response.AddHeader("Cache-Control", "public, max-age=3600"); + response.AddHeader("Expires", $"{DateTime.Now.AddHours(1):ddd\\,\\ dd\\ MMM\\ yyyy\\ HH\\:mm\\:ss} GMT"); string lastModifiedStr = ""; if (image.Metadata.IptcProfile is not null) { From 1bd914571cd7b5d4e4f1118c0b2f54b28f62c959 Mon Sep 17 00:00:00 2001 From: Glax Date: Mon, 11 Nov 2024 17:09:19 +0100 Subject: [PATCH 94/95] Asuratoon Server-V2 --- Tranga/MangaConnectors/AsuraToon.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tranga/MangaConnectors/AsuraToon.cs b/Tranga/MangaConnectors/AsuraToon.cs index bf3bdaa1..c2f1c8b7 100644 --- a/Tranga/MangaConnectors/AsuraToon.cs +++ b/Tranga/MangaConnectors/AsuraToon.cs @@ -8,7 +8,7 @@ namespace Tranga.MangaConnectors; public class AsuraToon : MangaConnector { - public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"]) + public AsuraToon(GlobalBase clone) : base(clone, "AsuraToon", ["en"], ["asuracomic.net"]) { this.downloadClient = new HttpDownloadClient(clone); } @@ -111,7 +111,7 @@ private Manga ParseSinglePublicationFromHtml(HtmlDocument document, string publi HtmlNode? firstChapterNode = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'chapter/1')]/../following-sibling::h3"); int? year = int.Parse(firstChapterNode?.InnerText.Split(' ')[^1] ?? "2000"); - Manga manga = new (sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links, + Manga manga = new (this, sortName, authors, description, altTitles, tags, coverUrl, coverFileNameInCache, links, year, originalLanguage, publicationId, releaseStatus, websiteUrl); AddMangaToCache(manga); return manga; From afcc2cacaf81440da47b42fcd9daf827bfc76e85 Mon Sep 17 00:00:00 2001 From: Glax Date: Thu, 12 Dec 2024 22:48:46 +0100 Subject: [PATCH 95/95] merge --- Tranga/Server/v2Manga.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tranga/Server/v2Manga.cs b/Tranga/Server/v2Manga.cs index b40150fa..cad38d67 100644 --- a/Tranga/Server/v2Manga.cs +++ b/Tranga/Server/v2Manga.cs @@ -129,8 +129,8 @@ public partial class Server float latest = requestParameters.TryGetValue("language", out string? parameter) switch { - true => float.Parse(manga.Value.mangaConnector.GetChapters(manga.Value, parameter).Max().chapterNumber), - false => float.Parse(manga.Value.mangaConnector.GetChapters(manga.Value).Max().chapterNumber) + true => manga.Value.mangaConnector.GetChapters(manga.Value, parameter).Max().chapterNumber, + false => manga.Value.mangaConnector.GetChapters(manga.Value).Max().chapterNumber }; return new ValueTuple(HttpStatusCode.OK, latest); }