Files
iptvc/app/playlist/playlist.go
AnthonyAxenov e98d923ce5 Оптимизация проверки каналов (#5)
- теперь проверяется только первый 1 Кб контента
- скорректирована проверка mpd-контента
2025-05-16 23:09:27 +08:00

222 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2025, Антон Аксенов
* This file is part of iptvc project
* MIT License: https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE
*/
package playlist
import (
"axenov/iptv-checker/app/utils"
"errors"
"os"
"regexp"
"strings"
"time"
)
// Group - структура для хранения информации о группе каналов
type Group struct {
Id string `json:"id"` // Хэш MD5 от названия группы
Name string `json:"name"` // Название группы (тег #EXTGRP или атрибут group-title тега #EXTINF)
Attributes map[string]string `json:"attributes"` // Атрибуты тега #EXTGRP
}
// Channel - структура для хранения информации о канале и статусе его проверки
type Channel struct {
Id string `json:"id"` // Хэш MD5 от ссылки на канал
Title string `json:"title"` // Название канала
URL string `json:"url"` // Ссылка на канал
GroupId string `json:"groupId"` // Хэш MD5 от названия группы канала (тег #EXTGRP или атрибут group-title тега #EXTINF)
Attributes map[string]string `json:"attributes"` // Атрибуты тега #EXTINF
Status int `json:"status"` // Код статуса HTTP
IsOnline bool `json:"isOnline"` // Признак доступности канала (при Status < 400)
Error string `json:"error"` // Текст ошибки (при Status >= 400)
ContentType string `json:"contentType"` // MIME-тип тела ответа
Tags []string `json:"tags"` // Список тегов канала
CheckedAt int64 `json:"checkedAt"` // Время проверки в формате UNIX timestamp
}
// Playlist - структура для хранения информации о плейлисте
type Playlist struct {
Code string `json:"code"` // Код плейлиста (из ini-файла)
Name string `json:"name"` // Название плейлиста (из ini-файла)
Description string `json:"description"` // Описание плейлиста (из ini-файла)
Url string `json:"url"` // URL плейлиста
Source string `json:"source"` // Источник плейлиста (из ini-файла)
Content string `json:"content"` // Содержимое плейлиста (m3u, m3u8)
IsOnline bool `json:"isOnline"` // Признак доступности плейлиста
Attributes map[string]string `json:"attributes"` // Атрибуты тега #EXTM3U
Groups map[string]Group `json:"groups"` // Группы каналов (по тегам #EXTGRP и атрибутам tvg-group тегов #EXTINF)
Channels map[string]Channel `json:"channels"` // Каналы
OnlineCount int `json:"onlineCount"` // Количество рабочих каналов
OfflineCount int `json:"offlineCount"` // Количество нерабочих каналов
CheckedAt int64 `json:"checkedAt"` // Время проверки в формате UNIX timestamp
}
// tmpChannel хранит временные данные о канале, который обрабатывается в Parse
var tmpChannel = Channel{}
// MakeFromFile создаёт экземпляр плейлиста из файла
func MakeFromFile(filepath string) (Playlist, error) {
expandedPath, err := utils.ExpandPath(filepath)
if err != nil {
return Playlist{}, errors.New("File read error: " + err.Error())
}
_, err = os.Stat(expandedPath)
if err != nil {
return Playlist{}, errors.New("File read error: " + err.Error())
}
playlist := Playlist{
Code: "",
Name: "",
Description: "Playlist from filesystem",
Url: expandedPath,
Source: "-f",
IsOnline: true,
}
return playlist, playlist.ReadFromFs()
}
// MakeFromUrl создаёт экземпляр плейлиста из URL-адреса
func MakeFromUrl(url string) (Playlist, error) {
playlist := Playlist{
Code: "",
Name: "",
Description: "Remote playlist",
Url: url,
Source: "-u",
IsOnline: true,
}
return playlist, nil
}
// parseAttributes парсит атрибуты тегов #EXT*
func parseAttributes(line string) map[string]string {
result := make(map[string]string)
regex := regexp.MustCompile(`(?U)([a-z-]+)="(.*)"`)
regexMatches := regex.FindAllStringSubmatch(line, -1)
for _, match := range regexMatches {
result[match[1]] = match[2]
}
return result
}
// parseName парсит название канала из строки тега #EXTINF
func parseName(line string) string {
//TODO https://git.axenov.dev/IPTV/iptvc/issues/7
parts := strings.Split(line, ",")
if len(parts) == 2 {
return strings.Trim(parts[1], " ")
}
regex := regexp.MustCompile(`['"]?\s*,\s*(.+)`)
regexMatches := regex.FindAllStringSubmatch(line, -1)
return regexMatches[0][1]
}
// Download загружает плейлист по URL-адресу
func (pls *Playlist) Download() error {
content, err := utils.Fetch(pls.Url)
if err != nil {
pls.Content = err.Error()
pls.CheckedAt = time.Now().Unix()
return err
}
pls.Content = string(content)
return nil
}
// ReadFromFs читает плейлист из файла
func (pls *Playlist) ReadFromFs() error {
content, err := os.ReadFile(pls.Url)
if err != nil {
pls.Content = err.Error()
pls.CheckedAt = time.Now().Unix()
return err
}
pls.Content = string(content)
return nil
}
// Parse разбирает плейлист
func (pls *Playlist) Parse() Playlist {
isChannel := false
pls.Attributes = make(map[string]string)
pls.Channels = make(map[string]Channel)
pls.Groups = make(map[string]Group)
content := pls.Content
content = strings.ReplaceAll(content, "\r\n", "\n") // replace windows line endings
content = strings.ReplaceAll(content, "\xef\xbb\xbf", "") // remove UTF8 BOM
for _, line := range strings.Split(content, "\n") {
line = strings.Trim(line, "\t\r\n ")
if line == "" || line == "#EXTM3U" {
continue
}
if strings.HasPrefix(line, "#EXTM3U") {
pls.Attributes = parseAttributes(line)
continue
}
if strings.HasPrefix(line, "#EXTINF") {
isChannel = true
tmpChannel.Attributes = parseAttributes(line)
tmpChannel.Title = parseName(line)
if tmpChannel.Title == "" {
if tvgid, ok := tmpChannel.Attributes["tvg-id"]; ok {
tmpChannel.Title = "(канал без названия, tvg-id=" + tvgid + ")"
} else {
tmpChannel.Title = "(канал без названия, tvg-id неизвестен)"
}
}
if groupName, ok := tmpChannel.Attributes["group-title"]; ok {
id := utils.Md5str(groupName)
tmpChannel.GroupId = id
pls.Groups[id] = Group{
Id: id,
Name: groupName,
Attributes: nil,
}
}
continue
}
if isChannel && strings.HasPrefix(line, "#EXTGRP") {
parts := strings.Split(line, ":")
groupName := strings.Trim(parts[1], " ")
id := utils.Md5str(groupName)
tmpChannel.GroupId = id
pls.Groups[id] = Group{
Id: id,
Name: groupName,
Attributes: nil,
}
continue
}
if isChannel && strings.HasPrefix(line, "http") {
tmpChannel.URL = strings.Trim(line, " ")
tmpChannel.Id = utils.Md5str(tmpChannel.URL)
if tmpChannel.Id != "" {
pls.Channels[tmpChannel.Id] = tmpChannel
isChannel = false
tmpChannel = Channel{}
tmpChannel.Attributes = make(map[string]string)
}
}
}
return *pls
}