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;