diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 279c89b07..914d89e23 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - Lunox-Scans paths: - 'plugins/**' - 'public/**' @@ -11,7 +12,7 @@ on: - '.github/workflows/publish-plugins.yml' permissions: - contents: read + contents: write concurrency: group: ${{ github.workflow }} @@ -23,29 +24,26 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@v4 with: - token: ${{ secrets.REPO_SCOPED_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Fetch All Remote Branches + run: git fetch origin '+refs/heads/*:refs/remotes/origin/*' - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@v4 with: node-version: '24' - name: Install Dependencies - run: npm ci --omit=dev --ignore-scripts + run: npm ci --ignore-scripts - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - - name: Publish Plugins (Main Repository) - if: github.repository == 'LNReader/lnreader-plugins' - run: npm run publish:plugins 2>> $GITHUB_STEP_SUMMARY - shell: bash - - - name: Publish Plugins (Fork - All Branches) - if: github.repository != 'LNReader/lnreader-plugins' + - name: Publish Plugins run: npm run publish:plugins -- --all-branches shell: bash diff --git a/plugins/multisrc/madara/filters/lunoxscans.json b/plugins/multisrc/madara/filters/lunoxscans.json new file mode 100644 index 000000000..0eefe5dd5 --- /dev/null +++ b/plugins/multisrc/madara/filters/lunoxscans.json @@ -0,0 +1,264 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chaebol", + "value": "chaebol" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Growth", + "value": "growth" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Possession", + "value": "possession" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Reverse", + "value": "reverse" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Royalty", + "value": "royalty" + }, + { + "label": "s", + "value": "s" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Warrior", + "value": "warrior" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "x", + "value": "x" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/sources.json b/plugins/multisrc/madara/sources.json index 6f4b088e4..8f5823838 100644 --- a/plugins/multisrc/madara/sources.json +++ b/plugins/multisrc/madara/sources.json @@ -651,5 +651,15 @@ "useNewChapterEndpoint": true, "versionIncrements": 2 } + }, + { + "id": "lunoxscans", + "sourceSite": "https://lunoxscans.com/", + "sourceName": "LunoxScans", + "options": { + "lang": "English", + "useNewChapterEndpoint": false, + "versionIncrements": 2 + } } ] diff --git a/plugins/multisrc/ranobes/template.ts b/plugins/multisrc/ranobes/template.ts index 13d2613f8..8e725fc1e 100644 --- a/plugins/multisrc/ranobes/template.ts +++ b/plugins/multisrc/ranobes/template.ts @@ -28,12 +28,59 @@ export class RanobesPlugin implements Plugin.PluginBase { this.name = metadata.sourceName; this.icon = 'multisrc/ranobes/ranobes/icon.png'; this.site = metadata.sourceSite; - this.version = '2.0.2'; + this.version = '2.0.3'; this.options = metadata.options as RanobesOptions; } + private static requestQueue: Promise = Promise.resolve(); + private static lastRequestTime = 0; + private static readonly MIN_DELAY_MS = 1200; - async safeFecth(url: string, init?: FetchInit): Promise { + private async throttle(): Promise { + const previous = RanobesPlugin.requestQueue; + let release!: () => void; + RanobesPlugin.requestQueue = new Promise(res => { + release = res; + }); + try { + await previous; + const now = Date.now(); + const elapsed = now - RanobesPlugin.lastRequestTime; + const remaining = RanobesPlugin.MIN_DELAY_MS - elapsed; + if (remaining > 0) { + await new Promise(res => setTimeout(res, remaining)); + } + RanobesPlugin.lastRequestTime = Date.now(); + } finally { + release(); + } + } + + async safeFecth(url: string, init?: FetchInit, retries = 3): Promise { + await this.throttle(); const r = await fetchApi(url, init); + + if (r.status === 429) { + if (retries <= 0) + throw new Error('Rate limited (429), too many retries.'); + + const retryAfter = r.headers.get('Retry-After'); + let waitMs = 3000; // safe default + if (retryAfter) { + if (/^\d+$/.test(retryAfter.trim())) { + // delta-seconds format + waitMs = parseInt(retryAfter, 10) * 1000; + } else { + // HTTP-date format + const dateMs = Date.parse(retryAfter); + if (!isNaN(dateMs)) { + waitMs = Math.max(0, dateMs - Date.now()); + } + } + } + await new Promise(res => setTimeout(res, waitMs)); + return this.safeFecth(url, init, retries - 1); + } + if (!r.ok) throw new Error( 'Could not reach site (' + r.status + ') try to open in webview.', diff --git a/public/static/multisrc/madara/lunoxscans/icon.png b/public/static/multisrc/madara/lunoxscans/icon.png new file mode 100644 index 000000000..0dcfd325c Binary files /dev/null and b/public/static/multisrc/madara/lunoxscans/icon.png differ