Add configurable allowed file types
This commit is contained in:
@@ -214,6 +214,7 @@ func triggerAutoCopy(cp *copier.Copier, cfg *config.Config, info disk.DiskInfo)
|
||||
MediaPath: cfg.MediaPath,
|
||||
DestFolder: cfg.DestFolder,
|
||||
SourceRules: cfg.Sources,
|
||||
AllowedExtensions: cfg.EffectiveAllowedExtensions(),
|
||||
ReserveFreeGB: cfg.ReserveFreeGB,
|
||||
OverwriteMode: cfg.OverwriteMode,
|
||||
FileSelectMode: cfg.FileSelectMode,
|
||||
|
||||
@@ -19,6 +19,7 @@ func (s *Server) copyOptions(cfg *config.Config, diskInfo disk.DiskInfo, overwri
|
||||
MediaPath: cfg.MediaPath,
|
||||
DestFolder: cfg.DestFolder,
|
||||
SourceRules: cfg.Sources,
|
||||
AllowedExtensions: cfg.EffectiveAllowedExtensions(),
|
||||
ReserveFreeGB: cfg.ReserveFreeGB,
|
||||
OverwriteMode: overwriteMode,
|
||||
FileSelectMode: cfg.FileSelectMode,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
type OverwriteMode string
|
||||
type FileSelectMode string
|
||||
type AllowedFilesMode string
|
||||
|
||||
const (
|
||||
DefaultDestFolder = "media"
|
||||
@@ -21,6 +22,13 @@ const (
|
||||
|
||||
SelectNew FileSelectMode = "new"
|
||||
SelectAll FileSelectMode = "all"
|
||||
|
||||
AllowedFilesByMediaType AllowedFilesMode = "media_types"
|
||||
AllowedFilesByExtensions AllowedFilesMode = "extensions"
|
||||
|
||||
MediaTypeAudio = "audio"
|
||||
MediaTypeVideo = "video"
|
||||
MediaTypePhoto = "photo"
|
||||
)
|
||||
|
||||
type SourceFolder struct {
|
||||
@@ -36,6 +44,9 @@ type Config struct {
|
||||
Sources []SourceFolder `json:"sources"`
|
||||
OverwriteMode OverwriteMode `json:"overwrite_mode"`
|
||||
FileSelectMode FileSelectMode `json:"file_select_mode"`
|
||||
AllowedFilesMode AllowedFilesMode `json:"allowed_files_mode"`
|
||||
EnabledMediaTypes []string `json:"enabled_media_types,omitempty"`
|
||||
AllowedExtensions []string `json:"allowed_extensions,omitempty"`
|
||||
AutoCopy bool `json:"auto_copy"`
|
||||
FileReplicaCounts map[string]int `json:"file_replica_counts,omitempty"`
|
||||
DiskReplicaFiles map[string][]string `json:"disk_replica_files,omitempty"`
|
||||
@@ -47,6 +58,9 @@ func defaults() Config {
|
||||
DestFolder: DefaultDestFolder,
|
||||
OverwriteMode: OverwriteSkip,
|
||||
FileSelectMode: SelectNew,
|
||||
AllowedFilesMode: AllowedFilesByMediaType,
|
||||
EnabledMediaTypes: DefaultEnabledMediaTypes(),
|
||||
AllowedExtensions: DefaultAllowedExtensions(),
|
||||
AutoCopy: false,
|
||||
}
|
||||
}
|
||||
@@ -69,6 +83,17 @@ func Load(path string) (*Config, error) {
|
||||
} else {
|
||||
cfg.DestFolder = defaults().DestFolder
|
||||
}
|
||||
if cfg.AllowedFilesMode != AllowedFilesByMediaType && cfg.AllowedFilesMode != AllowedFilesByExtensions {
|
||||
cfg.AllowedFilesMode = defaults().AllowedFilesMode
|
||||
}
|
||||
cfg.EnabledMediaTypes = NormalizeMediaTypes(cfg.EnabledMediaTypes)
|
||||
if len(cfg.EnabledMediaTypes) == 0 {
|
||||
cfg.EnabledMediaTypes = defaults().EnabledMediaTypes
|
||||
}
|
||||
cfg.AllowedExtensions = NormalizeExtensions(cfg.AllowedExtensions)
|
||||
if len(cfg.AllowedExtensions) == 0 {
|
||||
cfg.AllowedExtensions = defaults().AllowedExtensions
|
||||
}
|
||||
cfg.MediaPath = NormalizeMediaPath(cfg.MediaPath)
|
||||
cfg.Sources = NormalizeSources(cfg.Sources, cfg.MediaPath)
|
||||
return &cfg, nil
|
||||
@@ -126,9 +151,145 @@ func (c *Config) Validate() error {
|
||||
default:
|
||||
return errors.New("file_select_mode must be 'new' or 'all'")
|
||||
}
|
||||
switch c.AllowedFilesMode {
|
||||
case "", AllowedFilesByMediaType:
|
||||
c.AllowedFilesMode = AllowedFilesByMediaType
|
||||
c.EnabledMediaTypes = NormalizeMediaTypes(c.EnabledMediaTypes)
|
||||
if len(c.EnabledMediaTypes) == 0 {
|
||||
return errors.New("enabled_media_types must contain at least one of: audio, video, photo")
|
||||
}
|
||||
case AllowedFilesByExtensions:
|
||||
c.AllowedExtensions = NormalizeExtensions(c.AllowedExtensions)
|
||||
if len(c.AllowedExtensions) == 0 {
|
||||
return errors.New("allowed_extensions must contain at least one file extension")
|
||||
}
|
||||
default:
|
||||
return errors.New("allowed_files_mode must be 'media_types' or 'extensions'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultEnabledMediaTypes() []string {
|
||||
return []string{MediaTypeAudio, MediaTypeVideo}
|
||||
}
|
||||
|
||||
func DefaultAllowedExtensions() []string {
|
||||
return extensionsForMediaTypes(DefaultEnabledMediaTypes())
|
||||
}
|
||||
|
||||
func BuiltInMediaTypeExtensions() map[string][]string {
|
||||
return map[string][]string{
|
||||
MediaTypeAudio: {
|
||||
".aac", ".aif", ".aiff", ".alac", ".ape", ".flac", ".m4a", ".mp2", ".mp3", ".ogg", ".opus", ".wav", ".wma",
|
||||
},
|
||||
MediaTypeVideo: {
|
||||
".3gp", ".avi", ".m2ts", ".m4v", ".mkv", ".mov", ".mp4", ".mpeg", ".mpg", ".mts", ".ts", ".webm", ".wmv",
|
||||
},
|
||||
MediaTypePhoto: {
|
||||
".bmp", ".gif", ".heic", ".heif", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeMediaTypes(items []string) []string {
|
||||
order := []string{MediaTypeAudio, MediaTypeVideo, MediaTypePhoto}
|
||||
allowed := make(map[string]struct{}, len(order))
|
||||
for _, item := range order {
|
||||
allowed[item] = struct{}{}
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
result := make([]string, 0, len(order))
|
||||
for _, item := range items {
|
||||
value := strings.ToLower(strings.TrimSpace(item))
|
||||
if _, ok := allowed[value]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
}
|
||||
for _, item := range order {
|
||||
if _, ok := seen[item]; ok {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NormalizeExtensions(items []string) []string {
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
value := normalizeExtension(item)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeExtension(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
value = strings.TrimPrefix(value, "*")
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(value, ".") {
|
||||
value = "." + value
|
||||
}
|
||||
if len(value) < 2 {
|
||||
return ""
|
||||
}
|
||||
for _, ch := range value[1:] {
|
||||
switch {
|
||||
case ch >= 'a' && ch <= 'z':
|
||||
case ch >= '0' && ch <= '9':
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (c Config) EffectiveAllowedExtensions() []string {
|
||||
switch c.AllowedFilesMode {
|
||||
case AllowedFilesByExtensions:
|
||||
if items := NormalizeExtensions(c.AllowedExtensions); len(items) > 0 {
|
||||
return items
|
||||
}
|
||||
default:
|
||||
types := NormalizeMediaTypes(c.EnabledMediaTypes)
|
||||
if len(types) == 0 {
|
||||
types = DefaultEnabledMediaTypes()
|
||||
}
|
||||
return extensionsForMediaTypes(types)
|
||||
}
|
||||
return DefaultAllowedExtensions()
|
||||
}
|
||||
|
||||
func extensionsForMediaTypes(items []string) []string {
|
||||
sets := BuiltInMediaTypeExtensions()
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, mediaType := range NormalizeMediaTypes(items) {
|
||||
for _, ext := range sets[mediaType] {
|
||||
if _, ok := seen[ext]; ok {
|
||||
continue
|
||||
}
|
||||
seen[ext] = struct{}{}
|
||||
result = append(result, ext)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NormalizeMediaPath(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultsAllowedFiles(t *testing.T) {
|
||||
cfg := defaults()
|
||||
|
||||
if cfg.AllowedFilesMode != AllowedFilesByMediaType {
|
||||
t.Fatalf("allowed files mode = %q, want %q", cfg.AllowedFilesMode, AllowedFilesByMediaType)
|
||||
}
|
||||
|
||||
wantTypes := []string{MediaTypeAudio, MediaTypeVideo}
|
||||
if len(cfg.EnabledMediaTypes) != len(wantTypes) {
|
||||
t.Fatalf("enabled media types len = %d, want %d", len(cfg.EnabledMediaTypes), len(wantTypes))
|
||||
}
|
||||
for i, want := range wantTypes {
|
||||
if cfg.EnabledMediaTypes[i] != want {
|
||||
t.Fatalf("enabled media types[%d] = %q, want %q", i, cfg.EnabledMediaTypes[i], want)
|
||||
}
|
||||
}
|
||||
|
||||
exts := cfg.EffectiveAllowedExtensions()
|
||||
if containsString(exts, ".jpg") {
|
||||
t.Fatalf("default extensions unexpectedly include photo files: %v", exts)
|
||||
}
|
||||
if !containsString(exts, ".mp3") || !containsString(exts, ".mp4") {
|
||||
t.Fatalf("default extensions = %v, want audio/video entries", exts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAllowedFilesModeExtensions(t *testing.T) {
|
||||
cfg := defaults()
|
||||
cfg.AllowedFilesMode = AllowedFilesByExtensions
|
||||
cfg.AllowedExtensions = []string{" mp3 ", ".MP4", "*.jpg", ".mp3"}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{".mp3", ".mp4", ".jpg"}
|
||||
if len(cfg.AllowedExtensions) != len(want) {
|
||||
t.Fatalf("allowed extensions len = %d, want %d", len(cfg.AllowedExtensions), len(want))
|
||||
}
|
||||
for i, item := range want {
|
||||
if cfg.AllowedExtensions[i] != item {
|
||||
t.Fatalf("allowed extensions[%d] = %q, want %q", i, cfg.AllowedExtensions[i], item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsEmptyAllowedFiles(t *testing.T) {
|
||||
cfg := defaults()
|
||||
cfg.AllowedFilesMode = AllowedFilesByExtensions
|
||||
cfg.AllowedExtensions = nil
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Validate() error = nil, want non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func containsString(items []string, want string) bool {
|
||||
for _, item := range items {
|
||||
if item == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -27,6 +27,7 @@ type Options struct {
|
||||
MediaPath string
|
||||
DestFolder string // subfolder on disk, default "media"
|
||||
SourceRules []config.SourceFolder
|
||||
AllowedExtensions []string
|
||||
ReserveFreeGB float64
|
||||
OverwriteMode config.OverwriteMode
|
||||
FileSelectMode config.FileSelectMode
|
||||
@@ -241,7 +242,7 @@ func (c *Copier) run(ctx context.Context, taskID string, opts Options, database
|
||||
if t, ok := c.tasks.Get(taskID); ok {
|
||||
_ = database.UpdateTask(*t)
|
||||
}
|
||||
files, err := buildFileList(opts.MediaPath, opts.SourceRules, copiedPaths)
|
||||
files, err := buildFileList(opts.MediaPath, opts.SourceRules, copiedPaths, opts.AllowedExtensions)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
return
|
||||
@@ -358,10 +359,11 @@ type fileEntry struct {
|
||||
size int64
|
||||
}
|
||||
|
||||
func buildFileList(mediaPath string, rules []config.SourceFolder, skip map[string]struct{}) ([]fileEntry, error) {
|
||||
func buildFileList(mediaPath string, rules []config.SourceFolder, skip map[string]struct{}, allowedExtensions []string) ([]fileEntry, error) {
|
||||
_ = mediaPath
|
||||
roots, selectedRoots, ruleMap := normalizeSourceRules(rules)
|
||||
aliases := sourceAliases(roots)
|
||||
allowedExts := makeAllowedExtensionSet(allowedExtensions)
|
||||
|
||||
var result []fileEntry
|
||||
for _, src := range selectedRoots {
|
||||
@@ -398,6 +400,9 @@ func buildFileList(mediaPath string, rules []config.SourceFolder, skip map[strin
|
||||
if !isPathEnabled(path, ruleMap) {
|
||||
return nil
|
||||
}
|
||||
if !isExtensionAllowed(path, allowedExts) {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
rel = filepath.ToSlash(rel)
|
||||
destRel := filepath.ToSlash(filepath.Join(alias, rel))
|
||||
@@ -421,6 +426,27 @@ func buildFileList(mediaPath string, rules []config.SourceFolder, skip map[strin
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func makeAllowedExtensionSet(items []string) map[string]struct{} {
|
||||
normalized := config.NormalizeExtensions(items)
|
||||
if len(normalized) == 0 {
|
||||
normalized = config.DefaultAllowedExtensions()
|
||||
}
|
||||
result := make(map[string]struct{}, len(normalized))
|
||||
for _, item := range normalized {
|
||||
result[item] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isExtensionAllowed(path string, allowed map[string]struct{}) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext == "" {
|
||||
return false
|
||||
}
|
||||
_, ok := allowed[ext]
|
||||
return ok
|
||||
}
|
||||
|
||||
func normalizeSourceRules(rules []config.SourceFolder) ([]string, []string, map[string]bool) {
|
||||
ruleMap := make(map[string]bool, len(rules))
|
||||
rootSet := make(map[string]struct{})
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package copier
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"jukebox_maker/internal/config"
|
||||
)
|
||||
|
||||
func TestBuildFileListFiltersAllowedExtensions(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
source := filepath.Join(root, "music")
|
||||
if err := os.MkdirAll(filepath.Join(source, "nested"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
filepath.Join(source, "track.mp3"): "audio",
|
||||
filepath.Join(source, "clip.mp4"): "video",
|
||||
filepath.Join(source, "cover.jpg"): "photo",
|
||||
filepath.Join(source, "nested", "note.txt"): "text",
|
||||
}
|
||||
for path, body := range files {
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(%q) error = %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
items, err := buildFileList("", []config.SourceFolder{
|
||||
{Path: source, Enabled: true, Root: true},
|
||||
}, nil, config.DefaultAllowedExtensions())
|
||||
if err != nil {
|
||||
t.Fatalf("buildFileList() error = %v", err)
|
||||
}
|
||||
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("buildFileList() len = %d, want 2", len(items))
|
||||
}
|
||||
|
||||
got := make(map[string]struct{}, len(items))
|
||||
for _, item := range items {
|
||||
got[filepath.Base(item.relPath)] = struct{}{}
|
||||
}
|
||||
if _, ok := got["track.mp3"]; !ok {
|
||||
t.Fatalf("missing mp3 file: %v", got)
|
||||
}
|
||||
if _, ok := got["clip.mp4"]; !ok {
|
||||
t.Fatalf("missing mp4 file: %v", got)
|
||||
}
|
||||
if _, ok := got["cover.jpg"]; ok {
|
||||
t.Fatalf("unexpected jpg file: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,48 @@
|
||||
<span class="form-hint">The new-only mode skips files already copied to this disk, even if they were later removed.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="allowedFilesMode">Allowed file types</label>
|
||||
<select class="form-select" id="allowedFilesMode" style="width:auto;max-width:420px" onchange="updateAllowedFilesModeUI()">
|
||||
<option value="media_types">Audio, video, photo</option>
|
||||
<option value="extensions">Custom extensions list</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="mediaTypesGroup">
|
||||
<label class="form-label">Built-in media types</label>
|
||||
<div style="display:grid;gap:8px">
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="mediaTypeAudio" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
<span>
|
||||
<strong>Audio</strong>
|
||||
<span class="form-hint" id="mediaTypeAudioHint" style="display:block"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="mediaTypeVideo" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
<span>
|
||||
<strong>Video</strong>
|
||||
<span class="form-hint" id="mediaTypeVideoHint" style="display:block"></span>
|
||||
</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="mediaTypePhoto" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
<span>
|
||||
<strong>Photo</strong>
|
||||
<span class="form-hint" id="mediaTypePhotoHint" style="display:block"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="form-hint">Built into the app by default: audio, video, and photo. New installations start with only audio and video enabled.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="extensionsGroup" style="display:none">
|
||||
<label class="form-label" for="allowedExtensions">Allowed extensions</label>
|
||||
<textarea class="form-input" id="allowedExtensions" rows="5" placeholder=".mp3, .flac, .mp4"></textarea>
|
||||
<span class="form-hint">One extension per line or separated by commas. You can write <code>mp3</code> or <code>.mp3</code>.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="destFolder">Destination folder on disk</label>
|
||||
<input class="form-input" type="text" id="destFolder" placeholder="media" style="width:200px">
|
||||
@@ -78,6 +120,11 @@
|
||||
const sourceTree = new Map();
|
||||
const expandedNodes = new Set();
|
||||
const loadingNodes = new Set();
|
||||
const builtInMediaTypes = {
|
||||
audio: ['.aac', '.aif', '.aiff', '.alac', '.ape', '.flac', '.m4a', '.mp2', '.mp3', '.ogg', '.opus', '.wav', '.wma'],
|
||||
video: ['.3gp', '.avi', '.m2ts', '.m4v', '.mkv', '.mov', '.mp4', '.mpeg', '.mpg', '.mts', '.ts', '.webm', '.wmv'],
|
||||
photo: ['.bmp', '.gif', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.tif', '.tiff', '.webp'],
|
||||
};
|
||||
let sourceRoots = [];
|
||||
let sourceConfig = {};
|
||||
|
||||
@@ -328,6 +375,52 @@ function deriveRootsFromSources(sources) {
|
||||
.filter((path, index, all) => !all.some((other, otherIndex) => otherIndex !== index && isPathWithin(other, path) && !isSamePath(other, path)));
|
||||
}
|
||||
|
||||
function defaultAllowedExtensions() {
|
||||
return [...builtInMediaTypes.audio, ...builtInMediaTypes.video];
|
||||
}
|
||||
|
||||
function parseExtensionsInput(value) {
|
||||
const items = String(value || '')
|
||||
.split(/[\n,]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const result = [];
|
||||
const seen = new Set();
|
||||
items.forEach((item) => {
|
||||
let value = item.toLowerCase().replace(/^\*/, '');
|
||||
if (!value.startsWith('.')) value = '.' + value;
|
||||
if (!/^\.[a-z0-9]+$/.test(value)) return;
|
||||
if (seen.has(value)) return;
|
||||
seen.add(value);
|
||||
result.push(value);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatExtensionsInput(items) {
|
||||
return (items || []).join('\n');
|
||||
}
|
||||
|
||||
function selectedMediaTypes() {
|
||||
return ['audio', 'video', 'photo'].filter((name) => {
|
||||
const id = 'mediaType' + name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return document.getElementById(id).checked;
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllowedFilesModeUI() {
|
||||
const mode = document.getElementById('allowedFilesMode').value || 'media_types';
|
||||
document.getElementById('mediaTypesGroup').style.display = mode === 'media_types' ? '' : 'none';
|
||||
document.getElementById('extensionsGroup').style.display = mode === 'extensions' ? '' : 'none';
|
||||
}
|
||||
|
||||
function renderMediaTypeHints() {
|
||||
document.getElementById('mediaTypeAudioHint').textContent = builtInMediaTypes.audio.join(', ');
|
||||
document.getElementById('mediaTypeVideoHint').textContent = builtInMediaTypes.video.join(', ');
|
||||
document.getElementById('mediaTypePhotoHint').textContent = builtInMediaTypes.photo.join(', ');
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const r = await fetch('/api/config');
|
||||
@@ -336,8 +429,14 @@ async function loadSettings() {
|
||||
document.getElementById('reserveGB').value = cfg.reserve_free_gb ?? 2;
|
||||
document.getElementById('destFolder').value = cfg.dest_folder || 'media';
|
||||
document.getElementById('fileSelectMode').value = cfg.file_select_mode || 'new';
|
||||
document.getElementById('allowedFilesMode').value = cfg.allowed_files_mode || 'media_types';
|
||||
document.getElementById('overwriteMode').value = cfg.overwrite_mode || 'skip';
|
||||
document.getElementById('autoCopy').checked = !!cfg.auto_copy;
|
||||
document.getElementById('mediaTypeAudio').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('audio');
|
||||
document.getElementById('mediaTypeVideo').checked = (cfg.enabled_media_types || ['audio', 'video']).includes('video');
|
||||
document.getElementById('mediaTypePhoto').checked = (cfg.enabled_media_types || []).includes('photo');
|
||||
document.getElementById('allowedExtensions').value = formatExtensionsInput((cfg.allowed_extensions || []).length ? cfg.allowed_extensions : defaultAllowedExtensions());
|
||||
updateAllowedFilesModeUI();
|
||||
|
||||
sourceConfig = {};
|
||||
(cfg.sources || []).forEach((source) => {
|
||||
@@ -357,6 +456,9 @@ async function saveSettings(event) {
|
||||
reserve_free_gb: parseFloat(document.getElementById('reserveGB').value) || 2,
|
||||
dest_folder: document.getElementById('destFolder').value.trim() || 'media',
|
||||
file_select_mode: document.getElementById('fileSelectMode').value,
|
||||
allowed_files_mode: document.getElementById('allowedFilesMode').value,
|
||||
enabled_media_types: selectedMediaTypes(),
|
||||
allowed_extensions: parseExtensionsInput(document.getElementById('allowedExtensions').value),
|
||||
overwrite_mode: document.getElementById('overwriteMode').value,
|
||||
auto_copy: document.getElementById('autoCopy').checked,
|
||||
sources: collectSourcesForSave(),
|
||||
@@ -405,6 +507,7 @@ document.getElementById('sourceTree').addEventListener('change', (event) => {
|
||||
toggleSource(checkbox.dataset.path, checkbox.checked);
|
||||
});
|
||||
|
||||
renderMediaTypeHints();
|
||||
loadSettings();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user