test
This commit is contained in:
parent
3fc81e5c80
commit
3409091557
@ -223,6 +223,10 @@ func RoutesProtected(r *mux.Router, bd *gorm.DB) {
|
|||||||
r.HandleFunc("/folders", renders.StreamHandler)
|
r.HandleFunc("/folders", renders.StreamHandler)
|
||||||
r.HandleFunc("/folders/detail", renders.DetailHandler).Methods("GET")
|
r.HandleFunc("/folders/detail", renders.DetailHandler).Methods("GET")
|
||||||
|
|
||||||
|
r.HandleFunc("/api/paths/{id:[0-9]+}/media", renders.PathMedia(bd)).Methods("GET")
|
||||||
|
r.HandleFunc("/stream/{partID:[0-9]+}", renders.Stream(bd)).Methods("GET")
|
||||||
|
r.HandleFunc("/media/{partID:[0-9]+}", renders.MediaDetail(bd)).Methods("GET")
|
||||||
|
|
||||||
//API Scan folder
|
//API Scan folder
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -839,7 +840,136 @@ func getAllPaths(db *gorm.DB) []*models.PathDownload {
|
|||||||
return paths
|
return paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mediaItemView struct {
|
||||||
|
models.MetadataItem
|
||||||
|
MediaPartID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathMedia renvoie la partial HTML de la grille de médias
|
||||||
|
func PathMedia(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pid, _ := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
|
||||||
|
|
||||||
|
// 1) tentative depuis la BDD
|
||||||
|
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
|
||||||
|
if err := db.First(&pd, pid).Error; err == nil {
|
||||||
|
files, _ := filepath.Glob(filepath.Join(pd.Path, "*.*"))
|
||||||
|
for _, f := range files {
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPartial(w, "media_list", map[string]interface{}{
|
||||||
|
"mediaItems": items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaDetail affiche la page détail + player
|
||||||
|
func MediaDetail(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64)
|
||||||
|
|
||||||
|
// on récupère 1 metadata + file pour ce media_part
|
||||||
|
var item struct {
|
||||||
|
models.MetadataItem
|
||||||
|
MediaPartID int64
|
||||||
|
File string
|
||||||
|
}
|
||||||
|
db.Table("metadata_items").
|
||||||
|
Select("metadata_items.*, media_parts.id AS media_part_id, media_parts.file").
|
||||||
|
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("media_parts.id = ?", partID).
|
||||||
|
Scan(&item)
|
||||||
|
|
||||||
|
if item.MediaPartID == 0 {
|
||||||
|
http.Error(w, "Média introuvable", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemplate(w, "media_detail", map[string]interface{}{
|
||||||
|
"item": item,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream : transcode à la volée en MP4 progressif et pipe directement dans la réponse
|
||||||
|
func Stream(db *gorm.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
partID, _ := strconv.ParseInt(mux.Vars(r)["partID"], 10, 64)
|
||||||
|
var part models.MediaPart
|
||||||
|
if err := db.First(&part, partID).Error; err != nil {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "video/mp4")
|
||||||
|
// ffmpeg en pipe
|
||||||
|
cmd := exec.CommandContext(r.Context(),
|
||||||
|
"ffmpeg",
|
||||||
|
"-i", part.File,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-movflags", "frag_keyframe+empty_moov+faststart",
|
||||||
|
"-f", "mp4",
|
||||||
|
"pipe:1",
|
||||||
|
)
|
||||||
|
cmd.Stdout = w
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Println("ffmpeg:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -85,4 +85,12 @@ function hide(target){
|
|||||||
document.addEventListener("htmx:afterOnLoad", function (event) {
|
document.addEventListener("htmx:afterOnLoad", function (event) {
|
||||||
// console.log("Réponse du serveur :", event.detail.xhr.responseText);
|
// console.log("Réponse du serveur :", event.detail.xhr.responseText);
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('.path-link').forEach(el=>{
|
||||||
|
el.addEventListener('click', e=>{
|
||||||
|
e.preventDefault();
|
||||||
|
let id = el.dataset.id;
|
||||||
|
fetch('/api/paths/'+id+'/media')
|
||||||
|
.then(r=>r.text())
|
||||||
|
.then(html=> document.getElementById('content').innerHTML = html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -29,15 +29,14 @@
|
|||||||
<ul class="menu-list" id="libraryMenu" hidden>
|
<ul class="menu-list" id="libraryMenu" hidden>
|
||||||
<li>
|
<li>
|
||||||
<a class="is-active">Choise Library</a>
|
<a class="is-active">Choise Library</a>
|
||||||
<ul>
|
<ul id="paths-list">
|
||||||
{{range .paths}}
|
{{range .paths}}
|
||||||
<li>
|
<li>
|
||||||
<span class="icon-text">
|
<a href="#" class="path-link" data-id="{{.ID}}">
|
||||||
<span><a>{{ .PathName }}</a></span>
|
{{.PathName}}
|
||||||
<span class="icon">
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
<a style="padding-top: 0;height: 0;"><i class="fas fa-ellipsis-v"></i></a>
|
</a>
|
||||||
</span>
|
</li>
|
||||||
</span>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
11
templates/media_detail.pages.tmpl
Normal file
11
templates/media_detail.pages.tmpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{define "media_detail"}}
|
||||||
|
<div class="detail">
|
||||||
|
<img src="{{.item.UserThumbURL}}" alt="{{.item.Title}}" class="cover">
|
||||||
|
<h1>{{.item.Title}}</h1>
|
||||||
|
{{with .item.Summary}}<p>{{.}}</p>{{end}}
|
||||||
|
<video controls autoplay width="100%" poster="{{.item.UserThumbURL}}">
|
||||||
|
<source src="/stream/{{.item.MediaPartID}}" type="video/mp4">
|
||||||
|
Votre navigateur ne supporte pas la vidéo.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
19
templates/media_list.pages.tmpl
Normal file
19
templates/media_list.pages.tmpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{{define "media_list"}}
|
||||||
|
<div class="grid">
|
||||||
|
{{if .mediaItems}}
|
||||||
|
{{range .mediaItems}}
|
||||||
|
<div class="card">
|
||||||
|
<a href="/media/{{.MediaPartID}}">
|
||||||
|
<img src="{{.UserThumbURL}}" alt="{{.Title}}" class="thumb" />
|
||||||
|
<div class="infos">
|
||||||
|
<h3>{{.Title}}</h3>
|
||||||
|
{{with .Summary}}<p>{{.}}</p>{{end}}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<p>Aucun média trouvé dans ce dossier.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
Loading…
Reference in New Issue
Block a user