Skip to content

Commit 6b32f39

Browse files
authored
Merge pull request #1085 from fllppi/feature/1019-channel-folder-template
feat: add configurable channel folder template for storage paths
2 parents a2d6409 + bfcea86 commit 6b32f39

File tree

13 files changed

+632
-116
lines changed

13 files changed

+632
-116
lines changed

frontend/app/admin/settings/page.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const AdminSettingsPage = () => {
5454
storage_templates: {
5555
folder_template: data?.storage_templates.folder_template || "",
5656
file_template: data?.storage_templates.file_template || "",
57+
channel_folder_template: data?.storage_templates.channel_folder_template || "{{channel}}",
5758
},
5859
livestream: {
5960
proxies: data?.livestream.proxies || [],
@@ -67,8 +68,16 @@ const AdminSettingsPage = () => {
6768
useEffect(() => {
6869
if (!data || !form) return
6970

70-
form.setValues(data)
71-
form.resetDirty(data)
71+
const dataWithDefaults = {
72+
...data,
73+
storage_templates: {
74+
...data.storage_templates,
75+
channel_folder_template: data.storage_templates.channel_folder_template || "{{channel}}",
76+
},
77+
}
78+
79+
form.setValues(dataWithDefaults)
80+
form.resetDirty(dataWithDefaults)
7281

7382
// eslint-disable-next-line react-hooks/exhaustive-deps
7483
}, [data])
@@ -191,8 +200,20 @@ const AdminSettingsPage = () => {
191200
<Text mb={10}>
192201
{t('archiveSettings.storageTemplateSettingsDescription')}
193202
</Text>
203+
204+
<div>
205+
<Title order={4}>Channel Folder Template</Title>
206+
207+
<Textarea
208+
description="Controls the top-level channel directory name. Use {{channel_id}} for a stable identifier that won't change when a channel renames."
209+
key={form.key('storage_templates.channel_folder_template')}
210+
{...form.getInputProps('storage_templates.channel_folder_template')}
211+
required
212+
/>
213+
</div>
214+
194215
<div>
195-
<Title order={4}>{t('archiveSettings.folderTemplateText')}</Title>
216+
<Title mt={10} order={4}>{t('archiveSettings.folderTemplateText')}</Title>
196217

197218
<Textarea
198219
description="{{uuid}} is required to be present for the folder template."
@@ -216,19 +237,24 @@ const AdminSettingsPage = () => {
216237
</div>
217238

218239
<div>
219-
<Title mt={5} order={4}>
240+
<Title mt={10} order={4}>
220241
Available Variables
221242
</Title>
222243

223244
<div>
224-
<Text>Ganymede</Text>
245+
<Text fw={600}>Channel (all templates)</Text>
246+
<ul>
247+
<li><Code>{"{{channel}}"}</Code>: Channel login name (lowercase username)</li>
248+
<li><Code>{"{{channel_id}}"}</Code>: External platform ID (e.g., Twitch User ID) - stable, never changes</li>
249+
<li><Code>{"{{channel_display_name}}"}</Code>: Channel display name</li>
250+
</ul>
251+
<Text fw={600}>Ganymede (folder &amp; file templates only)</Text>
225252
<ul>
226253
<li><Code>{"{{uuid}}"}</Code>: Unique identifier for the archive</li>
227254
</ul>
228-
<Text>Video</Text>
255+
<Text fw={600}>Video (folder &amp; file templates only)</Text>
229256
<ul>
230257
<li><Code>{"{{id}}"}</Code>: Video ID</li>
231-
<li><Code>{"{{channel}}"}</Code>: Channel name</li>
232258
<li><Code>{"{{title}}"}</Code>: Video title (file safe)</li>
233259
<li><Code>{"{{type}}"}</Code>: Video type (live, archive, highlight)</li>
234260
<li><Code>{"{{date}}"}</Code>: Formatted date (YYYY-MM-DD)</li>
@@ -237,7 +263,6 @@ const AdminSettingsPage = () => {
237263
<li><Code>{"{{DD}}"}</Code>: Day</li>
238264
<li><Code>{"{{HH}}"}</Code>: Hour</li>
239265
</ul>
240-
241266
</div>
242267
</div>
243268

@@ -246,10 +271,14 @@ const AdminSettingsPage = () => {
246271
{t('archiveSettings.examplesText')}
247272
</Title>
248273

249-
<Text>Folder</Text>
274+
<Text>Channel Folder</Text>
275+
<Code block>{"{{channel}}"}</Code>
276+
<Code block>{"{{channel_id}}"}</Code>
277+
<Code block>{"{{channel_id}}_{{channel}}"}</Code>
278+
<Text mt={5}>VOD Folder</Text>
250279
<Code block>{folderExample1}</Code>
251280
<Code block>{folderExample2}</Code>
252-
<Text>File</Text>
281+
<Text mt={5}>File</Text>
253282
<Code block>{fileExample1}</Code>
254283
</div>
255284

frontend/app/hooks/useConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface Config {
2929
export interface StorageTemplate {
3030
folder_template: string;
3131
file_template: string;
32+
channel_folder_template: string;
3233
}
3334

3435
export enum ProxyType {

internal/archive/archive.go

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,25 @@ func (s *Service) ArchiveChannel(ctx context.Context, channelName string) (*ent.
6363
return nil, fmt.Errorf("channel already exists")
6464
}
6565

66+
// Resolve channel folder name from template
67+
channelFolderName, err := GetChannelFolderName(ChannelTemplateInput{
68+
ChannelName: platformChannel.Login,
69+
ChannelID: platformChannel.ID,
70+
ChannelDisplayName: platformChannel.DisplayName,
71+
})
72+
if err != nil {
73+
log.Warn().Err(err).Msg("error resolving channel folder template, falling back to channel login name")
74+
channelFolderName = platformChannel.Login
75+
}
76+
6677
// Create channel folder
67-
err = utils.CreateDirectory(fmt.Sprintf("%s/%s", env.VideosDir, platformChannel.Login))
78+
err = utils.CreateDirectory(fmt.Sprintf("%s/%s", env.VideosDir, channelFolderName))
6879
if err != nil {
6980
return nil, fmt.Errorf("error creating channel folder: %v", err)
7081
}
7182

7283
// Download channel profile image
73-
err = utils.DownloadFile(platformChannel.ProfileImageURL, fmt.Sprintf("%s/%s/%s", env.VideosDir, platformChannel.Login, "profile.png"))
84+
err = utils.DownloadFile(platformChannel.ProfileImageURL, fmt.Sprintf("%s/%s/%s", env.VideosDir, channelFolderName, "profile.png"))
7485
if err != nil {
7586
log.Error().Err(err).Msg("error downloading channel profile image")
7687
}
@@ -80,7 +91,7 @@ func (s *Service) ArchiveChannel(ctx context.Context, channelName string) (*ent.
8091
ExtID: platformChannel.ID,
8192
Name: platformChannel.Login,
8293
DisplayName: platformChannel.DisplayName,
83-
ImagePath: fmt.Sprintf("%s/%s/profile.png", env.VideosDir, platformChannel.Login),
94+
ImagePath: fmt.Sprintf("%s/%s/profile.png", env.VideosDir, channelFolderName),
8495
}
8596

8697
dbC, err := s.ChannelService.CreateChannel(channelDTO)
@@ -156,17 +167,30 @@ func (s *Service) ArchiveVideo(ctx context.Context, input ArchiveVideoInput) (*A
156167
return nil, fmt.Errorf("error creating vod uuid: %v", err)
157168
}
158169

170+
// Resolve channel folder name from template
171+
channelFolderName, err := GetChannelFolderName(ChannelTemplateInput{
172+
ChannelName: channel.Name,
173+
ChannelID: channel.ExtID,
174+
ChannelDisplayName: channel.DisplayName,
175+
})
176+
if err != nil {
177+
log.Warn().Err(err).Msg("error resolving channel folder template, falling back to channel login name")
178+
channelFolderName = channel.Name
179+
}
180+
159181
storageTemplateInput := StorageTemplateInput{
160-
UUID: vUUID,
161-
ID: input.VideoId,
162-
Channel: channel.Name,
163-
Title: video.Title,
164-
Type: video.Type,
165-
Date: video.CreatedAt.Format("2006-01-02"),
166-
YYYY: video.CreatedAt.Format("2006"),
167-
MM: video.CreatedAt.Format("01"),
168-
DD: video.CreatedAt.Format("02"),
169-
HH: video.CreatedAt.Format("15"),
182+
UUID: vUUID,
183+
ID: input.VideoId,
184+
Channel: channel.Name,
185+
ChannelID: channel.ExtID,
186+
ChannelDisplayName: channel.DisplayName,
187+
Title: video.Title,
188+
Type: video.Type,
189+
Date: video.CreatedAt.Format("2006-01-02"),
190+
YYYY: video.CreatedAt.Format("2006"),
191+
MM: video.CreatedAt.Format("01"),
192+
DD: video.CreatedAt.Format("02"),
193+
HH: video.CreatedAt.Format("15"),
170194
}
171195
// Create directory paths
172196
folderName, err := GetFolderName(vUUID, storageTemplateInput)
@@ -181,7 +205,7 @@ func (s *Service) ArchiveVideo(ctx context.Context, input ArchiveVideoInput) (*A
181205
}
182206

183207
// set facts
184-
rootVideoPath := fmt.Sprintf("%s/%s/%s", envConfig.VideosDir, video.UserLogin, folderName)
208+
rootVideoPath := fmt.Sprintf("%s/%s/%s", envConfig.VideosDir, channelFolderName, folderName)
185209
chatPath := ""
186210
chatVideoPath := ""
187211
liveChatPath := ""
@@ -355,17 +379,30 @@ func (s *Service) ArchiveClip(ctx context.Context, input ArchiveClipInput) (*Arc
355379
return nil, fmt.Errorf("error creating vod uuid: %v", err)
356380
}
357381

382+
// Resolve channel folder name from template
383+
channelFolderName, err := GetChannelFolderName(ChannelTemplateInput{
384+
ChannelName: channel.Name,
385+
ChannelID: channel.ExtID,
386+
ChannelDisplayName: channel.DisplayName,
387+
})
388+
if err != nil {
389+
log.Warn().Err(err).Msg("error resolving channel folder template, falling back to channel login name")
390+
channelFolderName = channel.Name
391+
}
392+
358393
storageTemplateInput := StorageTemplateInput{
359-
UUID: vUUID,
360-
ID: clip.ID,
361-
Channel: channel.Name,
362-
Title: clip.Title,
363-
Type: string(utils.Clip),
364-
Date: clip.CreatedAt.Format("2006-01-02"),
365-
YYYY: clip.CreatedAt.Format("2006"),
366-
MM: clip.CreatedAt.Format("01"),
367-
DD: clip.CreatedAt.Format("02"),
368-
HH: clip.CreatedAt.Format("15"),
394+
UUID: vUUID,
395+
ID: clip.ID,
396+
Channel: channel.Name,
397+
ChannelID: channel.ExtID,
398+
ChannelDisplayName: channel.DisplayName,
399+
Title: clip.Title,
400+
Type: string(utils.Clip),
401+
Date: clip.CreatedAt.Format("2006-01-02"),
402+
YYYY: clip.CreatedAt.Format("2006"),
403+
MM: clip.CreatedAt.Format("01"),
404+
DD: clip.CreatedAt.Format("02"),
405+
HH: clip.CreatedAt.Format("15"),
369406
}
370407
// Create directory paths
371408
folderName, err := GetFolderName(vUUID, storageTemplateInput)
@@ -380,7 +417,7 @@ func (s *Service) ArchiveClip(ctx context.Context, input ArchiveClipInput) (*Arc
380417
}
381418

382419
// set facts
383-
rootVideoPath := fmt.Sprintf("%s/%s/%s", envConfig.VideosDir, channel.Name, folderName)
420+
rootVideoPath := fmt.Sprintf("%s/%s/%s", envConfig.VideosDir, channelFolderName, folderName)
384421
chatPath := ""
385422
chatVideoPath := ""
386423
liveChatPath := ""
@@ -518,17 +555,30 @@ func (s *Service) ArchiveLivestream(ctx context.Context, input ArchiveVideoInput
518555
return nil, fmt.Errorf("error creating vod uuid: %v", err)
519556
}
520557

558+
// Resolve channel folder name from template
559+
channelFolderName, err := GetChannelFolderName(ChannelTemplateInput{
560+
ChannelName: channel.Name,
561+
ChannelID: channel.ExtID,
562+
ChannelDisplayName: channel.DisplayName,
563+
})
564+
if err != nil {
565+
log.Warn().Err(err).Msg("error resolving channel folder template, falling back to channel login name")
566+
channelFolderName = channel.Name
567+
}
568+
521569
storageTemplateInput := StorageTemplateInput{
522-
UUID: vUUID,
523-
ID: video.ID,
524-
Channel: channel.Name,
525-
Title: video.Title,
526-
Type: video.Type,
527-
Date: video.StartedAt.Format("2006-01-02"),
528-
YYYY: video.StartedAt.Format("2006"),
529-
MM: video.StartedAt.Format("01"),
530-
DD: video.StartedAt.Format("02"),
531-
HH: video.StartedAt.Format("15"),
570+
UUID: vUUID,
571+
ID: video.ID,
572+
Channel: channel.Name,
573+
ChannelID: channel.ExtID,
574+
ChannelDisplayName: channel.DisplayName,
575+
Title: video.Title,
576+
Type: video.Type,
577+
Date: video.StartedAt.Format("2006-01-02"),
578+
YYYY: video.StartedAt.Format("2006"),
579+
MM: video.StartedAt.Format("01"),
580+
DD: video.StartedAt.Format("02"),
581+
HH: video.StartedAt.Format("15"),
532582
}
533583
// Create directory paths
534584
folderName, err := GetFolderName(vUUID, storageTemplateInput)
@@ -543,7 +593,7 @@ func (s *Service) ArchiveLivestream(ctx context.Context, input ArchiveVideoInput
543593
}
544594

545595
// set facts
546-
rootVideoPath := fmt.Sprintf("%s/%s/%s", envConfig.VideosDir, video.UserLogin, folderName)
596+
rootVideoPath := fmt.Sprintf("%s/%s/%s", envConfig.VideosDir, channelFolderName, folderName)
547597
chatPath := ""
548598
chatVideoPath := ""
549599
liveChatPath := ""

0 commit comments

Comments
 (0)