node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1 @@
Source code from [CedArtic/go-vlc-ctrl](https://github.com/CedArctic/go-vlc-ctrl), updated and modified for the purpose of this project.

View File

@@ -0,0 +1,41 @@
package vlc
import (
"errors"
"strconv"
)
// Art fetches cover art based on a playlist item's ID. If no ID is provided, Art returns the current item's cover art.
// Cover art is returned in the form of a byte array.
func (vlc *VLC) Art(itemID ...int) (byteArr []byte, err error) {
// Check variadic arguments
if len(itemID) > 1 {
err = errors.New("please provide only up to one ID")
return
}
// Build request URL
urlSegment := "/art"
if len(itemID) == 1 {
urlSegment = urlSegment + "?item=" + strconv.Itoa(itemID[0])
}
// Make request
var response string
response, err = vlc.RequestMaker(urlSegment)
// Error Handling
if err != nil {
return
}
if response == "Error" {
err = errors.New("no cover art available for item")
return
}
// Convert response to byte array
byteArr = []byte(response)
return
}

View File

@@ -0,0 +1,39 @@
package vlc
import "github.com/goccy/go-json"
// File struct represents a single item in the browsed directory. Can be a file or a dir
type File struct {
Type string `json:"type"` // file or dir
Path string `json:"path"`
Name string `json:"name"`
AccessTime uint `json:"access_time"`
UID uint `json:"uid"`
CreationTime uint `json:"creation_time"`
GID uint `json:"gid"`
ModificationTime uint `json:"modification_time"`
Mode uint `json:"mode"`
URI string `json:"uri"`
Size uint `json:"size"`
}
// ParseBrowse parses Browse() responses to []File
func ParseBrowse(browseResponse string) (files []File, err error) {
var temp struct {
Files []File `json:"element"`
}
err = json.Unmarshal([]byte(browseResponse), &temp)
files = temp.Files
return
}
// Browse returns a File array with the items of the provided directory URI
func (vlc *VLC) Browse(uri string) (files []File, err error) {
var response string
response, err = vlc.RequestMaker("/requests/browse.json?uri=" + uri)
if err != nil {
return
}
files, err = ParseBrowse(response)
return
}

View File

@@ -0,0 +1,39 @@
package vlc
import "github.com/goccy/go-json"
// Node structure (node or leaf type) is the basic element of VLC's playlist tree representation.
// Leafs are playlist items. Nodes are playlists or folders inside playlists.
type Node struct {
Ro string `json:"ro"`
Type string `json:"type"` // node or leaf
Name string `json:"name"`
ID string `json:"id"`
Duration int `json:"duration,omitempty"`
URI string `json:"uri,omitempty"`
Current string `json:"current,omitempty"`
Children []Node `json:"children,omitempty"`
}
// ParsePlaylist parses Playlist() responses to Node
func ParsePlaylist(playlistResponse string) (playlist Node, err error) {
err = json.Unmarshal([]byte(playlistResponse), &playlist)
if err != nil {
return
}
return
}
// Playlist returns a Node object that is the root node of VLC's Playlist tree
// Playlist tree structure: Level 0 - Root Node (Type="node"), Level 1 - Playlists (Type="node"),
// Level 2+: Playlist Items (Type="leaf") or Folder (Type="node")
func (vlc *VLC) Playlist() (playlist Node, err error) {
// Make response and check for errors
response, err := vlc.RequestMaker("/requests/playlist.json")
if err != nil {
return
}
// Parse to node
playlist, err = ParsePlaylist(response)
return
}

View File

@@ -0,0 +1,66 @@
package vlc
import (
"fmt"
"runtime"
"seanime/internal/util"
"time"
)
func (vlc *VLC) getExecutableName() string {
switch runtime.GOOS {
case "windows":
return "vlc.exe"
case "linux":
return "vlc"
case "darwin":
return "vlc"
default:
return "vlc"
}
}
func (vlc *VLC) GetExecutablePath() string {
if len(vlc.Path) > 0 {
return vlc.Path
}
switch runtime.GOOS {
case "windows":
return "C:\\Program Files\\VideoLAN\\VLC\\vlc.exe"
case "linux":
return "/usr/bin/vlc" // Default path for VLC on most Linux distributions
case "darwin":
return "/Applications/VLC.app/Contents/MacOS/VLC" // Default path for VLC on macOS
default:
return "C:\\Program Files\\VideoLAN\\VLC\\vlc.exe"
}
}
func (vlc *VLC) Start() error {
// If the path is empty, do not check if VLC is running
if vlc.Path == "" {
return nil
}
// Check if VLC is already running
name := vlc.getExecutableName()
if util.ProgramIsRunning(name) {
return nil
}
// Start VLC
exe := vlc.GetExecutablePath()
cmd := util.NewCmd(exe)
err := cmd.Start()
if err != nil {
vlc.Logger.Error().Err(err).Msg("vlc: Error starting VLC")
return fmt.Errorf("error starting VLC: %w", err)
}
time.Sleep(1 * time.Second)
return nil
}

View File

@@ -0,0 +1,390 @@
package vlc
import (
"errors"
"net/url"
"path/filepath"
"strconv"
"strings"
"github.com/goccy/go-json"
)
// Status contains information related to the VLC instance status. Use parseStatus to parse the response from a
// status.go function.
type Status struct {
// TODO: The Status structure is still a work in progress
Fullscreen bool `json:"fullscreen"`
Stats Stats `json:"stats"`
AspectRatio string `json:"aspectratio"`
AudioDelay float64 `json:"audiodelay"`
APIVersion uint `json:"apiversion"`
CurrentPlID uint `json:"currentplid"`
Time uint `json:"time"`
Volume uint `json:"volume"`
Length uint `json:"length"`
Random bool `json:"random"`
AudioFilters map[string]string `json:"audiofilters"`
Rate float64 `json:"rate"`
VideoEffects VideoEffects `json:"videoeffects"`
State string `json:"state"`
Loop bool `json:"loop"`
Version string `json:"version"`
Position float64 `json:"position"`
Information Information `json:"information"`
Repeat bool `json:"repeat"`
SubtitleDelay float64 `json:"subtitledelay"`
Equalizer []Equalizer `json:"equalizer"`
}
// Stats contains certain statistics of a VLC instance. A Stats variable is included in Status
type Stats struct {
InputBitRate float64 `json:"inputbitrate"`
SentBytes uint `json:"sentbytes"`
LosABuffers uint `json:"lostabuffers"`
AveragedEMuxBitrate float64 `json:"averagedemuxbitrate"`
ReadPackets uint `json:"readpackets"`
DemuxReadPackets uint `json:"demuxreadpackets"`
LostPictures uint `json:"lostpictures"`
DisplayedPictures uint `json:"displayedpictures"`
SentPackets uint `json:"sentpackets"`
DemuxReadBytes uint `json:"demuxreadbytes"`
DemuxBitRate float64 `json:"demuxbitrate"`
PlayedABuffers uint `json:"playedabuffers"`
DemuxDiscontinuity uint `json:"demuxdiscontinuity"`
DecodeAudio uint `json:"decodedaudio"`
SendBitRate float64 `json:"sendbitrate"`
ReadBytes uint `json:"readbytes"`
AverageInputBitRate float64 `json:"averageinputbitrate"`
DemuxCorrupted uint `json:"demuxcorrupted"`
DecodedVideo uint `json:"decodedvideo"`
}
// VideoEffects contains the current video effects configuration. A VideoEffects variable is included in Status
type VideoEffects struct {
Hue int `json:"hue"`
Saturation int `json:"saturation"`
Contrast int `json:"contrast"`
Brightness int `json:"brightness"`
Gamma int `json:"gamma"`
}
// Information contains information related to the item currently being played. It is also part of Status
type Information struct {
Chapter int `json:"chapter"`
// TODO: Chapters definition might need to be changed
Chapters []interface{} `json:"chapters"`
Title int `json:"title"`
// TODO: Category definition might need to be updated/modified
Category map[string]struct {
Filename string `json:"filename"`
Codec string `json:"Codec"`
Channels string `json:"Channels"`
BitsPerSample string `json:"Bits_per_sample"`
Type string `json:"Type"`
SampleRate string `json:"Sample_rate"`
} `json:"category"`
Titles []interface{} `json:"titles"`
}
// Equalizer contains information related to the equalizer configuration. An Equalizer variable is included in Status
type Equalizer struct {
Presets map[string]string `json:"presets"`
Bands map[string]string `json:"bands"`
Preamp int `json:"preamp"`
}
// parseStatus parses GetStatus() responses to Status struct.
func parseStatus(statusResponse string) (status *Status, err error) {
err = json.Unmarshal([]byte(statusResponse), &status)
if err != nil {
return
}
return status, nil
}
// GetStatus returns a Status object containing information of the instances' status
func (vlc *VLC) GetStatus() (status *Status, err error) {
// Make request
var response string
response, err = vlc.RequestMaker("/requests/status.json")
// Error handling
if err != nil {
return
}
// Parse response to Status
status, err = parseStatus(response)
return
}
// Play playlist item with given id. If id is omitted, play last active item
func (vlc *VLC) Play(itemID ...int) (err error) {
// Check variadic arguments and form urlSegment
if len(itemID) > 1 {
err = errors.New("please provide only up to one ID")
return
}
urlSegment := "/requests/status.json?command=pl_play"
if len(itemID) == 1 {
urlSegment = urlSegment + "&id=" + strconv.Itoa(itemID[0])
}
_, err = vlc.RequestMaker(urlSegment)
return
}
// Pause toggles pause: If current state was 'stop', play item with given id, if no id specified, play current item.
// If no current item, play the first item in the playlist.
func (vlc *VLC) Pause(itemID ...int) (err error) {
// Check variadic arguments and form urlSegment
if len(itemID) > 1 {
err = errors.New("please provide only up to one ID")
return
}
urlSegment := "/requests/status.json?command=pl_pause"
if len(itemID) == 1 {
urlSegment = urlSegment + "&id=" + strconv.Itoa(itemID[0])
}
_, err = vlc.RequestMaker(urlSegment)
return
}
// Stop stops playback
func (vlc *VLC) Stop() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_stop")
return
}
// Next skips to the next playlist item
func (vlc *VLC) Next() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_next")
return
}
// Previous goes back to the previous playlist item
func (vlc *VLC) Previous() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_previous")
return
}
// EmptyPlaylist empties the playlist
func (vlc *VLC) EmptyPlaylist() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_empty")
return
}
// ToggleLoop toggles Random Playback
func (vlc *VLC) ToggleLoop() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_random")
return
}
// ToggleRepeat toggles Playback Looping
func (vlc *VLC) ToggleRepeat() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_loop")
return
}
// ToggleRandom toggles Repeat
func (vlc *VLC) ToggleRandom() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_repeat")
return
}
// ToggleFullscreen toggles Fullscreen mode
func (vlc *VLC) ToggleFullscreen() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=fullscreen")
return
}
func escapeInput(input string) string {
if strings.HasPrefix(input, "http") {
return url.QueryEscape(input)
} else {
input = filepath.FromSlash(input)
return strings.ReplaceAll(url.QueryEscape(input), "+", "%20")
}
}
// AddAndPlay adds a URI to the playlist and starts playback.
// The option field is optional and can have the values: noaudio, novideo
func (vlc *VLC) AddAndPlay(uri string, option ...string) error {
// Check variadic arguments and form urlSegment
if len(option) > 1 {
return errors.New("please provide only one option")
}
urlSegment := "/requests/status.json?command=in_play&input=" + escapeInput(uri)
if len(option) == 1 {
if (option[0] != "noaudio") && (option[0] != "novideo") {
return errors.New("invalid option")
}
urlSegment = urlSegment + "&option=" + option[0]
}
_, err := vlc.RequestMaker(urlSegment)
return err
}
// Add adds a URI to the playlist
func (vlc *VLC) Add(uri string) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=in_enqueue&input=" + escapeInput(uri))
return
}
// AddSubtitle adds a subtitle from URI to currently playing file
func (vlc *VLC) AddSubtitle(uri string) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=addsubtitle&val=" + escapeInput(uri))
return
}
// Resume resumes playback if paused, else does nothing
func (vlc *VLC) Resume() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_forceresume")
return
}
// ForcePause pauses playback, does nothing if already paused
func (vlc *VLC) ForcePause() (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_forcepause")
return
}
// Delete deletes an item with given id from playlist
func (vlc *VLC) Delete(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_delete&id=" + strconv.Itoa(id))
return
}
// AudioDelay sets Audio Delay in seconds
func (vlc *VLC) AudioDelay(delay float64) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=audiodelay&val=" + strconv.FormatFloat(delay, 'f', -1, 64))
return
}
// SubDelay sets Subtitle Delay in seconds
func (vlc *VLC) SubDelay(delay float64) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=subdelay&val=" + strconv.FormatFloat(delay, 'f', -1, 64))
return
}
// PlaybackRate sets Playback Rate. Must be > 0
func (vlc *VLC) PlaybackRate(rate float64) (err error) {
if rate <= 0 {
err = errors.New("rate must be greater than 0")
return
}
_, err = vlc.RequestMaker("/requests/status.json?command=rate&val=" + strconv.FormatFloat(rate, 'f', -1, 64))
return
}
// AspectRatio sets aspect ratio. Must be one of the following values. Any other value will reset aspect ratio to default.
// Valid aspect ratio values: 1:1 , 4:3 , 5:4 , 16:9 , 16:10 , 221:100 , 235:100 , 239:100
func (vlc *VLC) AspectRatio(ratio string) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=aspectratio&val=" + ratio)
return
}
// Sort sorts playlist using sort mode <val> and order <id>.
// If id=0 then items will be sorted in normal order, if id=1 they will be sorted in reverse order.
// A non exhaustive list of sort modes: 0 Id, 1 Name, 3 Author, 5 Random, 7 Track number.
func (vlc *VLC) Sort(id int, val int) (err error) {
if (id != 0) && (id != 1) {
err = errors.New("sorting order must be 0 or 1")
return
}
_, err = vlc.RequestMaker("/requests/status.json?command=pl_sort&id=" + strconv.Itoa(id) + "&val=" + strconv.Itoa(val))
return
}
// ToggleSD toggle-enables service discovery module <val>.
// Typical values are: sap shoutcast, podcast, hal
func (vlc *VLC) ToggleSD(val string) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=pl_sd&val=" + val)
return
}
// Volume sets Volume level <val> (can be absolute integer, or +/- relative value).
// Percentage isn't working at the moment. Allowed values are of the form: +<int>, -<int>, <int> or <int>%
func (vlc *VLC) Volume(val string) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=volume&val=" + val)
return
}
// Seek seeks to <val>
//
// Allowed values are of the form:
// [+ or -][<int><H or h>:][<int><M or m or '>:][<int><nothing or S or s or ">]
// or [+ or -]<int>%
// (value between [ ] are optional, value between < > are mandatory)
// examples:
// 1000 -> seek to the 1000th second
// +1H:2M -> seek 1 hour and 2 minutes forward
// -10% -> seek 10% back
func (vlc *VLC) Seek(val string) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=seek&val=" + val)
return
}
// Preamp sets the preamp gain value, must be >=-20 and <=20
func (vlc *VLC) Preamp(gain int) (err error) {
if (gain < -20) || (gain > 20) {
err = errors.New("preamp must be between -20 and 20")
return
}
_, err = vlc.RequestMaker("/requests/status.json?command=preamp&val=" + strconv.Itoa(gain))
return
}
// SetEQ sets the gain for a specific Equalizer band
func (vlc *VLC) SetEQ(band int, gain int) (err error) {
if (gain < -20) || (gain > 20) {
err = errors.New("gain must be between -20 and 20")
return
}
_, err = vlc.RequestMaker("/requests/status.json?command=equalizer&band=" + strconv.Itoa(band) + "&val=" + strconv.Itoa(gain))
return
}
// ToggleEQ toggles the EQ (true to enable, false to disable)
func (vlc *VLC) ToggleEQ(enable bool) (err error) {
enableStr := "0"
if enable == true {
enableStr = "1"
}
_, err = vlc.RequestMaker("/requests/status.json?command=enableeq&val=" + enableStr)
return
}
// SetEQPreset sets the equalizer preset as per the id specified
func (vlc *VLC) SetEQPreset(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=setpreset&id=" + strconv.Itoa(id))
return
}
// SelectTitle selects the title using the title number
func (vlc *VLC) SelectTitle(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=title&val=" + strconv.Itoa(id))
return
}
// SelectChapter selects the chapter using the chapter number
func (vlc *VLC) SelectChapter(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=chapter&val=" + strconv.Itoa(id))
return
}
// SelectAudioTrack selects the audio track (use the number from the stream)
func (vlc *VLC) SelectAudioTrack(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=audio_track&val=" + strconv.Itoa(id))
return
}
// SelectVideoTrack selects the video track (use the number from the stream)
func (vlc *VLC) SelectVideoTrack(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=video_track&val=" + strconv.Itoa(id))
return
}
// SelectSubtitleTrack selects the subtitle track (use the number from the stream)
func (vlc *VLC) SelectSubtitleTrack(id int) (err error) {
_, err = vlc.RequestMaker("/requests/status.json?command=subtitle_track&val=" + strconv.Itoa(id))
return
}

