diff --git a/Dockerfile b/Dockerfile index 58f61fd..e7c7fbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,15 @@ # Production Dockerfile for OpenTracker FROM node:20-alpine AS base +ARG TRACKER_HTTP_URL +ARG TRACKER_UDP_URL +ARG TRACKER_WS_URL + +ENV TRACKER_HTTP_URL=${TRACKER_HTTP_URL} +ENV TRACKER_UDP_URL=${TRACKER_UDP_URL} +ENV TRACKER_WS_URL=${TRACKER_WS_URL} + + # Install dependencies only when needed FROM base AS deps RUN apk add --no-cache libc6-compat diff --git a/app/components/TorrentTable.vue b/app/components/TorrentTable.vue index 02e34ab..3f86de4 100644 --- a/app/components/TorrentTable.vue +++ b/app/components/TorrentTable.vue @@ -60,7 +60,7 @@ v-if="torrent.category" class="text-[10px] bg-bg-tertiary border border-border px-1.5 py-0.5 rounded-sm text-text-secondary uppercase font-bold tracking-wider" > - {{ torrent.category.name }} + {{ getCategoryDisplayName(torrent.category) }} @@ -128,6 +128,8 @@ interface TorrentWithStats { }; } +const { data: categories } = await useFetch('/api/categories'); + const props = defineProps<{ torrents: TorrentWithStats[]; compact?: boolean; @@ -138,6 +140,20 @@ const emit = defineEmits<{ deleted: [infoHash: string]; }>(); +function getCategoryDisplayName(category) { + let displayName = category.name; + + const parent = categories.value.find( + (cat) => cat.id === category.parentId + ); + + if (parent) { + displayName = `${parent.name}/${displayName}`; + } + + return displayName; +} + async function deleteTorrent(torrent: TorrentWithStats) { if (!confirm(`Delete "${torrent.name}"?`)) return; diff --git a/app/components/UploadTorrentModal.vue b/app/components/UploadTorrentModal.vue index bc916c0..4171e16 100644 --- a/app/components/UploadTorrentModal.vue +++ b/app/components/UploadTorrentModal.vue @@ -95,7 +95,7 @@ class="input w-full !py-2 text-xs font-bold uppercase tracking-wider" > - @@ -344,6 +344,26 @@ const renderedDescription = computed(() => { return marked.parse(description.value || ''); }); +function getFlattenedCategories( + categories: Array<{ id: string; name: string; subcategories?: any[] }>, + prefix = '' +): Array<{ id: string; name: string }> { + + let result: Array<{ id: string; name: string }> = []; + + for (const category of categories) { + result.push({ id: category.id, name: prefix + category.name }); + + if (category.subcategories) { + result = result.concat( + getFlattenedCategories(category.subcategories, prefix + '╚=> ') + ); + } + } + + return result; +} + function close() { emit('close'); // Reset state after animation diff --git a/app/pages/search.vue b/app/pages/search.vue index 45aba7b..a78852a 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -187,7 +187,7 @@ const { }>('/api/torrents', { query: computed(() => ({ search: searchQuery.value, - category: selectedCategory.value, + categoryId: selectedCategory.value, page: page.value, limit: 20, })), diff --git a/app/pages/torrents/index.vue b/app/pages/torrents/index.vue index cea5503..1f1b7c1 100644 --- a/app/pages/torrents/index.vue +++ b/app/pages/torrents/index.vue @@ -146,7 +146,7 @@ const router = useRouter(); const search = ref((route.query.search as string) || ''); const page = ref(parseInt((route.query.page as string) || '1', 10)); -const selectedCategory = ref((route.query.category as string) || ''); +const selectedCategory = ref((route.query.categoryId as string) || ''); const showUploadModal = ref(false); @@ -160,7 +160,7 @@ const { data, refresh, pending } = await useFetch<{ page: page.value, limit: 25, search: search.value || undefined, - category: selectedCategory.value || undefined, + categoryId: selectedCategory.value || undefined, })), }); @@ -178,7 +178,7 @@ function doSearch() { router.push({ query: { search: search.value || undefined, - category: selectedCategory.value || undefined, + categoryId: selectedCategory.value || undefined, page: undefined, }, }); diff --git a/docker-compose.loadtest.yml b/docker-compose.loadtest.yml index 2814ea6..c2662e9 100644 --- a/docker-compose.loadtest.yml +++ b/docker-compose.loadtest.yml @@ -8,6 +8,10 @@ services: build: context: . dockerfile: Dockerfile + args: + TRACKER_HTTP_URL: ${TRACKER_HTTP_URL:-http://localhost:8080/announce} + TRACKER_UDP_URL: ${TRACKER_UDP_URL:-udp://localhost:8081/announce} + TRACKER_WS_URL: ${TRACKER_WS_URL:-ws://localhost:8082} container_name: trackarr-loadtest restart: unless-stopped environment: diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 40e3e5e..a14f3af 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -14,6 +14,10 @@ services: build: context: . dockerfile: Dockerfile + args: + TRACKER_HTTP_URL: ${TRACKER_HTTP_URL:-http://localhost:8080/announce} + TRACKER_UDP_URL: ${TRACKER_UDP_URL:-udp://localhost:8081/announce} + TRACKER_WS_URL: ${TRACKER_WS_URL:-ws://localhost:8082} container_name: trackarr restart: unless-stopped environment: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b86a134..45dd096 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -47,6 +47,10 @@ services: build: context: . dockerfile: Dockerfile + args: + TRACKER_HTTP_URL: ${TRACKER_HTTP_URL:-http://localhost:8080/announce} + TRACKER_UDP_URL: ${TRACKER_UDP_URL:-udp://localhost:8081/announce} + TRACKER_WS_URL: ${TRACKER_WS_URL:-ws://localhost:8082} container_name: trackarr-app restart: always # Enhanced security: Drop all capabilities and add only required ones diff --git a/docker-compose.yml b/docker-compose.yml index 991279a..d1e364b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,10 @@ services: build: context: . target: base + args: + TRACKER_HTTP_URL: ${TRACKER_HTTP_URL:-http://localhost:8080/announce} + TRACKER_UDP_URL: ${TRACKER_UDP_URL:-udp://localhost:8081/announce} + TRACKER_WS_URL: ${TRACKER_WS_URL:-ws://localhost:8082} container_name: trackarr-app restart: unless-stopped ports: diff --git a/server/api/torrents/index.get.ts b/server/api/torrents/index.get.ts index 5150bc2..91a05d6 100644 --- a/server/api/torrents/index.get.ts +++ b/server/api/torrents/index.get.ts @@ -44,7 +44,17 @@ export default defineEventHandler(async (event) => { } } if (query.categoryId) { - conditions.push(eq(schema.torrents.categoryId, query.categoryId)); + // add category and subcategories filter + const subcategories = await db.query.categories.findMany({ + where: eq(schema.categories.parentId, query.categoryId), + select: { id: true }, + }); + conditions.push( + or( + eq(schema.torrents.categoryId, query.categoryId), + ...subcategories.map((subcat) => eq(schema.torrents.categoryId, subcat.id)) + ) + ); } const whereClause = conditions.length > 0 ? and(...conditions) : undefined;