mirror of
https://github.com/agregarr/agregarr.git
synced 2026-05-01 15:39:25 -05:00
chore(release): merge develop into latest
This commit is contained in:
@@ -6,14 +6,11 @@ on:
|
||||
- latest
|
||||
|
||||
jobs:
|
||||
semantic-release:
|
||||
name: Tag and release latest version
|
||||
version-check:
|
||||
name: Check for new version
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
packages: write
|
||||
outputs:
|
||||
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
|
||||
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
|
||||
@@ -31,17 +28,19 @@ jobs:
|
||||
env:
|
||||
HUSKY: 0
|
||||
run: yarn
|
||||
- name: Release
|
||||
- name: Check version (dry-run)
|
||||
id: semantic
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
with:
|
||||
dry_run: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build_and_push:
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: semantic-release
|
||||
if: needs.semantic-release.outputs.new_release_published == 'true'
|
||||
needs: version-check
|
||||
if: needs.version-check.outputs.new_release_published == 'true'
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
@@ -56,6 +55,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update version in package.json
|
||||
run: npm version ${{ needs.version-check.outputs.new_release_version }} --no-git-tag-version
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -95,7 +97,7 @@ jobs:
|
||||
name: Create multi-platform manifest
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- semantic-release
|
||||
- version-check
|
||||
- build_and_push
|
||||
permissions:
|
||||
packages: write
|
||||
@@ -129,7 +131,7 @@ jobs:
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ghcr.io/agregarr/agregarr:latest \
|
||||
-t ghcr.io/agregarr/agregarr:v${{ needs.semantic-release.outputs.new_release_version }} \
|
||||
-t ghcr.io/agregarr/agregarr:v${{ needs.version-check.outputs.new_release_version }} \
|
||||
$(printf 'ghcr.io/agregarr/agregarr@sha256:%s ' *)
|
||||
|
||||
- name: Install regctl
|
||||
@@ -140,45 +142,62 @@ jobs:
|
||||
- name: Copy to Docker Hub
|
||||
run: |
|
||||
/tmp/regctl image copy ghcr.io/agregarr/agregarr:latest agregarr/agregarr:latest
|
||||
/tmp/regctl image copy ghcr.io/agregarr/agregarr:v${{ needs.semantic-release.outputs.new_release_version }} agregarr/agregarr:v${{ needs.semantic-release.outputs.new_release_version }}
|
||||
/tmp/regctl image copy ghcr.io/agregarr/agregarr:v${{ needs.version-check.outputs.new_release_version }} agregarr/agregarr:v${{ needs.version-check.outputs.new_release_version }}
|
||||
|
||||
- name: Inspect images
|
||||
run: |
|
||||
docker buildx imagetools inspect ghcr.io/agregarr/agregarr:latest
|
||||
docker buildx imagetools inspect agregarr/agregarr:latest
|
||||
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
release:
|
||||
name: Create release
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- semantic-release
|
||||
- version-check
|
||||
- merge
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: v${{ needs.semantic-release.outputs.new_release_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get release notes from tag
|
||||
id: notes
|
||||
run: |
|
||||
# Get the release notes from CHANGELOG.md for this version
|
||||
VERSION="v${{ needs.semantic-release.outputs.new_release_version }}"
|
||||
echo "Creating release for ${VERSION}"
|
||||
|
||||
# Extract notes between this version and the previous one
|
||||
NOTES=$(awk "/^## \[${VERSION#v}\]/{flag=1; next} /^## \[/{flag=0} flag" CHANGELOG.md)
|
||||
|
||||
# Write to file for gh release
|
||||
echo "$NOTES" > /tmp/release-notes.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
env:
|
||||
HUSKY: 0
|
||||
run: yarn
|
||||
- name: Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
discord:
|
||||
name: Discord Notification
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- version-check
|
||||
- release
|
||||
steps:
|
||||
- name: Send Discord Notification
|
||||
run: |
|
||||
gh release create "v${{ needs.semantic-release.outputs.new_release_version }}" \
|
||||
--title "v${{ needs.semantic-release.outputs.new_release_version }}" \
|
||||
--notes-file /tmp/release-notes.md
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"content": "@everyone",
|
||||
"embeds": [{
|
||||
"title": "🎉 Agregarr v${{ needs.version-check.outputs.new_release_version }} Released!",
|
||||
"description": "A new version of Agregarr is now available!\n\n**[📝 View Release Notes](${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ needs.version-check.outputs.new_release_version }})**",
|
||||
"color": 15105570,
|
||||
"fields": [
|
||||
{"name": "🐳 Update your Docker image", "value": "```docker pull agregarr/agregarr:latest```", "inline": false}
|
||||
],
|
||||
"thumbnail": {"url": "https://raw.githubusercontent.com/agregarr/agregarr/develop/public/android-chrome-512x512.png"}
|
||||
}]
|
||||
}' \
|
||||
${{ secrets.DISCORD_WEBHOOK_LATEST }}
|
||||
|
||||
+83
-214
@@ -1,7 +1,5 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { ImdbAxiosClient } from '@server/lib/collections/utils/ImdbAxiosClient';
|
||||
import logger from '@server/logger';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
/**
|
||||
* IMDb List Item interface
|
||||
@@ -55,36 +53,16 @@ export interface ImdbTop250Result {
|
||||
/**
|
||||
* IMDb API client for fetching lists and popular content
|
||||
*
|
||||
* Note: IMDb doesn't have a public API for lists, so this uses web scraping
|
||||
* for public IMDb lists. This is a best-effort implementation.
|
||||
* Uses the shared ImdbAxiosClient which handles AWS WAF challenges
|
||||
* and maintains cookies for reliable access to IMDb.
|
||||
*/
|
||||
class ImdbAPI extends ExternalAPI {
|
||||
class ImdbAPI {
|
||||
// Cache for Top 250 lists (refreshed periodically)
|
||||
private top250MoviesCache: Map<string, number> = new Map();
|
||||
private top250TvCache: Map<string, number> = new Map();
|
||||
private top250LastRefresh: { movies?: number; tv?: number } = {};
|
||||
private readonly TOP250_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
'https://www.imdb.com',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
Accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('imdb').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a predefined IMDb top list
|
||||
*/
|
||||
@@ -98,41 +76,46 @@ class ImdbAPI extends ExternalAPI {
|
||||
|
||||
switch (listType) {
|
||||
case ImdbTopList.TOP_250_MOVIES:
|
||||
url = '/chart/top/';
|
||||
url = 'https://www.imdb.com/chart/top/';
|
||||
expectedType = 'movie';
|
||||
break;
|
||||
case ImdbTopList.TOP_250_ENGLISH_MOVIES:
|
||||
url = '/chart/top-english-movies/';
|
||||
url = 'https://www.imdb.com/chart/top-english-movies/';
|
||||
expectedType = 'movie';
|
||||
break;
|
||||
case ImdbTopList.TOP_250_TV:
|
||||
url = '/chart/toptv/';
|
||||
url = 'https://www.imdb.com/chart/toptv/';
|
||||
expectedType = 'tv';
|
||||
break;
|
||||
case ImdbTopList.POPULAR_MOVIES:
|
||||
url = '/chart/moviemeter/';
|
||||
url = 'https://www.imdb.com/chart/moviemeter/';
|
||||
expectedType = 'movie';
|
||||
break;
|
||||
case ImdbTopList.POPULAR_TV:
|
||||
url = '/chart/tvmeter/';
|
||||
url = 'https://www.imdb.com/chart/tvmeter/';
|
||||
expectedType = 'tv';
|
||||
break;
|
||||
case ImdbTopList.MOST_POPULAR_MOVIES:
|
||||
url = '/chart/boxoffice/';
|
||||
url = 'https://www.imdb.com/chart/boxoffice/';
|
||||
expectedType = 'movie';
|
||||
break;
|
||||
case ImdbTopList.MOST_POPULAR_TV:
|
||||
url = '/chart/tvpopular/';
|
||||
url = 'https://www.imdb.com/chart/tvpopular/';
|
||||
expectedType = 'tv';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown IMDb top list type: ${listType}`);
|
||||
}
|
||||
|
||||
const html = await this.get<string>(url, undefined, 30000);
|
||||
// Use the shared ImdbAxiosClient with WAF handling
|
||||
const axios = await ImdbAxiosClient.getInstance();
|
||||
const response = await axios.get(url, { timeout: 30000 });
|
||||
const html = response.data as string;
|
||||
|
||||
return this.parseTopListHtml(html, expectedType, limit);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch IMDb top list ${listType}:`, {
|
||||
label: 'IMDb API',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
listType,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
@@ -145,205 +128,91 @@ class ImdbAPI extends ExternalAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a custom IMDb list by URL
|
||||
*/
|
||||
public async getCustomList(
|
||||
listUrl: string,
|
||||
limit = 9999
|
||||
): Promise<ImdbListItem[]> {
|
||||
try {
|
||||
// Extract list ID from URL
|
||||
const listMatch = listUrl.match(/\/list\/(ls\d+)/);
|
||||
if (!listMatch) {
|
||||
throw new Error('Invalid IMDb list URL format');
|
||||
}
|
||||
|
||||
const listId = listMatch[1];
|
||||
const url = `/list/${listId}/`;
|
||||
|
||||
const html = await this.get<string>(url, undefined, 30000);
|
||||
return this.parseCustomListHtml(html, limit);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch IMDb custom list ${listUrl}:`, {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
listUrl,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw new Error(
|
||||
`Failed to fetch IMDb custom list: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML for top lists (Top 250, Popular, etc.)
|
||||
* Uses JSON-LD structured data which IMDb provides for all chart pages.
|
||||
*/
|
||||
private parseTopListHtml(
|
||||
html: string,
|
||||
expectedType: 'movie' | 'tv',
|
||||
limit: number
|
||||
): ImdbListItem[] {
|
||||
const dom = new JSDOM(html);
|
||||
const document = dom.window.document;
|
||||
const items: ImdbListItem[] = [];
|
||||
const items = this.parseJsonLd(html, expectedType, limit);
|
||||
|
||||
// Different selectors for different chart types
|
||||
const itemSelectors = [
|
||||
'.cli-item', // Top 250 movies/TV
|
||||
'.titleColumn', // Some chart pages
|
||||
'.ipc-title-link-wrapper', // Newer layout
|
||||
'.titleListItem', // Fallback
|
||||
];
|
||||
|
||||
let itemElements: NodeListOf<Element> | null = null;
|
||||
|
||||
for (const selector of itemSelectors) {
|
||||
itemElements = document.querySelectorAll(selector);
|
||||
if (itemElements.length > 0) break;
|
||||
if (items.length > 0) {
|
||||
logger.debug('Parsed IMDb list from JSON-LD', {
|
||||
label: 'IMDb API',
|
||||
itemCount: items.length,
|
||||
expectedType,
|
||||
});
|
||||
} else {
|
||||
logger.warn('No items found in IMDb JSON-LD data', {
|
||||
label: 'IMDb API',
|
||||
expectedType,
|
||||
});
|
||||
}
|
||||
|
||||
if (!itemElements || itemElements.length === 0) {
|
||||
logger.warn('No items found in IMDb top list HTML');
|
||||
return [];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
for (let i = 0; i < Math.min(itemElements.length, limit); i++) {
|
||||
const element = itemElements[i];
|
||||
const imdbId = this.extractImdbId(element);
|
||||
const title = this.extractTitle(element);
|
||||
const year = this.extractYear(element);
|
||||
/**
|
||||
* Parse JSON-LD structured data from IMDb page
|
||||
* IMDb includes ItemList schema with all items - much more reliable than HTML scraping
|
||||
*/
|
||||
private parseJsonLd(
|
||||
html: string,
|
||||
expectedType: 'movie' | 'tv',
|
||||
limit: number
|
||||
): ImdbListItem[] {
|
||||
try {
|
||||
// Look for ItemList JSON-LD script
|
||||
const jsonLdMatch = html.match(
|
||||
/<script type="application\/ld\+json">(\{"@type":"ItemList"[^<]+)<\/script>/
|
||||
);
|
||||
|
||||
if (!jsonLdMatch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = JSON.parse(jsonLdMatch[1]) as {
|
||||
itemListElement?: {
|
||||
item?: {
|
||||
url?: string;
|
||||
name?: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
const itemListElement = data.itemListElement || [];
|
||||
const items: ImdbListItem[] = [];
|
||||
|
||||
for (let i = 0; i < Math.min(itemListElement.length, limit); i++) {
|
||||
const listItem = itemListElement[i];
|
||||
const movie = listItem.item;
|
||||
|
||||
if (!movie?.url || !movie?.name) continue;
|
||||
|
||||
// Extract IMDb ID from URL (e.g., https://www.imdb.com/title/tt0111161/)
|
||||
const urlMatch = movie.url.match(/\/title\/(tt\d+)/);
|
||||
if (!urlMatch) continue;
|
||||
|
||||
const imdbId = urlMatch[1];
|
||||
|
||||
if (imdbId && title) {
|
||||
items.push({
|
||||
imdbId,
|
||||
title,
|
||||
year,
|
||||
title: movie.name,
|
||||
type: expectedType,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
} catch (error) {
|
||||
logger.debug('Failed to parse JSON-LD from IMDb page', {
|
||||
label: 'IMDb API',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML for custom user lists
|
||||
*/
|
||||
private parseCustomListHtml(html: string, limit: number): ImdbListItem[] {
|
||||
const dom = new JSDOM(html);
|
||||
const document = dom.window.document;
|
||||
const items: ImdbListItem[] = [];
|
||||
|
||||
const itemElements = document.querySelectorAll('.lister-item, .ipc-title');
|
||||
|
||||
for (let i = 0; i < Math.min(itemElements.length, limit); i++) {
|
||||
const element = itemElements[i];
|
||||
const imdbId = this.extractImdbId(element);
|
||||
const title = this.extractTitle(element);
|
||||
const year = this.extractYear(element);
|
||||
const type = this.inferType(element);
|
||||
|
||||
if (imdbId && title) {
|
||||
items.push({
|
||||
imdbId,
|
||||
title,
|
||||
year,
|
||||
type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract IMDb ID from an element
|
||||
*/
|
||||
private extractImdbId(element: Element): string | null {
|
||||
// Look for links with IMDb title patterns
|
||||
const linkSelectors = ['a[href*="/title/"]', '[href*="/title/"]'];
|
||||
|
||||
for (const selector of linkSelectors) {
|
||||
const link = element.querySelector(selector) || element.closest(selector);
|
||||
if (link) {
|
||||
const href = link.getAttribute('href');
|
||||
const match = href?.match(/\/title\/(tt\d+)/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from an element
|
||||
*/
|
||||
private extractTitle(element: Element): string | null {
|
||||
const titleSelectors = [
|
||||
'.cli-title a',
|
||||
'.titleColumn a',
|
||||
'.ipc-title__text',
|
||||
'.titleListItem .title a',
|
||||
'h3 a',
|
||||
'.title a',
|
||||
'a',
|
||||
];
|
||||
|
||||
for (const selector of titleSelectors) {
|
||||
const titleElement = element.querySelector(selector);
|
||||
if (titleElement?.textContent?.trim()) {
|
||||
return titleElement.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract year from an element
|
||||
*/
|
||||
private extractYear(element: Element): number | undefined {
|
||||
const yearSelectors = [
|
||||
'.cli-title-metadata .cli-title-metadata-item:first-child',
|
||||
'.secondaryInfo',
|
||||
'.lister-item-year',
|
||||
'.year',
|
||||
];
|
||||
|
||||
for (const selector of yearSelectors) {
|
||||
const yearElement = element.querySelector(selector);
|
||||
if (yearElement?.textContent) {
|
||||
const yearMatch = yearElement.textContent.match(/(\d{4})/);
|
||||
if (yearMatch) {
|
||||
return parseInt(yearMatch[1], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer media type from element content
|
||||
*/
|
||||
private inferType(element: Element): 'movie' | 'tv' {
|
||||
const text = element.textContent?.toLowerCase() || '';
|
||||
|
||||
// Look for TV indicators
|
||||
if (
|
||||
text.includes('tv series') ||
|
||||
text.includes('tv mini series') ||
|
||||
text.includes('episode') ||
|
||||
text.includes('season')
|
||||
) {
|
||||
return 'tv';
|
||||
}
|
||||
|
||||
// Default to movie
|
||||
return 'movie';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -606,6 +606,7 @@ export class CollectionSyncService {
|
||||
createPlaceholdersForMissing: config.createPlaceholdersForMissing,
|
||||
placeholderDaysAhead: config.placeholderDaysAhead,
|
||||
placeholderReleasedDays: config.placeholderReleasedDays,
|
||||
includeAllReleasedItems: config.includeAllReleasedItems,
|
||||
applyOverlaysDuringSync: config.applyOverlaysDuringSync,
|
||||
};
|
||||
|
||||
|
||||
@@ -458,6 +458,7 @@ const CollectionSettings = ({
|
||||
createPlaceholdersForMissing: config.createPlaceholdersForMissing,
|
||||
placeholderDaysAhead: config.placeholderDaysAhead,
|
||||
placeholderReleasedDays: config.placeholderReleasedDays,
|
||||
includeAllReleasedItems: config.includeAllReleasedItems,
|
||||
tautulliStatType: config.tautulliStatType,
|
||||
minimumPlays: config.minimumPlays,
|
||||
searchMissingMovies: config.searchMissingMovies,
|
||||
|
||||
+23
-3
@@ -765,7 +765,7 @@
|
||||
"components.Settings.testTraktConnection": "Tester la connexion",
|
||||
"components.Collections.FormSections.selectRootFolder": "Sélectionner le dossier racine...",
|
||||
"components.Settings.OverseerrModal.testFirstRootFolders": "Sélectionnez d'abord un serveur",
|
||||
"components.Collections.Forms.configureDownloads": "Configurer les fichiers fictifs",
|
||||
"components.Collections.Forms.configureDownloads": "Configurer les téléchargements",
|
||||
"components.OverlayEditor.segmentValue": "Contenu du texte",
|
||||
"components.PostersView.syncOverlaysConfirm": "Confirmer ?",
|
||||
"components.PostersView.localDescription": "Utilisez des affiches personnalisées à partir de dossiers organisés. Placez les images dans la structure de dossiers indiquée ci-dessous. Peut être alimenté avec des affiches Plex. En cas de fichier introuvable, TMDB est utilisé comme solution de secours.",
|
||||
@@ -1511,7 +1511,7 @@
|
||||
"components.Collections.FormSections.tvShows": "Séries",
|
||||
"components.Collections.FormSections.tmdbUrlExamples": "Exemples : Collection (https://www.themoviedb.org/collection/12345), Liste (https://www.themoviedb.org/list/310), Chaîne (https://www.themoviedb.org/network/213), Société (https://www.themoviedb.org/company/7505/movie ou /tv)",
|
||||
"components.Collections.FormSections.to": "À",
|
||||
"components.Collections.FormSections.traktUrlExamples": "Exemples : https://trakt.tv/users/nomutilisateur/lists/nomdelaliste ou https://trakt.tv/lists/official/jurassic-park-collection",
|
||||
"components.Collections.FormSections.traktUrlExamples": "Exemples : https://trakt.tv/users/nomutilisateur/lists/nomdelaliste ou https://app.trakt.tv/users/nomutilisateur/lists/nomdelaliste",
|
||||
"components.Collections.FormSections.useSeparator": "Utiliser un séparateur",
|
||||
"components.Collections.FormSections.useSeparatorHelp": "Créer une collection séparateur simple pour regrouper vos collections auto {type}.",
|
||||
"components.Collections.FormSections.userRequestCollectionsRestricted": "Les collections «demandes utilisateur» sont limitées à la visibilité «Onglet Bibliothèque uniquement» à cause d’un bug Plex qui ne respecte pas les restrictions de labels sur les écrans Accueil/Recommandé. Les collections «Franchises TMDB» et les «Collections auto par réalisateurs» de la bibliothèque Plex sont masquées pour ne pas encombrer les écrans Accueil/Recommandé.",
|
||||
@@ -1548,5 +1548,25 @@
|
||||
"components.Collections.Forms.franchiseTemplateNote": "<strong>Remarque :</strong> votre modèle de titre doit inclure <code>« {franchiseName} »</code> (ex. <code>« {franchiseName} »</code> ou « Films de la franchise <code>{franchiseName}</code> »).",
|
||||
"components.Collections.Forms.imdbRatingAsc": "Note IMDb (plus faible à la plus élevée)",
|
||||
"components.Collections.Forms.imdbRatingDesc": "Note IMDb (plus élevée à la plus faible)",
|
||||
"components.Collections.Forms.itemOrder": "Ordre des éléments"
|
||||
"components.Collections.Forms.itemOrder": "Ordre des éléments",
|
||||
"components.Collections.Forms.days": "jours",
|
||||
"components.Collections.Forms.includeAllReleasedItems": "Inclure tous les éléments déjà diffusés",
|
||||
"components.Collections.Forms.includeAllReleasedItemsHelp": "Créer des fichiers fictifs pour tous les éléments, quelle que soit leur date de diffusion",
|
||||
"components.Collections.Forms.limitCollectionItems": "Limiter la collection à ce nombre d’éléments{smartCollectionNote}",
|
||||
"components.Collections.Forms.limitCollectionItemsSmartNote": " (s’applique aux collections intelligentes)",
|
||||
"components.Collections.Forms.onlyRecentlyReleased": "Inclure uniquement les éléments diffusés depuis",
|
||||
"components.Collections.Forms.onlyRecentlyReleasedHelp": "Créer des fichiers fictifs uniquement pour les éléments diffusés dans le nombre de jours indiqué",
|
||||
"components.Collections.Forms.overseerrUserCollectionsDescription": "Crée une collection pour chaque utilisateur Overseerr à partir de ses demandes, et utilise des étiquettes et des restrictions pour que seul l’utilisateur demandeur puisse voir ses demandes. Comme le propriétaire du serveur ne peut pas être restreint, toutes les collections lui seront visibles.",
|
||||
"components.Collections.Forms.placeholderCreation": "Création de fichiers fictifs",
|
||||
"components.Collections.Forms.placeholderCreationHelp": "Crée des fichiers fictifs dans Plex pour les éléments pas encore disponibles, avec des overlays de compte à rebours indiquant les dates de sortie/diffusion.",
|
||||
"components.Collections.Forms.placeholderFoldersNotConfigured": "Les {pluralLibrary} suivantes {pluralNeed} des dossiers racine de fichiers fictifs configurés : <strong>{namesText}</strong>. Configurez les dossiers de fichiers fictifs pour {libraryCount} dans Paramètres > Téléchargements.",
|
||||
"components.Collections.Forms.pleaseFixErrors": "Veuillez corriger les erreurs suivantes :",
|
||||
"components.Collections.Forms.randomOrder": "Ordre aléatoire (mélangé à chaque synchro)",
|
||||
"components.Collections.Forms.randomizeHomeOrder": "Ordre de l'accueil aléatoire",
|
||||
"components.Collections.Forms.releaseDateAsc": "Date de diffusion (de la plus ancienne à la plus récente)",
|
||||
"components.Collections.Forms.releaseDateDesc": "Date de diffusion (de la plus récente à la plus ancienne)",
|
||||
"components.Collections.Forms.releasedItems": "Éléments déjà diffusés",
|
||||
"components.Collections.Forms.reverseOrder": "Ordre inversé",
|
||||
"components.Collections.Forms.shuffleHubCollectionHelp": "Si activé, la position de ce {itemType} sera mélangée aléatoirement avec les autres collections ayant cette option activée à chaque synchro. Une synchro personnalisée du mélange peut être définie depuis l'onglet « Tâches » des paramètres.",
|
||||
"components.Collections.Forms.shufflePositionHelp": "Si activé, la position de cette collection sera mélangée aléatoirement avec les autres collections ayant cette option activée à chaque synchro. Une synchro personnalisée du mélange peut être définie depuis l'onglet « Tâches » des paramètres."
|
||||
}
|
||||
|
||||
@@ -175,6 +175,9 @@ export const saveIndividualConfigs = async (
|
||||
...(collectionConfig.placeholderReleasedDays !== undefined && {
|
||||
placeholderReleasedDays: collectionConfig.placeholderReleasedDays,
|
||||
}),
|
||||
...(collectionConfig.includeAllReleasedItems !== undefined && {
|
||||
includeAllReleasedItems: collectionConfig.includeAllReleasedItems,
|
||||
}),
|
||||
...(collectionConfig.tautulliStatType && {
|
||||
tautulliStatType: collectionConfig.tautulliStatType,
|
||||
}),
|
||||
|
||||
@@ -224,6 +224,7 @@ export const linkCollectionConfig = async (
|
||||
masterConfig.createPlaceholdersForMissing,
|
||||
placeholderDaysAhead: masterConfig.placeholderDaysAhead,
|
||||
placeholderReleasedDays: masterConfig.placeholderReleasedDays,
|
||||
includeAllReleasedItems: masterConfig.includeAllReleasedItems,
|
||||
tautulliStatType: masterConfig.tautulliStatType,
|
||||
isMultiSource: masterConfig.isMultiSource,
|
||||
sources: masterConfig.sources,
|
||||
|
||||
Reference in New Issue
Block a user