Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/server/src/app/config/databasePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as fs from 'fs'
import path from 'path'

export const dataDir =
process.env.NODE_ENV === 'production'
? '/opt/data'
: path.join(__dirname, '../../../../../data')

const defaultDatabaseName = 'maintainerr.sqlite'
const databaseFilenamePattern = /^maintainerr.*\.sqlite$/i

export const resolveDatabasePath = () => {
const defaultDatabasePath = path.join(dataDir, defaultDatabaseName)

if (fs.existsSync(defaultDatabasePath)) {
return defaultDatabasePath
}

if (!fs.existsSync(dataDir)) {
return defaultDatabasePath
}

const matchingDatabases = fs
.readdirSync(dataDir, { withFileTypes: true })
.filter(
(entry) => entry.isFile() && databaseFilenamePattern.test(entry.name),
)
.map((entry) => entry.name)

if (matchingDatabases.length === 0) {
return defaultDatabasePath
}

if (matchingDatabases.length > 1) {
throw new Error(
`Multiple Maintainerr database files found in ${dataDir}: ${matchingDatabases.join(
', ',
)}. Rename the database you want to use to ${defaultDatabaseName}.`,
)
}

return path.join(dataDir, matchingDatabases[0])
}
6 changes: 2 additions & 4 deletions apps/server/src/app/config/typeOrmConfig.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm'
import { resolveDatabasePath } from './databasePath'

const ormConfig: TypeOrmModuleOptions = {
type: 'better-sqlite3',
logging: false,
database:
process.env.NODE_ENV === 'production'
? '/opt/data/maintainerr.sqlite'
: '../../data/maintainerr.sqlite',
database: resolveDatabasePath(),
subscribers: ['./**/*.subscriber{.ts,.js}'],
migrations:
process.env.NODE_ENV === 'production'
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/datasource-config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DataSource } from 'typeorm'
import { resolveDatabasePath } from './app/config/databasePath'

