up
This commit is contained in:
parent
a6d64980fa
commit
04e3430912
@ -840,48 +840,143 @@ func getAllPaths(db *gorm.DB) []*models.PathDownload {
|
|||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type ffprobeOut struct {
|
||||||
|
Format struct{ Duration string `json:"duration"` } `json:"format"`
|
||||||
|
Streams []struct {
|
||||||
|
CodecType string `json:"codec_type"`
|
||||||
|
Width int `json:"width,omitempty"`
|
||||||
|
Height int `json:"height,omitempty"`
|
||||||
|
} `json:"streams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func probe(ctx context.Context, file string) (*ffprobeOut, error) {
|
||||||
|
cmd := exec.CommandContext(ctx,
|
||||||
|
"ffprobe", "-v", "error",
|
||||||
|
"-print_format", "json",
|
||||||
|
"-show_format", "-show_streams",
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var info ffprobeOut
|
||||||
|
if err := json.Unmarshal(out, &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
type mediaItemView struct {
|
type mediaItemView struct {
|
||||||
models.MetadataItem
|
Title string
|
||||||
|
Duration int64
|
||||||
|
Width, Height int
|
||||||
|
ThumbURL string
|
||||||
|
FilePath string
|
||||||
MediaPartID int64
|
MediaPartID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathMedia renvoie la partial HTML de la grille de médias
|
// PathMedia renvoie la liste des sous-dossiers et médias du dossier `PathDownload`
|
||||||
|
// On passe un query param `sub` pour naviguer dans les sous-dossiers.
|
||||||
func PathMedia(db *gorm.DB) http.HandlerFunc {
|
func PathMedia(db *gorm.DB) http.HandlerFunc {
|
||||||
|
allowed := map[string]bool{
|
||||||
|
".mkv": true, ".avi": true, ".mp4": true, ".mov": true,
|
||||||
|
".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
|
||||||
|
".pdf": true, ".epub": true, ".cbz": true,
|
||||||
|
}
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
pid, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
// 1) Récupérer PathDownload
|
||||||
|
vars := mux.Vars(r)
|
||||||
// 1) tentative depuis la BDD
|
pid, _ := strconv.ParseInt(vars["id"], 10, 64)
|
||||||
var items []mediaItemView
|
|
||||||
db.Table("metadata_items").
|
|
||||||
Select("metadata_items.*, media_parts.id AS media_part_id").
|
|
||||||
Joins("JOIN media_items ON media_items.metadata_item_id = metadata_items.id").
|
|
||||||
Joins("JOIN media_parts ON media_parts.media_item_id = media_items.id").
|
|
||||||
Where("metadata_items.library_section_id = ?", pid).
|
|
||||||
Scan(&items)
|
|
||||||
|
|
||||||
// 2) si rien en BDD, on scan physiquement le dossier et on crée un item minimal
|
|
||||||
if len(items) == 0 {
|
|
||||||
var pd models.PathDownload
|
var pd models.PathDownload
|
||||||
if err := db.First(&pd, pid).Error; err == nil {
|
if err := db.First(&pd, pid).Error; err != nil {
|
||||||
files, _ := filepath.Glob(filepath.Join(pd.Path, "*.*"))
|
http.Error(w, "Dossier introuvable", http.StatusNotFound)
|
||||||
for _, f := range files {
|
return
|
||||||
// on prend juste le nom de fichier comme titre
|
|
||||||
items = append(items, mediaItemView{
|
|
||||||
MetadataItem: models.MetadataItem{
|
|
||||||
Title: filepath.Base(f),
|
|
||||||
},
|
|
||||||
MediaPartID: 0, // pas de part en BDD
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) Déterminer le chemin courant
|
||||||
|
sub := r.URL.Query().Get("sub") // ex: "Film/Test"
|
||||||
|
current := filepath.Join(pd.Path, filepath.FromSlash(sub))
|
||||||
|
|
||||||
|
// 3) Lire les entrées du dossier
|
||||||
|
entries, err := os.ReadDir(current)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Impossible de lire le dossier", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Préparer listes
|
||||||
|
type dirView struct {
|
||||||
|
Name string
|
||||||
|
SubPath string
|
||||||
|
}
|
||||||
|
var dirs []dirView
|
||||||
|
var medias []mediaItemView
|
||||||
|
|
||||||
|
thumbDir := filepath.Join("static", "thumbs")
|
||||||
|
os.MkdirAll(thumbDir, 0755)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
name := e.Name()
|
||||||
|
full := filepath.Join(current, name)
|
||||||
|
if e.IsDir() {
|
||||||
|
// sous-dossier
|
||||||
|
dirs = append(dirs, dirView{
|
||||||
|
Name: name,
|
||||||
|
SubPath: filepath.ToSlash(filepath.Join(sub, name)),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
if !allowed[ext] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
view := mediaItemView{Title: name, FilePath: full}
|
||||||
|
// vidéos : métadonnées + capture
|
||||||
|
if ext == ".mkv" || ext == ".avi" || ext == ".mp4" || ext == ".mov" {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||||
|
info, err := probe(ctx, full)
|
||||||
|
cancel()
|
||||||
|
if err == nil {
|
||||||
|
if d, err := strconv.ParseFloat(info.Format.Duration, 64); err == nil {
|
||||||
|
view.Duration = int64(d)
|
||||||
|
}
|
||||||
|
for _, s := range info.Streams {
|
||||||
|
if s.CodecType == "video" {
|
||||||
|
view.Width, view.Height = s.Width, s.Height
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// screenshot
|
||||||
|
base := strings.TrimSuffix(name, ext)
|
||||||
|
thumbName := base + ".jpg"
|
||||||
|
thumbPath := filepath.Join(thumbDir, thumbName)
|
||||||
|
if _, err := os.Stat(thumbPath); os.IsNotExist(err) {
|
||||||
|
exec.Command("ffmpeg", "-ss", "5", "-i", full, "-frames:v", "1", thumbPath).Run()
|
||||||
|
}
|
||||||
|
view.ThumbURL = "/static/thumbs/" + thumbName
|
||||||
|
} else {
|
||||||
|
// icônes génériques pour images/PDF/EPUB/CBZ
|
||||||
|
view.ThumbURL = "/static/icons/" + ext[1:] + ".svg"
|
||||||
|
}
|
||||||
|
medias = append(medias, view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5) Rendu
|
||||||
renderPartial(w, "media_list", map[string]interface{}{
|
renderPartial(w, "media_list", map[string]interface{}{
|
||||||
"mediaItems": items,
|
"PathID": pid,
|
||||||
|
"CurrentSub": sub,
|
||||||
|
"Dirs": dirs,
|
||||||
|
"MediaItems": medias,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MediaDetail affiche la page détail + player
|
// MediaDetail affiche la page détail + player
|
||||||
func MediaDetail(db *gorm.DB) http.HandlerFunc {
|
func MediaDetail(db *gorm.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -940,36 +1035,7 @@ func Stream(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FfmpegProbeOutput reflète la partie “format” et “streams” de ffprobe JSON
|
|
||||||
type ffprobeOut struct {
|
|
||||||
Format struct {
|
|
||||||
Duration string `json:"duration"`
|
|
||||||
} `json:"format"`
|
|
||||||
Streams []struct {
|
|
||||||
CodecType string `json:"codec_type"`
|
|
||||||
Width int `json:"width,omitempty"`
|
|
||||||
Height int `json:"height,omitempty"`
|
|
||||||
} `json:"streams"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// appelle ffprobe et parse le JSON
|
|
||||||
func probe(ctx context.Context, file string) (*ffprobeOut, error) {
|
|
||||||
cmd := exec.CommandContext(ctx,
|
|
||||||
"ffprobe", "-v", "error",
|
|
||||||
"-print_format", "json",
|
|
||||||
"-show_format", "-show_streams",
|
|
||||||
file,
|
|
||||||
)
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var info ffprobeOut
|
|
||||||
if err := json.Unmarshal(out, &info); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user