Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a39a98615b | ||
|
|
2a3defdb34 | ||
|
|
733f8790ca | ||
|
|
4a9bbd9402 | ||
|
|
1562ca32f9 | ||
|
|
7b780e4fa1 | ||
|
|
444b30ef3d | ||
|
|
42b46496d1 | ||
|
|
f092da4108 | ||
|
|
8e635c1d28 | ||
|
|
3b8b7a0454 | ||
|
|
9ef1516b0b | ||
|
|
1af210058c | ||
|
|
6caad83081 | ||
|
|
520bfcfae1 | ||
|
|
2e5bc8953a | ||
|
|
f304562f01 | ||
|
|
c9690d9a1b | ||
|
|
5daface279 | ||
|
|
5687aaa5d9 | ||
|
|
ba67817ea0 |
No files matched your search
@@ -55,7 +55,7 @@ jobs:
|
||||
archive: fankarr-linux-x64.zip
|
||||
- target: linux-arm64
|
||||
os: ubuntu-latest
|
||||
bun-target: bun-linux-arm64-termux
|
||||
bun-target: bun-linux-arm64
|
||||
binary: binaries/fankarr
|
||||
archive: fankarr-linux-arm64-termux.zip
|
||||
- target: macos
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## [3.3.3](https://github.com/Masutayunikon/FanKarr/compare/v3.3.2...v3.3.3) (2026-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MediaManagementView.vue api call is now in mounted ([8c5b650](https://github.com/Masutayunikon/FanKarr/commit/8c5b650ca6eeac75f33e8c5423f6d4fd24f9871b))
|
||||
* path ([58ae88a](https://github.com/Masutayunikon/FanKarr/commit/58ae88a01c063318407134cd7351572220acd892))
|
||||
|
||||
## [3.3.2](https://github.com/Masutayunikon/FanKarr/compare/v3.3.1...v3.3.2) (2026-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use fetch instead of useFetch ([af550ec](https://github.com/Masutayunikon/FanKarr/commit/af550eccfbeeb116491e8ea4973eac493ed962d3))
|
||||
|
||||
## [3.3.1](https://github.com/Masutayunikon/FanKarr/compare/v3.3.0...v3.3.1) (2026-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add driver register for transmission ([7d2096e](https://github.com/Masutayunikon/FanKarr/commit/7d2096ec1972079a2e0e5bdfe01c4cc702e6b185))
|
||||
|
||||
## [3.3.0](https://github.com/Masutayunikon/FanKarr/compare/v3.2.1...v3.3.0) (2026-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add transmission client ([a2b88de](https://github.com/Masutayunikon/FanKarr/commit/a2b88de3a123aeb9c140e83605901dd5cdad041a))
|
||||
* set defaultPath for any system ([a2b88de](https://github.com/Masutayunikon/FanKarr/commit/a2b88de3a123aeb9c140e83605901dd5cdad041a))
|
||||
|
||||
## [3.2.1](https://github.com/Masutayunikon/FanKarr/compare/v3.2.0...v3.2.1) (2026-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bun target android ([a167a87](https://github.com/Masutayunikon/FanKarr/commit/a167a870188bdcd6b26644e304c2059ad5f05e96))
|
||||
|
||||
## [3.2.0](https://github.com/Masutayunikon/FanKarr/compare/v3.1.1...v3.2.0) (2026-04-07)
|
||||
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ import {
|
||||
sanitizeClient, dispatchDownload, dispatchList
|
||||
} from './torrent-clients/index.js'
|
||||
import qbittorrentDriver from './torrent-clients/qbittorrent.js'
|
||||
import transmissionDriver from './torrent-clients/transmission.js'
|
||||
import { organizeTorrent, autoOrganizeAll, scanMediaPath, workerRunning } from './organize.js'
|
||||
import { logger, readLogs, clearLogs, logsFileSize } from './logger.js'
|
||||
import { DATA_DIR, BASE_DIR } from './config.js'
|
||||
import { systemInfo } from './system.js'
|
||||
|
||||
registerDriver(qbittorrentDriver)
|
||||
registerDriver(transmissionDriver)
|
||||
|
||||
const app = express()
|
||||
const PORT = Number(process.env.PORT) || 9898
|
||||
@@ -36,6 +39,9 @@ app.post('/api/auth/setup', authSetup)
|
||||
app.post('/api/auth/login', authLogin)
|
||||
app.post('/api/auth/logout', authLogout)
|
||||
|
||||
// system info
|
||||
app.get('/api/system/info', systemInfo)
|
||||
|
||||
// ── Settings ───────────────────────────────────────────────────
|
||||
app.get('/api/settings', requireAuth, (_req, res) => {
|
||||
res.json(readSettings())
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
// GET /api/system/info
|
||||
export function systemInfo(_req: Request, res: Response): void {
|
||||
const isTermux = fs.existsSync('/data/data/com.termux')
|
||||
const platform = os.platform()
|
||||
|
||||
res.json({
|
||||
platform,
|
||||
isTermux,
|
||||
defaultPath: isTermux ? '/sdcard/' : '/',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Transmission Driver
|
||||
*/
|
||||
|
||||
import type { TorrentClientDriver, TorrentInfo } from './index.js'
|
||||
import { logger } from '../logger.js'
|
||||
|
||||
function mapState(status: number): TorrentInfo['state'] {
|
||||
// Transmission status codes :
|
||||
// 0 = stopped, 1 = check queued, 2 = checking, 3 = download queued,
|
||||
// 4 = downloading, 5 = seed queued, 6 = seeding
|
||||
if (status === 4 || status === 3) return 'downloading'
|
||||
if (status === 6 || status === 5) return 'seeding'
|
||||
if (status === 0) return 'paused'
|
||||
if (status === 1 || status === 2) return 'checking'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
async function trRequest(
|
||||
config: Record<string, string | number>,
|
||||
method: string,
|
||||
args: Record<string, unknown> = {},
|
||||
sessionId = ''
|
||||
): Promise<{ result: string; arguments?: any; sessionId?: string }> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Transmission-Session-Id': sessionId,
|
||||
}
|
||||
|
||||
if (config.username && config.password) {
|
||||
headers['Authorization'] = 'Basic ' + btoa(`${config.username}:${config.password}`)
|
||||
}
|
||||
|
||||
const res = await fetch(`${config.url}/transmission/rpc`, {
|
||||
method : 'POST',
|
||||
headers,
|
||||
body : JSON.stringify({ method, arguments: args }),
|
||||
})
|
||||
|
||||
// Transmission renvoie 409 avec le session ID à la première requête
|
||||
if (res.status === 409) {
|
||||
const newSessionId = res.headers.get('X-Transmission-Session-Id') ?? ''
|
||||
if (!newSessionId) throw new Error('Session ID introuvable dans la réponse 409')
|
||||
return trRequest(config, method, args, newSessionId)
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error(`Transmission HTTP ${res.status}`)
|
||||
|
||||
const data = await res.json()
|
||||
if (data.result !== 'success') throw new Error(`Transmission erreur : ${data.result}`)
|
||||
|
||||
return { ...data, sessionId }
|
||||
}
|
||||
|
||||
const TR: TorrentClientDriver = {
|
||||
definition: {
|
||||
id : 'transmission',
|
||||
label : 'Transmission',
|
||||
fields: [
|
||||
{ key: 'url', label: 'URL', type: 'url', placeholder: 'http://localhost:9091', required: true },
|
||||
{ key: 'username', label: 'Identifiant', type: 'text', placeholder: 'transmission', required: false },
|
||||
{ key: 'password', label: 'Mot de passe', type: 'password', placeholder: '••••••••', required: false },
|
||||
{ key: 'category', label: 'Dossier cible',type: 'text', placeholder: 'fankai', required: false, default: 'fankai' },
|
||||
],
|
||||
},
|
||||
|
||||
async test(config) {
|
||||
try {
|
||||
await trRequest(config, 'session-get')
|
||||
logger.info('transmission', `Test de connexion réussi sur ${config.url}`)
|
||||
return { ok: true, message: 'Connexion réussie' }
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Erreur inconnue'
|
||||
logger.warn('transmission', `Test de connexion échoué sur ${config.url} : ${msg}`)
|
||||
return { ok: false, message: msg }
|
||||
}
|
||||
},
|
||||
|
||||
async healthcheck(config) {
|
||||
try {
|
||||
const data = await trRequest(config, 'session-get')
|
||||
const version = data.arguments?.version ?? 'inconnue'
|
||||
logger.debug('transmission', `Healthcheck OK — version ${version}`)
|
||||
return { online: true, version }
|
||||
} catch (err) {
|
||||
logger.debug('transmission', `Healthcheck échoué : ${err instanceof Error ? err.message : err}`)
|
||||
return { online: false }
|
||||
}
|
||||
},
|
||||
|
||||
async list(config, category) {
|
||||
const fields = ['hashString', 'name', 'status', 'percentDone', 'totalSize',
|
||||
'downloadedEver', 'rateDownload', 'eta', 'downloadDir', 'labels']
|
||||
const data = await trRequest(config, 'torrent-get', { fields })
|
||||
const torrents: any[] = data.arguments?.torrents ?? []
|
||||
|
||||
return torrents
|
||||
.filter(t => {
|
||||
if (!category) return true
|
||||
// Filtrer par label ou par dossier contenant la catégorie
|
||||
return t.labels?.includes(category) ||
|
||||
(t.downloadDir as string)?.includes(category)
|
||||
})
|
||||
.map(t => ({
|
||||
hash : t.hashString,
|
||||
name : t.name,
|
||||
state : mapState(t.status),
|
||||
progress : Math.round(t.percentDone * 100),
|
||||
size : t.totalSize,
|
||||
downloaded: t.downloadedEver,
|
||||
speed : t.rateDownload,
|
||||
eta : t.eta ?? -1,
|
||||
save_path : t.downloadDir,
|
||||
category : t.labels?.[0] ?? '',
|
||||
} satisfies TorrentInfo))
|
||||
},
|
||||
|
||||
async add(config, url) {
|
||||
const args: Record<string, unknown> = { filename: url }
|
||||
|
||||
if (config.savePath) {
|
||||
args['download-dir'] = String(config.savePath)
|
||||
} else if (config.category) {
|
||||
// Si pas de savePath explicite, ajouter la catégorie comme sous-dossier
|
||||
args['download-dir'] = `/sdcard/Download/${config.category}`
|
||||
}
|
||||
|
||||
if (config.category) {
|
||||
args['labels'] = [String(config.category)]
|
||||
}
|
||||
|
||||
await trRequest(config, 'torrent-add', args)
|
||||
logger.info('transmission', `Torrent ajouté avec succès (catégorie: ${config.category ?? 'aucune'})`)
|
||||
},
|
||||
}
|
||||
|
||||
export default TR
|
||||
@@ -130,11 +130,13 @@ const form = ref({
|
||||
})
|
||||
|
||||
const picker = ref<{ open: boolean; field: 'completePath' | 'mediaPath'; currentPath: string }>({
|
||||
open: false, field: 'completePath', currentPath: '/',
|
||||
open: false,
|
||||
field: 'completePath',
|
||||
currentPath: '/',
|
||||
})
|
||||
|
||||
function openPicker(field: 'completePath' | 'mediaPath') {
|
||||
picker.value = { open: true, field, currentPath: form.value[field] || '/' }
|
||||
picker.value = { open: true, field, currentPath: form.value[field] || picker.value.currentPath || '/' }
|
||||
}
|
||||
|
||||
function onPickerSelect(path: string) {
|
||||
@@ -143,12 +145,15 @@ function onPickerSelect(path: string) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [settingsRes, systemRes] = await Promise.all([
|
||||
const [settingsRes, systemRes, infoRes] = await Promise.all([
|
||||
fetch('/api/settings', { credentials: 'include' }),
|
||||
fetch('/api/system', { credentials: 'include' }),
|
||||
fetch('/api/system/info', { credentials: 'include' }),
|
||||
])
|
||||
if (settingsRes.ok) Object.assign(form.value, await settingsRes.json())
|
||||
if (systemRes.ok) isDocker.value = (await systemRes.json()).isDocker
|
||||
if (infoRes.ok) picker.value.currentPath = (await infoRes.json()).defaultPath ?? '/'
|
||||
|
||||
})
|
||||
|
||||
async function save() {
|
||||
|
||||
Reference in new issue
Block a user