diff --git a/README.md b/README.md index e2ac5d00..b034490a 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Access the generated M3U playlist at `http://:8080/playlist.m3u`. |-----------------------------|----------------------------------------------------------|---------------|------------------------------------------------| | PUID | Set UID of user running the container. | 1000 | Any valid UID | | PGID | Set GID of user running the container. | 1000 | Any valid GID | +| TZ | Set timezone | Etc/UTC | [TZ Identifiers](https://nodatime.org/TimeZones) | | M3U_URL_1, M3U_URL_2, M3U_URL_X | Set M3U URLs as environment variables. | N/A | Any valid M3U URLs | | M3U_MAX_CONCURRENCY_1, M3U_MAX_CONCURRENCY_2, M3U_MAX_CONCURRENCY_X | Set max concurrency. | 1 | Any integer | | MAX_RETRIES | Set max number of retries (loop) across all M3Us while streaming. 0 to never stop retrying (beware of throttling from provider). | 5 | Any integer greater than or equal 0 | @@ -114,7 +115,6 @@ Access the generated M3U playlist at `http://:8080/playlist.m3u`. | EXCLUDE_TITLE_1, EXCLUDE_TITLE_2, EXCLUDE_TITLE_X | Set channels to exclude based on title | N/A | Go regexp | | TITLE_SUBSTR_FILTER | Sets a regex pattern used to exclude substrings from channel titles | none | Go regexp | | BASE_URL | Sets the base URL for the stream URls in the M3U file to be generated. | http/s:// (e.g. ) | Any string that follows the URL format | -| TZ | Set timezone | Etc/UTC | [TZ Identifiers](https://nodatime.org/TimeZones) | | SYNC_CRON | Set cron schedule expression of the background updates. | 0 0 * * * | Any valid cron expression | | SYNC_ON_BOOT | Set if an initial background syncing will be executed on boot | true | true/false | | CACHE_ON_SYNC | Set if an initial background cache building will be executed after sync. Requires BASE_URL to be set. | false | true/false | diff --git a/m3u/generate.go b/m3u/generate.go index 5db163b0..08d63cf0 100644 --- a/m3u/generate.go +++ b/m3u/generate.go @@ -55,12 +55,39 @@ func getFileExtensionFromUrl(rawUrl string) (string, error) { return path.Ext(u.Path), nil } -func GenerateStreamURL(baseUrl string, slug string, sampleUrl string) string { - ext, err := getFileExtensionFromUrl(sampleUrl) +func getSubPathFromUrl(rawUrl string) (string, error) { + parsedURL, err := url.Parse(rawUrl) if err != nil { - return fmt.Sprintf("%s/%s\n", baseUrl, slug) + return "", err + } + + pathSegments := strings.Split(parsedURL.Path, "/") + + if len(pathSegments) <= 1 { + return "stream", nil + } + + basePath := strings.Join(pathSegments[1:len(pathSegments)-1], "/") + return basePath, nil +} + +func GenerateStreamURL(baseUrl string, stream database.StreamInfo) string { + var subPath string + var err error + for _, srcUrl := range stream.URLs { + subPath, err = getSubPathFromUrl(srcUrl) + if err != nil { + continue + } + + ext, err := getFileExtensionFromUrl(srcUrl) + if err != nil { + return fmt.Sprintf("%s/proxy/%s/%s\n", baseUrl, subPath, stream.Slug) + } + + return fmt.Sprintf("%s/proxy/%s/%s%s\n", baseUrl, subPath, stream.Slug, ext) } - return fmt.Sprintf("%s/%s%s\n", baseUrl, slug, ext) + return fmt.Sprintf("%s/proxy/stream/%s\n", baseUrl, stream.Slug) } func GenerateAndCacheM3UContent(db *database.Instance, r *http.Request) string { @@ -109,7 +136,7 @@ func GenerateAndCacheM3UContent(db *database.Instance, r *http.Request) string { extInfTags = append(extInfTags, fmt.Sprintf("group-title=\"%s\"", stream.Group)) content.WriteString(fmt.Sprintf("%s,%s\n", strings.Join(extInfTags, " "), stream.Title)) - content.WriteString(GenerateStreamURL(baseUrl, stream.Slug, stream.URLs[0])) + content.WriteString(GenerateStreamURL(baseUrl, stream)) } if debug { diff --git a/m3u/m3u_test.go b/m3u/m3u_test.go index 7a63825c..5d8d1ad7 100644 --- a/m3u/m3u_test.go +++ b/m3u/m3u_test.go @@ -69,7 +69,7 @@ func TestGenerateM3UContent(t *testing.T) { // Check the generated M3U content expectedContent := fmt.Sprintf(`#EXTM3U #EXTINF:-1 tvg-id="1" tvg-logo="http://example.com/logo.png" tvg-name="TestStream" group-title="TestGroup",TestStream -%s`, GenerateStreamURL("http:///stream", "test-stream", stream.URLs[0])) +%s`, GenerateStreamURL("http://", stream)) if rr.Body.String() != expectedContent { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expectedContent) diff --git a/main.go b/main.go index 846fe133..1acebd15 100644 --- a/main.go +++ b/main.go @@ -49,14 +49,14 @@ func main() { http.HandleFunc("/playlist.m3u", func(w http.ResponseWriter, r *http.Request) { m3u.Handler(w, r) }) - http.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) { + http.HandleFunc("/proxy/", func(w http.ResponseWriter, r *http.Request) { proxy.Handler(w, r) }) // Start the server utils.SafeLogln("Server is running on port 8080...") utils.SafeLogln("Playlist Endpoint is running (`/playlist.m3u`)") - utils.SafeLogln("Stream Endpoint is running (`/stream/{streamID}.{fileExt}`)") + utils.SafeLogln("Stream Endpoint is running (`/proxy/{originalBasePath}/{streamID}.{fileExt}`)") err = http.ListenAndServe(":8080", nil) if err != nil { utils.SafeLogFatalf("HTTP server error: %v", err) diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 4e748e27..eccaf72d 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -62,7 +62,7 @@ func TestStreamHandler(t *testing.T) { for _, stream := range streams { log.Printf("Stream (%s): %v", stream.Title, stream) - genStreamUrl := strings.TrimSpace(m3u.GenerateStreamURL("", stream.Slug, stream.URLs[0])) + genStreamUrl := strings.TrimSpace(m3u.GenerateStreamURL("", stream)) req := httptest.NewRequest("GET", genStreamUrl, nil) w := httptest.NewRecorder() diff --git a/proxy/stream_handler.go b/proxy/stream_handler.go index 33ec4f3a..434a2cba 100644 --- a/proxy/stream_handler.go +++ b/proxy/stream_handler.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "os" + "path" "slices" "sort" "strconv" @@ -286,7 +287,7 @@ func Handler(w http.ResponseWriter, r *http.Request) { utils.SafeLogf("Received request from %s for URL: %s\n", r.RemoteAddr, r.URL.Path) - streamUrl := strings.Split(strings.TrimPrefix(r.URL.Path, "/stream/"), ".")[0] + streamUrl := strings.Split(path.Base(r.URL.Path), ".")[0] if streamUrl == "" { utils.SafeLogf("Invalid m3uID for request from %s: %s\n", r.RemoteAddr, r.URL.Path) http.NotFound(w, r) diff --git a/utils/env.go b/utils/env.go index e245c444..60b40e95 100644 --- a/utils/env.go +++ b/utils/env.go @@ -21,8 +21,14 @@ func GetEnv(env string) string { } } +var m3uIndexes []int +var m3uIndexesInitialized bool + func GetM3UIndexes() []int { - m3uIndexes := []int{} + if m3uIndexesInitialized { + return m3uIndexes + } + m3uIndexes = []int{} for _, env := range os.Environ() { pair := strings.SplitN(env, "=", 2) if strings.HasPrefix(pair[0], "M3U_URL_") { @@ -34,11 +40,18 @@ func GetM3UIndexes() []int { m3uIndexes = append(m3uIndexes, index-1) } } + m3uIndexesInitialized = true return m3uIndexes } +var filters []string +var filtersInitialized bool + func GetFilters(baseEnv string) []string { - filters := []string{} + if filtersInitialized { + return filters + } + filters = []string{} for _, env := range os.Environ() { pair := strings.SplitN(env, "=", 2) if strings.HasPrefix(pair[0], baseEnv) { @@ -50,5 +63,6 @@ func GetFilters(baseEnv string) []string { filters = append(filters, pair[1]) } } + filtersInitialized = true return filters } diff --git a/utils/http.go b/utils/http.go index f3cb403d..f444a700 100644 --- a/utils/http.go +++ b/utils/http.go @@ -30,14 +30,14 @@ func CustomHttpRequest(method string, url string) (*http.Response, error) { func DetermineBaseURL(r *http.Request) string { if customBase, ok := os.LookupEnv("BASE_URL"); ok { - return fmt.Sprintf("%s/stream", strings.TrimSuffix(customBase, "/")) + return strings.TrimSuffix(customBase, "/") } if r != nil { if r.TLS == nil { - return fmt.Sprintf("http://%s/stream", r.Host) + return fmt.Sprintf("http://%s", r.Host) } else { - return fmt.Sprintf("https://%s/stream", r.Host) + return fmt.Sprintf("https://%s", r.Host) } }