const datasource = new DataSource({
type: 'better-sqlite3',
database: '../../data/maintainerr.sqlite',
database: resolveDatabasePath(),
entities: ['./src/**/*.entities.ts'],
synchronize: false,
migrationsTableName: 'migrations',
Expand Down
8 changes: 2 additions & 6 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import * as fs from 'fs'
import { cleanupOpenApiDoc } from 'nestjs-zod'
import path from 'path'
import { AppModule } from './app/app.module'
import { dataDir, resolveDatabasePath } from './app/config/databasePath'
import { MaintainerrLogger } from './modules/logging/logs.service'

const dataDir =
process.env.NODE_ENV === 'production'
? '/opt/data'
: path.join(__dirname, '../../../data')

async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
Expand Down Expand Up @@ -60,7 +56,7 @@ function createDataDirectoryStructure() {
}

// if db already exists, check r/w permissions
const db = path.join(dataDir, 'maintainerr.sqlite')
const db = resolveDatabasePath()
if (fs.existsSync(db)) {
fs.accessSync(db, fs.constants.R_OK | fs.constants.W_OK)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ describe('RuleExecutorService', () => {
const settings = {
media_server_type: mediaServerType,
testConnections: jest.fn().mockResolvedValue(true),
testMediaServerConnection: jest.fn().mockResolvedValue(true),
testRadarr: jest.fn().mockResolvedValue({ status: 'OK' }),
testSonarr: jest.fn().mockResolvedValue({ status: 'OK' }),
testSeerr: jest.fn().mockResolvedValue({ status: 'OK' }),
testTautulli: jest.fn().mockResolvedValue({ status: 'OK' }),
seerrConfigured: jest.fn().mockReturnValue(true),
tautulliConfigured: jest.fn().mockReturnValue(true),
testSetup: jest.fn().mockResolvedValue(true),
} as unknown as jest.Mocked<SettingsService>

Expand Down Expand Up @@ -250,4 +257,47 @@ describe('RuleExecutorService', () => {
)
expect(progressManager.reset).not.toHaveBeenCalled()
})

it('only checks the selected Radarr server when executing a Radarr rule group', async () => {
const { service, rulesService, settings } = createService(
MediaServerType.PLEX,
)

rulesService.getRuleGroup.mockResolvedValue({
id: 77,
name: 'Movies',
isActive: true,
libraryId: '1',
dataType: 'movie',
useRules: true,
collection: {
radarrSettingsId: 1,
},
rules: [
{
ruleJson: JSON.stringify({
firstVal: [1, 0],
action: 0,
customVal: {
ruleTypeId: 0,
value: 1,
},
}),
},
],
} as any)
rulesService.getRuleGroupById.mockResolvedValue({
id: 77,
collectionId: 1,
} as any)

const abortController = new AbortController()

await service.executeForRuleGroups(77, abortController.signal)

expect(settings.testRadarr).toHaveBeenCalledTimes(1)
expect(settings.testRadarr).toHaveBeenCalledWith(1)
expect(settings.testSonarr).not.toHaveBeenCalled()
expect(settings.testConnections).not.toHaveBeenCalled()
})
})
69 changes: 67 additions & 2 deletions apps/server/src/modules/rules/tasks/rule-executor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
} from '../../events/events.dto'
import { MaintainerrLogger } from '../../logging/logs.service'
import { SettingsService } from '../../settings/settings.service'
import { RuleConstants } from '../constants/rules.constants'
import { Application, RuleConstants } from '../constants/rules.constants'
import { RuleDto } from '../dtos/rule.dto'
import { RulesDto } from '../dtos/rules.dto'
import { RuleGroup } from '../entities/rule-group.entities'
import { RuleComparatorServiceFactory } from '../helpers/rule.comparator.service'
Expand Down Expand Up @@ -110,7 +111,7 @@ export class RuleExecutorService {
return
}

const appStatus = await this.settings.testConnections()
const appStatus = await this.testRuleGroupConnections(ruleGroup)

if (appStatus) {
// reset API caches, make sure latest data is used
Expand Down Expand Up @@ -340,6 +341,70 @@ export class RuleExecutorService {
}
}

private async testRuleGroupConnections(ruleGroup: RulesDto) {
if (!(await this.settings.testMediaServerConnection())) {
return false
}

const referencedApplications = this.getReferencedApplications(ruleGroup)
const selectedRadarrSettingsId = ruleGroup.collection?.radarrSettingsId
const selectedSonarrSettingsId = ruleGroup.collection?.sonarrSettingsId

if (
(referencedApplications.has(Application.RADARR) ||
selectedRadarrSettingsId != null) &&
(!selectedRadarrSettingsId ||
(await this.settings.testRadarr(selectedRadarrSettingsId)).status !==
'OK')
) {
return false
}

if (
(referencedApplications.has(Application.SONARR) ||
selectedSonarrSettingsId != null) &&
(!selectedSonarrSettingsId ||
(await this.settings.testSonarr(selectedSonarrSettingsId)).status !==
'OK')
) {
return false
}

if (
referencedApplications.has(Application.SEERR) &&
(!this.settings.seerrConfigured() ||
(await this.settings.testSeerr()).status !== 'OK')
) {
return false
}

if (
referencedApplications.has(Application.TAUTULLI) &&
(!this.settings.tautulliConfigured() ||
(await this.settings.testTautulli()).status !== 'OK')
) {
return false
}

return true
}

private getReferencedApplications(ruleGroup: RulesDto) {
const applications = new Set<number>()

for (const rule of ruleGroup.rules ?? []) {
const parsedRule =
'ruleJson' in rule ? (JSON.parse(rule.ruleJson) as RuleDto) : rule
applications.add(parsedRule.firstVal[0])

if (parsedRule.lastVal) {
applications.add(parsedRule.lastVal[0])
}
}

return applications
}

private async handleCollection(rulegroup: RuleGroup): Promise<Set<string>> {
try {
let collection = await this.collectionService.getCollection(
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/Common/DeleteButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface IDeleteButton {
const DeleteButton = (props: IDeleteButton) => {
return (
<button
className="right-5 m-auto flex h-8 w-full rounded-b bg-zinc-800 text-white shadow-md ring-1 ring-zinc-700 hover:bg-zinc-700 xl:rounded-l-none xl:rounded-r"
className="right-5 m-auto flex h-8 w-full rounded-b bg-red-700 text-white shadow-md hover:bg-red-600 xl:rounded-l-none xl:rounded-r"
onClick={props.onClick}
>
<div className="m-auto ml-auto flex">
Expand Down
23 changes: 13 additions & 10 deletions apps/ui/src/components/Overview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -959,30 +959,33 @@ const MediaMetricCard = ({ libraries }: { libraries: AppLibraryStats[] }) => {
Media
</span>
</div>
<div className="mt-2 grid grid-cols-1 gap-1.5 xs:grid-cols-2">
<div className="mt-2 grid grid-cols-1 gap-1.5 xs:grid-cols-2 lg:gap-2">
{libraries.map((library) => (
<div key={library.id} className="rounded-md bg-zinc-900 px-2 py-1">
<p className="truncate text-[11px] font-medium text-zinc-400">
<div
key={library.id}
className="rounded-md bg-zinc-900 px-2 py-1 lg:px-3 lg:py-2"
>
<p className="truncate text-[11px] font-medium text-zinc-400 lg:text-xs">
{library.title}
</p>
<p className="text-xs font-semibold text-zinc-100">
<p className="text-xs font-semibold text-zinc-100 lg:text-sm">
{formatNumber(library.itemCount)}
</p>
</div>
))}
<div className="rounded-md bg-zinc-900 px-2 py-1">
<p className="truncate text-[11px] font-medium text-zinc-400">
<div className="rounded-md bg-zinc-900 px-2 py-1 lg:px-3 lg:py-2">
<p className="truncate text-[11px] font-medium text-zinc-400 lg:text-xs">
Seasons
</p>
<p className="text-xs font-semibold text-zinc-100">
<p className="text-xs font-semibold text-zinc-100 lg:text-sm">
{formatNumber(seasonTotal)}
</p>
</div>
<div className="rounded-md bg-zinc-900 px-2 py-1">
<p className="truncate text-[11px] font-medium text-zinc-400">
<div className="rounded-md bg-zinc-900 px-2 py-1 lg:px-3 lg:py-2">
<p className="truncate text-[11px] font-medium text-zinc-400 lg:text-xs">
Episodes
</p>
<p className="text-xs font-semibold text-zinc-100">
<p className="text-xs font-semibold text-zinc-100 lg:text-sm">
{formatNumber(episodeTotal)}
</p>
</div>
Expand Down
20 changes: 10 additions & 10 deletions apps/ui/src/components/Rules/Rule/RuleCreator/RuleInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,37 +125,37 @@ const RuleInput = (props: IRuleInput) => {
) {
queueMicrotask(() => {
setSecondVal(CustomParams.CUSTOM_DAYS)
setRuleType(RuleType.NUMBER)
setCustomValType(RuleType.TEXT)
})
} else {
queueMicrotask(() => {
setSecondVal(CustomParams.CUSTOM_NUMBER)
setRuleType(RuleType.NUMBER)
setCustomValType(RuleType.NUMBER)
})
}
break
case 1:
queueMicrotask(() => {
setSecondVal(CustomParams.CUSTOM_DATE)
setRuleType(RuleType.DATE)
setCustomValType(RuleType.DATE)
})
break
case 2:
queueMicrotask(() => {
setSecondVal(CustomParams.CUSTOM_TEXT)
setRuleType(RuleType.TEXT)
setCustomValType(RuleType.TEXT)
})
break
case 3:
queueMicrotask(() => {
setSecondVal(CustomParams.CUSTOM_BOOLEAN)
setRuleType(RuleType.BOOL)
setCustomValType(RuleType.BOOL)
})
break
case 4:
queueMicrotask(() => {
setSecondVal(CustomParams.CUSTOM_TEXT_LIST)
setRuleType(RuleType.TEXT_LIST)
setCustomValType(RuleType.TEXT_LIST)
})
break
}
Expand Down Expand Up @@ -388,16 +388,16 @@ const RuleInput = (props: IRuleInput) => {
} else if (secondVal === CustomParams.CUSTOM_BOOLEAN) {
setCustomValActive(true)
setCustomValType(RuleType.BOOL)
if (customVal !== '0') {
setCustomVal('1')
}
setCustomVal((currentCustomVal) =>
currentCustomVal === '0' ? currentCustomVal : '1',
)
} else {
setCustomValActive(false)
setCustomVal(undefined)
}
})
}
}, [customVal, secondVal])
}, [secondVal])

if (!constants || constantsLoading) {
return <LoadingSpinner />
Expand Down
Loading