View File

@@ -0,0 +1,66 @@
package vlc
// https://github.com/CedArctic/go-vlc-ctrl/tree/master
import (
"fmt"
"github.com/rs/zerolog"
"io"
"net/http"
"strconv"
)
// VLC struct represents an http interface enabled VLC instance. Build using NewVLC()
type VLC struct {
Host string
Port int
Password string
Path string
Logger *zerolog.Logger
}
func (vlc *VLC) url() string {
return fmt.Sprintf("http://%s:%s", vlc.Host, strconv.Itoa(vlc.Port))
}
// RequestMaker make requests to VLC using a urlSegment provided by other functions
func (vlc *VLC) RequestMaker(urlSegment string) (response string, err error) {
// Form a GET Request
client := &http.Client{}
request, reqErr := http.NewRequest("GET", vlc.url()+urlSegment, nil)
if reqErr != nil {
err = fmt.Errorf("http request error: %s\n", reqErr)
return
}
// Make a GET request
request.SetBasicAuth("", vlc.Password)
reqResponse, resErr := client.Do(request)
if resErr != nil {
err = fmt.Errorf("http response error: %s\n", resErr)
return
}
defer func() {
reqResponse.Body.Close()
}()
// Check HTTP status code and errors
statusCode := reqResponse.StatusCode
if !((statusCode >= 200) && (statusCode <= 299)) {
err = fmt.Errorf("http error code: %d\n", statusCode)
return "", err
}
// Get byte response and http status code
byteArr, readErr := io.ReadAll(reqResponse.Body)
if readErr != nil {
err = fmt.Errorf("error reading response: %s\n", readErr)
return
}
// Write response
response = string(byteArr)
return
}

