Skip to content

Commit c2a98be

Browse files
authored
Merge pull request #126 from AnthonyGress/dev
v1.2.3
2 parents 0fcd5cf + 118a891 commit c2a98be

23 files changed

+1530
-133
lines changed

backend/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const PORT = Number(process.env.PORT) || 2022;
1717
const iconsPath = path.join(__dirname, './node_modules/@loganmarchione/homelab-svg-assets/assets');
1818
const iconListPath = path.join(__dirname, './node_modules/@loganmarchione/homelab-svg-assets/icons.json');
1919

20+
// allow running the server behind a proxy
21+
app.set('trust proxy', 1);
22+
2023
// Middleware
2124
app.use(cors({
2225
origin: true,

backend/src/routes/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { configRoute } from './config.route';
77
import { delugeRoute } from './deluge.route';
88
import { healthRoute } from './health.route';
99
import { jellyfinRoute } from './jellyfin.route';
10-
import { jellyseerrRoute } from './jellyseerr.route';
10+
import { jellyseerRoute } from './jellyseerr.route';
11+
import { notesRoute } from './notes.route';
1112
import { piholeV6Route } from './pihole-v6.route';
1213
import { piholeRoute } from './pihole.route';
1314
import { qbittorrentRoute } from './qbittorrent.route';
@@ -52,6 +53,9 @@ router.use('/app-shortcut', apiLimiter, appShortcutRoute);
5253
// Uploads management routes
5354
router.use('/uploads', apiLimiter, uploadsRoute);
5455

56+
// Notes routes
57+
router.use('/notes', apiLimiter, notesRoute);
58+
5559
// Torrent client routes
5660
router.use('/qbittorrent', torrentApiLimiter, qbittorrentRoute);
5761
router.use('/transmission', torrentApiLimiter, transmissionRoute);
@@ -75,7 +79,7 @@ router.use('/deluge', torrentApiLimiter, delugeRoute);
7579
router.use('/jellyfin', apiLimiter, jellyfinRoute);
7680

7781
// Jellyseerr routes
78-
router.use('/jellyseerr', apiLimiter, jellyseerrRoute);
82+
router.use('/jellyseerr', apiLimiter, jellyseerRoute);
7983

8084
// Sonarr routes
8185
router.use('/sonarr', apiLimiter, sonarrRoute);

backend/src/routes/jellyseerr.route.ts

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import https from 'https';
55
import { getItemConnectionInfo } from '../utils/config-lookup';
66
import { decrypt, isEncrypted } from '../utils/crypto';
77

8-
export const jellyseerrRoute = Router();
8+
export const jellyseerRoute = Router();
99

1010
// Configure HTTPS agent to allow self-signed certificates
1111
const httpsAgent = new https.Agent({
@@ -53,7 +53,7 @@ const getApiKey = (req: Request): string | null => {
5353
};
5454

5555
// Search for movies and TV shows
56-
jellyseerrRoute.get('/search', async (req: Request, res: Response) => {
56+
jellyseerRoute.get('/search', async (req: Request, res: Response) => {
5757
console.log('Jellyseerr search request');
5858
try {
5959
const baseUrl = getBaseUrl(req);
@@ -76,12 +76,6 @@ jellyseerrRoute.get('/search', async (req: Request, res: Response) => {
7676
return;
7777
}
7878

79-
console.log('Jellyseerr search request:', {
80-
baseUrl,
81-
query,
82-
hasApiKey: !!apiKey
83-
});
84-
8579
// Simple search call without pagination for now
8680
const encodedQuery = encodeURIComponent(query.trim());
8781
const response = await axios.get(`${baseUrl}/api/v1/search?query=${encodedQuery}`, {
@@ -92,9 +86,6 @@ jellyseerrRoute.get('/search', async (req: Request, res: Response) => {
9286
httpsAgent: httpsAgent
9387
});
9488

95-
console.log('Jellyseerr search response status:', response.status);
96-
console.log('Jellyseerr search results count:', response.data?.results?.length || 0);
97-
9889
res.json({
9990
success: true,
10091
data: response.data
@@ -104,13 +95,13 @@ jellyseerrRoute.get('/search', async (req: Request, res: Response) => {
10495
console.error('Jellyseerr search error:', error.message);
10596
res.status(error.response?.status || 500).json({
10697
success: false,
107-
error: error.response?.data?.message || error.message || 'Failed to search Jellyseerr'
98+
error: error.response?.data?.message || error.message || 'Failed to search'
10899
});
109100
}
110101
});
111102

112103
// Get pending requests
113-
jellyseerrRoute.get('/requests', async (req: Request, res: Response) => {
104+
jellyseerRoute.get('/requests', async (req: Request, res: Response) => {
114105
console.log('Jellyseerr requests request');
115106
try {
116107
const baseUrl = getBaseUrl(req);
@@ -189,7 +180,7 @@ jellyseerrRoute.get('/requests', async (req: Request, res: Response) => {
189180
});
190181

191182
// Request a movie or TV show
192-
jellyseerrRoute.post('/request', async (req: Request, res: Response) => {
183+
jellyseerRoute.post('/request', async (req: Request, res: Response) => {
193184
console.log('Jellyseerr request creation');
194185
try {
195186
const baseUrl = getBaseUrl(req);
@@ -218,10 +209,18 @@ jellyseerrRoute.post('/request', async (req: Request, res: Response) => {
218209
};
219210

220211
// Add seasons for TV shows
221-
if (mediaType === 'tv' && seasons) {
222-
requestBody.seasons = seasons;
212+
if (mediaType === 'tv') {
213+
if (seasons && seasons.length > 0) {
214+
requestBody.seasons = seasons;
215+
} else {
216+
// If no seasons specified, don't include the seasons field
217+
// This will let Jellyseerr handle the default behavior
218+
console.warn('No seasons specified for TV show request');
219+
}
223220
}
224221

222+
console.log('Jellyseerr API URL:', `${baseUrl}/api/v1/request`);
223+
225224
const response = await axios.post(`${baseUrl}/api/v1/request`, requestBody, {
226225
headers: {
227226
'X-Api-Key': apiKey,
@@ -238,6 +237,7 @@ jellyseerrRoute.post('/request', async (req: Request, res: Response) => {
238237

239238
} catch (error: any) {
240239
console.error('Jellyseerr request creation error:', error.message);
240+
console.error('Error response status:', error.response?.status);
241241
res.status(error.response?.status || 500).json({
242242
success: false,
243243
error: error.response?.data?.message || error.message || 'Failed to create request'
@@ -246,7 +246,7 @@ jellyseerrRoute.post('/request', async (req: Request, res: Response) => {
246246
});
247247

248248
// Approve a request
249-
jellyseerrRoute.post('/request/:id/approve', async (req: Request, res: Response) => {
249+
jellyseerRoute.post('/request/:id/approve', async (req: Request, res: Response) => {
250250
console.log('Jellyseerr request approval');
251251
try {
252252
const baseUrl = getBaseUrl(req);
@@ -285,7 +285,7 @@ jellyseerrRoute.post('/request/:id/approve', async (req: Request, res: Response)
285285
});
286286

287287
// Decline a request
288-
jellyseerrRoute.post('/request/:id/decline', async (req: Request, res: Response) => {
288+
jellyseerRoute.post('/request/:id/decline', async (req: Request, res: Response) => {
289289
console.log('Jellyseerr request decline');
290290
try {
291291
const baseUrl = getBaseUrl(req);
@@ -323,8 +323,54 @@ jellyseerrRoute.post('/request/:id/decline', async (req: Request, res: Response)
323323
}
324324
});
325325

326+
// Get TV show details including seasons
327+
jellyseerRoute.get('/tv/:tmdbId', async (req: Request, res: Response) => {
328+
console.log('Jellyseerr TV show details request');
329+
try {
330+
const baseUrl = getBaseUrl(req);
331+
const apiKey = getApiKey(req);
332+
const { tmdbId } = req.params;
333+
334+
if (!apiKey) {
335+
res.status(400).json({
336+
success: false,
337+
error: 'API key is required or could not be decrypted'
338+
});
339+
return;
340+
}
341+
342+
if (!tmdbId) {
343+
res.status(400).json({
344+
success: false,
345+
error: 'tmdbId parameter is required'
346+
});
347+
return;
348+
}
349+
350+
const response = await axios.get(`${baseUrl}/api/v1/tv/${tmdbId}`, {
351+
headers: {
352+
'X-Api-Key': apiKey
353+
},
354+
timeout: 10000,
355+
httpsAgent: httpsAgent
356+
});
357+
358+
res.json({
359+
success: true,
360+
data: response.data
361+
});
362+
363+
} catch (error: any) {
364+
console.error('Jellyseerr TV show details error:', error.message);
365+
res.status(error.response?.status || 500).json({
366+
success: false,
367+
error: error.response?.data?.message || error.message || 'Failed to get TV show details'
368+
});
369+
}
370+
});
371+
326372
// Get system status
327-
jellyseerrRoute.get('/status', async (req: Request, res: Response) => {
373+
jellyseerRoute.get('/status', async (req: Request, res: Response) => {
328374
console.log('Jellyseerr status request');
329375
try {
330376
const baseUrl = getBaseUrl(req);

backend/src/routes/notes.route.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import express, { Request, Response } from 'express';
2+
import fs from 'fs';
3+
import path from 'path';
4+
5+
import { authenticateToken } from '../middleware/auth.middleware';
6+
import { Note } from '../types';
7+
8+
export const notesRoute = express.Router();
9+
10+
// Path to the config file
11+
const CONFIG_FILE = path.join(__dirname, '../config/config.json');
12+
13+
// Helper function to load config from file
14+
const loadConfig = (): any => {
15+
try {
16+
if (fs.existsSync(CONFIG_FILE)) {
17+
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
18+
return JSON.parse(data);
19+
}
20+
return { layout: { desktop: [], mobile: [] }, notes: [] };
21+
} catch (error) {
22+
console.error('Error reading config file:', error);
23+
return { layout: { desktop: [], mobile: [] }, notes: [] };
24+
}
25+
};
26+
27+
// Helper function to save config to file
28+
const saveConfig = (config: any): void => {
29+
try {
30+
// Ensure the config directory exists
31+
const configDir = path.dirname(CONFIG_FILE);
32+
if (!fs.existsSync(configDir)) {
33+
fs.mkdirSync(configDir, { recursive: true });
34+
}
35+
36+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
37+
} catch (error) {
38+
console.error('Error writing config file:', error);
39+
throw new Error('Failed to save config');
40+
}
41+
};
42+
43+
// Helper function to read notes from config
44+
const readNotes = (): Note[] => {
45+
try {
46+
const config = loadConfig();
47+
return config.notes || [];
48+
} catch (error) {
49+
console.error('Error reading notes from config:', error);
50+
return [];
51+
}
52+
};
53+
54+
// Helper function to write notes to config
55+
const writeNotes = (notes: Note[]): void => {
56+
try {
57+
const config = loadConfig();
58+
config.notes = notes;
59+
saveConfig(config);
60+
} catch (error) {
61+
console.error('Error writing notes to config:', error);
62+
throw new Error('Failed to save notes');
63+
}
64+
};
65+
66+
// GET /api/notes - Get all notes
67+
notesRoute.get('/', (req: Request, res: Response) => {
68+
try {
69+
const notes = readNotes();
70+
// Sort by updatedAt descending (most recent first)
71+
notes.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
72+
res.json(notes);
73+
} catch (error) {
74+
console.error('Error fetching notes:', error);
75+
res.status(500).json({ error: 'Failed to fetch notes' });
76+
}
77+
});
78+
79+
// POST /api/notes - Create a new note
80+
notesRoute.post('/', authenticateToken, (req: Request, res: Response) => {
81+
try {
82+
const { id, title, content } = req.body;
83+
84+
if (!id || typeof id !== 'string') {
85+
res.status(400).json({ error: 'ID is required and must be a string' });
86+
return;
87+
}
88+
89+
if (!title || typeof title !== 'string') {
90+
res.status(400).json({ error: 'Title is required and must be a string' });
91+
return;
92+
}
93+
94+
const notes = readNotes();
95+
96+
// Check if ID already exists
97+
if (notes.some(note => note.id === id)) {
98+
res.status(409).json({ error: 'Note with this ID already exists' });
99+
return;
100+
}
101+
102+
const now = new Date().toISOString();
103+
104+
const newNote: Note = {
105+
id: id,
106+
title: title.trim(),
107+
content: (content || '').trim(),
108+
createdAt: now,
109+
updatedAt: now
110+
};
111+
112+
notes.push(newNote);
113+
writeNotes(notes);
114+
115+
res.status(201).json(newNote);
116+
} catch (error) {
117+
console.error('Error creating note:', error);
118+
res.status(500).json({ error: 'Failed to create note' });
119+
}
120+
});
121+
122+
// PUT /api/notes/:id - Update an existing note
123+
notesRoute.put('/:id', authenticateToken, (req: Request, res: Response) => {
124+
try {
125+
const { id } = req.params;
126+
const { title, content } = req.body;
127+
128+
if (!title || typeof title !== 'string') {
129+
res.status(400).json({ error: 'Title is required and must be a string' });
130+
return;
131+
}
132+
133+
const notes = readNotes();
134+
const noteIndex = notes.findIndex(note => note.id === id);
135+
136+
if (noteIndex === -1) {
137+
res.status(404).json({ error: 'Note not found' });
138+
return;
139+
}
140+
141+
const updatedNote: Note = {
142+
...notes[noteIndex],
143+
title: title.trim(),
144+
content: (content || '').trim(),
145+
updatedAt: new Date().toISOString()
146+
};
147+
148+
notes[noteIndex] = updatedNote;
149+
writeNotes(notes);
150+
151+
res.json(updatedNote);
152+
} catch (error) {
153+
console.error('Error updating note:', error);
154+
res.status(500).json({ error: 'Failed to update note' });
155+
}
156+
});
157+
158+
// DELETE /api/notes/:id - Delete a note
159+
notesRoute.delete('/:id', authenticateToken, (req: Request, res: Response) => {
160+
try {
161+
const { id } = req.params;
162+
const notes = readNotes();
163+
const noteIndex = notes.findIndex(note => note.id === id);
164+
165+
if (noteIndex === -1) {
166+
res.status(404).json({ error: 'Note not found' });
167+
return;
168+
}
169+
170+
notes.splice(noteIndex, 1);
171+
writeNotes(notes);
172+
173+
res.status(204).send();
174+
} catch (error) {
175+
console.error('Error deleting note:', error);
176+
res.status(500).json({ error: 'Failed to delete note' });
177+
}
178+
});

0 commit comments

Comments
 (0)