diff --git a/Dockerfile b/Dockerfile index daa18556..88798d40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,12 @@ RUN go mod download COPY . . # Build the Go app -RUN go build -o main . +ENV CGO_ENABLED=1 +RUN apk add --no-cache gcc musl-dev +RUN go build -ldflags='-s -w -extldflags "-static"' -o main . + +# Run tests +RUN go test ./... #################### diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 00000000..be5e2ecc --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,52 @@ +package database + +import ( + "testing" + "os" + "path/filepath" +) + +func TestSaveAndLoadFromSQLite(t *testing.T) { + sqliteDBPath := filepath.Join(".", "data", "database.sqlite") + + // Test InitializeSQLite and check if the database file exists + err := InitializeSQLite() + if err != nil { + t.Errorf("InitializeSQLite returned error: %v", err) + } + defer os.Remove(sqliteDBPath) // Cleanup the database file after the test + + // Test LoadFromSQLite with existing data in the database + expected := []StreamInfo{{ + Title: "stream1", + TvgID: "test1", + LogoURL: "http://test.com/image.png", + Group: "test", + URLs: []StreamURL{{ + Content: "testing", + M3UIndex: 1, + }}, + }, { + Title: "stream2", + TvgID: "test2", + LogoURL: "http://test2.com/image.png", + Group: "test2", + URLs: []StreamURL{{ + Content: "testing2", + M3UIndex: 2, + }}, + }} + err = SaveToSQLite(expected) // Insert test data into the database + if err != nil { + t.Errorf("SaveToSQLite returned error: %v", err) + } + + result, err := LoadFromSQLite() + if err != nil { + t.Errorf("LoadFromSQLite returned error: %v", err) + } + + if len(result) != len(expected) { + t.Errorf("LoadFromSQLite returned %+v, expected %+v", result, expected) + } +} diff --git a/database/db.go b/database/db.go new file mode 100644 index 00000000..d289a0cf --- /dev/null +++ b/database/db.go @@ -0,0 +1,153 @@ +package database + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" +) + +var db *sql.DB + +func InitializeSQLite() error { + foldername := filepath.Join(".", "data") + filename := filepath.Join(foldername, "database.sqlite") + + err := os.MkdirAll(foldername, 0755) + if err != nil { + return fmt.Errorf("error creating data folder: %v\n", err) + } + + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return fmt.Errorf("error creating database file: %v\n", err) + } + file.Close() + + db, err = sql.Open("sqlite3", filename) + if err != nil { + return fmt.Errorf("error opening SQLite database: %v\n", err) + } + + // Create table if not exists + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS streams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT UNIQUE, + tvg_id TEXT, + logo_url TEXT, + group_name TEXT + ) + `) + if err != nil { + return fmt.Errorf("error creating table: %v\n", err) + } + + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS stream_urls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stream_id INTEGER, + content TEXT, + m3u_index INTEGER, + FOREIGN KEY(stream_id) REFERENCES streams(id) + ) + `) + if err != nil { + return fmt.Errorf("error creating table: %v\n", err) + } + + return nil +} + +func SaveToSQLite(streams []StreamInfo) error { + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("error beginning transaction: %v", err) + } + defer tx.Rollback() + + stmt, err := tx.Prepare("INSERT INTO streams(title, tvg_id, logo_url, group_name) VALUES(?, ?, ?, ?)") + if err != nil { + return fmt.Errorf("error preparing statement: %v", err) + } + defer stmt.Close() + + for _, s := range streams { + res, err := stmt.Exec(s.Title, s.TvgID, s.LogoURL, s.Group) + if err != nil { + return fmt.Errorf("error inserting stream: %v", err) + } + + streamID, err := res.LastInsertId() + if err != nil { + return fmt.Errorf("error getting last inserted ID: %v", err) + } + + urlStmt, err := tx.Prepare("INSERT INTO stream_urls(stream_id, content, m3u_index) VALUES(?, ?, ?)") + if err != nil { + return fmt.Errorf("error preparing statement: %v", err) + } + defer urlStmt.Close() + + for _, u := range s.URLs { + _, err := urlStmt.Exec(streamID, u.Content, u.M3UIndex) + if err != nil { + return fmt.Errorf("error inserting stream URL: %v", err) + } + } + } + + err = tx.Commit() + if err != nil { + return fmt.Errorf("error committing transaction: %v", err) + } + + return nil +} + +func LoadFromSQLite() ([]StreamInfo, error) { + rows, err := db.Query("SELECT id, title, tvg_id, logo_url, group_name FROM streams") + if err != nil { + return nil, fmt.Errorf("error querying streams: %v", err) + } + defer rows.Close() + + var streams []StreamInfo + for rows.Next() { + var s StreamInfo + var streamId int + err := rows.Scan(&streamId, &s.Title, &s.TvgID, &s.LogoURL, &s.Group) + if err != nil { + return nil, fmt.Errorf("error scanning stream: %v", err) + } + + urlRows, err := db.Query("SELECT content, m3u_index FROM stream_urls WHERE stream_id = ?", streamId) + if err != nil { + return nil, fmt.Errorf("error querying stream URLs: %v", err) + } + defer urlRows.Close() + + var urls []StreamURL + for urlRows.Next() { + var u StreamURL + err := urlRows.Scan(&u.Content, &u.M3UIndex) + if err != nil { + return nil, fmt.Errorf("error scanning stream URL: %v", err) + } + urls = append(urls, u) + } + if err := urlRows.Err(); err != nil { + return nil, fmt.Errorf("error iterating over URL rows: %v", err) + } + + s.URLs = urls + streams = append(streams, s) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating over rows: %v", err) + } + + return streams, nil +} diff --git a/m3u/types.go b/database/types.go similarity index 90% rename from m3u/types.go rename to database/types.go index b0b2b9a1..25db1b80 100644 --- a/m3u/types.go +++ b/database/types.go @@ -1,4 +1,4 @@ -package m3u +package database type StreamInfo struct { Title string diff --git a/go.mod b/go.mod index d71184b7..8acbac1d 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module m3u-stream-merger go 1.21.5 require github.com/satori/go.uuid v1.2.0 + +require github.com/mattn/go-sqlite3 v1.14.22 // indirect diff --git a/go.sum b/go.sum index a9a76200..ffa6caf2 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= diff --git a/m3u/generate.go b/m3u/generate.go index 7773c07c..6c0870af 100644 --- a/m3u/generate.go +++ b/m3u/generate.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "m3u-stream-merger/utils" + "m3u-stream-merger/database" "net/http" ) @@ -40,12 +41,12 @@ func GenerateM3UContent(w http.ResponseWriter, r *http.Request) { } } -func FindStreamByName(streamName string) (*StreamInfo, error) { +func FindStreamByName(streamName string) (*database.StreamInfo, error) { for _, s := range Streams { if s.Title == streamName { return &s, nil } } - return &StreamInfo{}, errors.New("stream not found") + return &database.StreamInfo{}, errors.New("stream not found") } diff --git a/m3u/global.go b/m3u/global.go index 71369c64..e92831ef 100644 --- a/m3u/global.go +++ b/m3u/global.go @@ -1,4 +1,8 @@ package m3u -var Streams []StreamInfo -var NewStreams []StreamInfo +import ( + "m3u-stream-merger/database" +) + +var Streams []database.StreamInfo +var NewStreams []database.StreamInfo diff --git a/m3u/parser.go b/m3u/parser.go index ab15c8a7..b116cf3b 100644 --- a/m3u/parser.go +++ b/m3u/parser.go @@ -8,22 +8,30 @@ import ( "regexp" "strings" "sync" + + "m3u-stream-merger/database" ) // GetStreams retrieves and merges stream information from multiple M3U files. func GetStreams(skipClearing bool) error { + // Initialize database + err := database.InitializeSQLite() + if err != nil { + return fmt.Errorf("InitializeSQLite error: %v", err) + } + if !skipClearing { // init - log.Println("Loading from JSON...") - fromJson, err := loadFromJSON() + log.Println("Loading from database...") + fromDB, err := database.LoadFromSQLite() if err == nil { - Streams = fromJson + Streams = fromDB return nil } } - err := loadM3UFiles(skipClearing) + err = loadM3UFiles(skipClearing) if err != nil { return fmt.Errorf("loadM3UFiles error: %v", err) } @@ -60,20 +68,20 @@ func GetStreams(skipClearing bool) error { Streams = NewStreams - fmt.Print("Saving to JSON...\n") - _ = saveToJSON(Streams) + fmt.Print("Saving to database...\n") + _ = database.SaveToSQLite(Streams) return nil } -// mergeStreamInfo merges two slices of StreamInfo based on Title. -func mergeStreamInfo(existing, new []StreamInfo) []StreamInfo { +// mergeStreamInfo merges two slices of database.StreamInfo based on Title. +func mergeStreamInfo(existing, new []database.StreamInfo) []database.StreamInfo { var wg sync.WaitGroup var mutex sync.Mutex for _, stream := range new { wg.Add(1) - go func(s StreamInfo) { + go func(s database.StreamInfo) { defer wg.Done() mutex.Lock() defer mutex.Unlock() @@ -95,9 +103,9 @@ func mergeStreamInfo(existing, new []StreamInfo) []StreamInfo { return existing } -func parseM3UFile(filePath string, m3uIndex int) ([]StreamInfo, error) { +func parseM3UFile(filePath string, m3uIndex int) ([]database.StreamInfo, error) { fmt.Printf("Parsing: %s\n", filePath) - var streams []StreamInfo + var streams []database.StreamInfo file, err := os.Open(filePath) if err != nil { @@ -107,13 +115,13 @@ func parseM3UFile(filePath string, m3uIndex int) ([]StreamInfo, error) { scanner := bufio.NewScanner(file) - var currentStream StreamInfo + var currentStream database.StreamInfo for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#EXTINF:") { - currentStream = StreamInfo{} + currentStream = database.StreamInfo{} // Define a regular expression to capture key-value pairs regex := regexp.MustCompile(`(\S+?)="([^"]*?)"`) @@ -144,7 +152,7 @@ func parseM3UFile(filePath string, m3uIndex int) ([]StreamInfo, error) { } } else if strings.HasPrefix(line, "http") { // Extract URL - currentStream.URLs = []StreamURL{ + currentStream.URLs = []database.StreamURL{ { Content: line, M3UIndex: m3uIndex, diff --git a/m3u/save.go b/m3u/save.go deleted file mode 100644 index 32ea2502..00000000 --- a/m3u/save.go +++ /dev/null @@ -1,39 +0,0 @@ -package m3u - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" -) - -func saveToJSON(streams []StreamInfo) error { - filename := filepath.Join(".", "data", "database.json") - data, err := json.MarshalIndent(streams, "", " ") - if err != nil { - return fmt.Errorf("error marshalling to JSON: %v", err) - } - - err = os.WriteFile(filename, data, 0644) - if err != nil { - return fmt.Errorf("error writing JSON file: %v", err) - } - - return nil -} - -func loadFromJSON() ([]StreamInfo, error) { - filename := filepath.Join(".", "data", "database.json") - data, err := os.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("error reading JSON file: %v", err) - } - - var streams []StreamInfo - err = json.Unmarshal(data, &streams) - if err != nil { - return nil, fmt.Errorf("error unmarshalling from JSON: %v", err) - } - - return streams, nil -}