View File

@@ -0,0 +1,87 @@
package vlc
import (
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"seanime/internal/test_utils"
"seanime/internal/util"
"testing"
"time"
)
func TestVLC_Play(t *testing.T) {
test_utils.InitTestProvider(t, test_utils.MediaPlayer())
vlc := &VLC{
Host: test_utils.ConfigData.Provider.VlcHost,
Port: test_utils.ConfigData.Provider.VlcPort,
Password: test_utils.ConfigData.Provider.VlcPassword,
Path: test_utils.ConfigData.Provider.VlcPath,
Logger: util.NewLogger(),
}
err := vlc.Start()
require.NoError(t, err)
err = vlc.AddAndPlay("E:\\Anime\\[Judas] Golden Kamuy (Seasons 1-2) [BD 1080p][HEVC x265 10bit][Eng-Subs]\\[Judas] Golden Kamuy - S2\\[Judas] Golden Kamuy S2 - 16.mkv")
if err != nil {
t.Fatal(err)
}
time.Sleep(400 * time.Millisecond)
vlc.ForcePause()
time.Sleep(400 * time.Millisecond)
status, err := vlc.GetStatus()
require.NoError(t, err)
assert.Equal(t, "paused", status.State)
if err != nil {
t.Fatal(err)
}
}
func TestVLC_Seek(t *testing.T) {
test_utils.InitTestProvider(t, test_utils.MediaPlayer())
vlc := &VLC{
Host: test_utils.ConfigData.Provider.VlcHost,
Port: test_utils.ConfigData.Provider.VlcPort,
Password: test_utils.ConfigData.Provider.VlcPassword,
Path: test_utils.ConfigData.Provider.VlcPath,
Logger: util.NewLogger(),
}
err := vlc.Start()
require.NoError(t, err)
err = vlc.AddAndPlay("E:\\ANIME\\[SubsPlease] Bocchi the Rock! (01-12) (1080p) [Batch]\\[SubsPlease] Bocchi the Rock! - 01v2 (1080p) [ABDDAE16].mkv")
time.Sleep(400 * time.Millisecond)
vlc.ForcePause()
time.Sleep(400 * time.Millisecond)
vlc.Seek("100")
time.Sleep(400 * time.Millisecond)
status, err := vlc.GetStatus()
require.NoError(t, err)
assert.Equal(t, "paused", status.State)
spew.Dump(status)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,21 @@
package vlc
import "net/url"
// Vlm returns the full list of VLM elements
func (vlc *VLC) Vlm() (response string, err error) {
response, err = vlc.RequestMaker("/requests/vlm.xml")
return
}
// VlmCmd executes a VLM Command and returns the response. Command is internally URL percent-encoded
func (vlc *VLC) VlmCmd(cmd string) (response string, err error) {
response, err = vlc.RequestMaker("/requests/vlm_cmd.xml?command=" + url.QueryEscape(cmd))
return
}
// VlmCmdErr returns the last VLM Error
func (vlc *VLC) VlmCmdErr() (response string, err error) {
response, err = vlc.RequestMaker("/requests/vlm_cmd.xml")
return
}