Compare commits

...
21 Commits
Author SHA1 Message Date
MasutaYunikon a39a98615b Merge pull request #69 from Masutayunikon/release-please--branches--main
chore(main): release 3.3.3
2026-04-08 10:04:19 +02:00
MasutaYunikon 2a3defdb34 chore(main): release 3.3.3 2026-04-07 20:07:35 +02:00
MasutaYunikon 733f8790ca fix: path 2026-04-07 20:07:03 +02:00
MasutaYunikon 4a9bbd9402 Merge remote-tracking branch 'origin/main' 2026-04-07 19:53:19 +02:00
MasutaYunikon 1562ca32f9 fix: MediaManagementView.vue api call is now in mounted 2026-04-07 19:53:10 +02:00
MasutaYunikon 7b780e4fa1 Merge pull request #68 from Masutayunikon/release-please--branches--main
chore(main): release 3.3.2
2026-04-07 19:42:14 +02:00
MasutaYunikon 444b30ef3d chore(main): release 3.3.2 2026-04-07 19:41:28 +02:00
MasutaYunikon 42b46496d1 Merge remote-tracking branch 'origin/main' 2026-04-07 19:40:57 +02:00
MasutaYunikon f092da4108 fix: use fetch instead of useFetch 2026-04-07 19:40:49 +02:00
MasutaYunikon 8e635c1d28 Merge pull request #67 from Masutayunikon/release-please--branches--main
chore(main): release 3.3.1
2026-04-07 19:24:53 +02:00
MasutaYunikon 3b8b7a0454 chore(main): release 3.3.1 2026-04-07 19:24:23 +02:00
MasutaYunikon 9ef1516b0b Merge remote-tracking branch 'origin/main' 2026-04-07 19:23:57 +02:00
MasutaYunikon 1af210058c fix: add driver register for transmission 2026-04-07 19:23:49 +02:00
MasutaYunikon 6caad83081 Merge pull request #66 from Masutayunikon/release-please--branches--main
chore(main): release 3.3.0
2026-04-07 19:05:43 +02:00
MasutaYunikon 520bfcfae1 chore(main): release 3.3.0 2026-04-07 19:05:07 +02:00
MasutaYunikon 2e5bc8953a Merge remote-tracking branch 'origin/main' 2026-04-07 19:04:34 +02:00
MasutaYunikon f304562f01 feat: add transmission client
feat: set defaultPath for any system
2026-04-07 19:04:26 +02:00
MasutaYunikon c9690d9a1b Merge pull request #65 from Masutayunikon/release-please--branches--main
chore(main): release 3.2.1
2026-04-07 16:18:47 +02:00
MasutaYunikon 5daface279 chore(main): release 3.2.1 2026-04-07 16:17:52 +02:00
MasutaYunikon 5687aaa5d9 Merge remote-tracking branch 'origin/main' 2026-04-07 16:17:22 +02:00
MasutaYunikon ba67817ea0 fix: bun target android 2026-04-07 16:17:13 +02:00
6 changed files with 204 additions and 4 deletions

No files matched your search

+1 -1
View File
@@ -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
+37
View File
@@ -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)
+6
View File
@@ -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())
+15
View File
@@ -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/' : '/',
})
}
+137
View File
@@ -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
+8 -3
View File
@@ -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() {