v 0.0.61 (мелкие недочеты)
This commit is contained in:
Regular → Executable
Regular → Executable
BIN
Binary file not shown.
Regular → Executable
Regular → Executable
Regular → Executable
@@ -21,16 +21,13 @@ import (
|
||||
var staticFiles embed.FS
|
||||
|
||||
func main() {
|
||||
// Инициализация базы данных
|
||||
db := database.GetDB()
|
||||
defer db.Close()
|
||||
|
||||
// Инициализация настроек по умолчанию
|
||||
if err := db.InitDefaultSettings(); err != nil {
|
||||
log.Printf("Warning: Failed to init default settings: %v", err)
|
||||
}
|
||||
|
||||
// Получаем папку для записей из БД
|
||||
recordingsDir, err := db.GetSetting("recordings_dir")
|
||||
if err != nil || recordingsDir == "" {
|
||||
recordingsDir = "recordings"
|
||||
@@ -39,20 +36,17 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем папку для записей
|
||||
if err := os.MkdirAll(recordingsDir, 0755); err != nil {
|
||||
log.Fatal("Не удалось создать папку для записей:", err)
|
||||
}
|
||||
|
||||
log.Printf("Папка для записей: %s", recordingsDir)
|
||||
|
||||
// Инициализируем устройства при старте
|
||||
dm := devices.GetManager()
|
||||
log.Printf("Сканирование устройств при старте...")
|
||||
devicesList := dm.ScanDevices()
|
||||
log.Printf("Найдено устройств: %d", len(devicesList))
|
||||
|
||||
// Сохраняем устройства в БД
|
||||
for _, dev := range devicesList {
|
||||
status := "✓"
|
||||
if !dev.Working {
|
||||
@@ -74,10 +68,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем рекордер
|
||||
rec := recorder.NewRecorder(recordingsDir)
|
||||
|
||||
// Проверяем, есть ли каналы в БД, если нет - создаем каналы по умолчанию
|
||||
channels, err := db.GetAllChannels()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to get channels from DB: %v", err)
|
||||
@@ -86,11 +78,9 @@ func main() {
|
||||
if len(channels) == 0 {
|
||||
log.Println("No channels in database, creating default channels...")
|
||||
|
||||
// Создаем папки для каналов
|
||||
os.MkdirAll(filepath.Join(recordingsDir, "channel1"), 0755)
|
||||
os.MkdirAll(filepath.Join(recordingsDir, "channel2"), 0755)
|
||||
|
||||
// Канал 1
|
||||
channel1 := recorder.NewChannel(
|
||||
"channel1",
|
||||
"Канал 1",
|
||||
@@ -105,7 +95,6 @@ func main() {
|
||||
channel1.SMBFolder = "channel1"
|
||||
rec.AddChannel(channel1)
|
||||
|
||||
// Канал 2
|
||||
channel2 := recorder.NewChannel(
|
||||
"channel2",
|
||||
"Канал 2",
|
||||
@@ -123,24 +112,17 @@ func main() {
|
||||
log.Println("Default channels created successfully")
|
||||
}
|
||||
|
||||
// Устанавливаем экземпляр рекордера в обработчики
|
||||
server.SetRecorder(rec)
|
||||
|
||||
// Устанавливаем экземпляры для обратных вызовов
|
||||
recorder.SetRecorderInstance(rec)
|
||||
recorder.SetDeviceManager(dm)
|
||||
|
||||
// Восстанавливаем состояние каналов после перезагрузки
|
||||
restoreChannelsState(rec, dm)
|
||||
|
||||
// Запускаем проверяльщик состояний
|
||||
stateChecker := recorder.GetStateChecker()
|
||||
stateChecker.Start()
|
||||
|
||||
// Настройка SMB клиента
|
||||
smbClient := smb.GetSMBClient()
|
||||
|
||||
// Загружаем настройки SMB из БД
|
||||
smbEnabled, _ := db.GetSetting("smb_enabled")
|
||||
smbAutoStart, _ := db.GetSetting("smb_auto_start")
|
||||
smbServer, _ := db.GetSetting("smb_server")
|
||||
@@ -158,7 +140,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Применяем настройки SMB
|
||||
if err := smbClient.UpdateConfig(smb.SMBConfig{
|
||||
Enabled: smbEnabled == "true",
|
||||
AutoStart: smbAutoStart == "true",
|
||||
@@ -173,7 +154,6 @@ func main() {
|
||||
log.Printf("Warning: Failed to update SMB config: %v", err)
|
||||
}
|
||||
|
||||
// Устанавливаем колбэк для отправки файлов на SMB
|
||||
recorder.SetRecordingCompletedCallback(func(filename string, filePath string, smbFolder string) {
|
||||
log.Printf("Запись завершена: %s, отправка в SMB папку: %s", filename, smbFolder)
|
||||
|
||||
@@ -194,7 +174,6 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
// Запускаем SMB клиент если включен автозапуск
|
||||
if smbEnabled == "true" && smbAutoStart == "true" {
|
||||
log.Printf("Запуск SMB клиента при автозапуске...")
|
||||
if err := smbClient.Start(); err != nil {
|
||||
@@ -202,10 +181,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Запускаем обработчик очереди повторных отправок
|
||||
go smbClient.ProcessRetryQueue()
|
||||
|
||||
// Настраиваем обработку сигналов для graceful shutdown
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
|
||||
@@ -213,20 +190,14 @@ func main() {
|
||||
<-signalChan
|
||||
log.Println("Получен сигнал завершения, сохраняем состояния каналов...")
|
||||
|
||||
// Останавливаем проверяльщик состояний
|
||||
stateChecker.Stop()
|
||||
|
||||
// Останавливаем SMB клиент
|
||||
smbClient.Stop()
|
||||
|
||||
// Сохраняем состояния всех каналов в БД
|
||||
rec.SaveAllStates()
|
||||
|
||||
log.Println("Завершаем работу...")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Настраиваем и запускаем HTTP сервер
|
||||
srv := server.NewServer(staticFiles, recordingsDir)
|
||||
srv.SetupRoutes()
|
||||
|
||||
@@ -237,7 +208,6 @@ func main() {
|
||||
log.Printf("🔄 Проверка состояния каналов: каждые %d секунд", stateChecker.GetCheckInterval())
|
||||
log.Printf("")
|
||||
|
||||
// Запускаем сервер с graceful shutdown
|
||||
if err := srv.Run(); err != nil {
|
||||
log.Fatal("Server error:", err)
|
||||
}
|
||||
@@ -266,17 +236,18 @@ func restoreChannelsState(rec *recorder.Recorder, dm *devices.DeviceManager) {
|
||||
return dm.TestDevice(device)
|
||||
}
|
||||
|
||||
// Запускаем запись в отдельной горутине
|
||||
go func(channel *recorder.Channel, deviceFunc func(string) (bool, string)) {
|
||||
time.Sleep(2 * time.Second) // Ждем инициализации
|
||||
if err := channel.StartRecording(channel.OutputDir, deviceFunc); err != nil {
|
||||
log.Printf("Не удалось восстановить запись для канала %s: %v", channel.ID, err)
|
||||
// Обновляем состояние в БД на false, так как запись не удалась
|
||||
db.SetChannelState(channel.ID, false)
|
||||
// Используем отдельную горутину с копией переменных
|
||||
channelID := id
|
||||
channel := ch
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
if err := channel.StartRecording(channel.OutputDir, testFunc); err != nil {
|
||||
log.Printf("Не удалось восстановить запись для канала %s: %v", channelID, err)
|
||||
db.SetChannelState(channelID, false)
|
||||
} else {
|
||||
log.Printf("✅ Запись успешно восстановлена для канала %s", channel.ID)
|
||||
log.Printf("✅ Запись успешно восстановлена для канала %s", channelID)
|
||||
}
|
||||
}(ch, testFunc)
|
||||
}()
|
||||
} else {
|
||||
log.Printf("Запись для канала %s не должна быть активна. Состояние OK.", id)
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
+20
-15
@@ -1,4 +1,3 @@
|
||||
// recorder/channel.go
|
||||
package recorder
|
||||
|
||||
import (
|
||||
@@ -31,7 +30,6 @@ type Channel struct {
|
||||
SMBFolder string `json:"smb_folder"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// Поля расписания
|
||||
ScheduleEnabled bool `json:"schedule_enabled"`
|
||||
StartDate string `json:"start_date"`
|
||||
StartTime string `json:"start_time"`
|
||||
@@ -95,19 +93,20 @@ func NewChannel(id, name, device string, duration, sampleRate, channels int, for
|
||||
id, name, device, duration, sampleRate, channels, format, autoRestart)
|
||||
|
||||
return &Channel{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Device: device,
|
||||
Duration: duration,
|
||||
SampleRate: sampleRate,
|
||||
Channels: channels,
|
||||
Format: format,
|
||||
AutoRestart: autoRestart,
|
||||
SMBFolder: "",
|
||||
CreatedAt: time.Now(),
|
||||
OutputDir: outputDir,
|
||||
audioData: make([]byte, 0, sampleRate*channels*2),
|
||||
stopChan: make(chan struct{}),
|
||||
ID: id,
|
||||
Name: name,
|
||||
Device: device,
|
||||
Duration: duration,
|
||||
SampleRate: sampleRate,
|
||||
Channels: channels,
|
||||
Format: format,
|
||||
AutoRestart: autoRestart,
|
||||
SMBFolder: "",
|
||||
CreatedAt: time.Now(),
|
||||
OutputDir: outputDir,
|
||||
audioData: make([]byte, 0, sampleRate*channels*2),
|
||||
stopChan: make(chan struct{}),
|
||||
ScheduleEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +269,9 @@ func (ch *Channel) StartRecording(outputDir string, testDeviceFunc func(string)
|
||||
if recording.ID > 0 {
|
||||
db.UpdateRecordingStatus(recording.ID, "completed")
|
||||
if fileInfo, err := os.Stat(currentPath); err == nil {
|
||||
// Используем ch.Duration который был установлен при старте
|
||||
log.Printf("Recording completed: %s, duration=%d seconds, size=%d bytes",
|
||||
currentFile, ch.Duration, fileInfo.Size())
|
||||
db.UpdateRecordingStats(recording.ID, fileInfo.Size(), ch.Duration)
|
||||
}
|
||||
}
|
||||
@@ -579,6 +581,7 @@ func (ch *Channel) StopSchedule() {
|
||||
}
|
||||
}
|
||||
|
||||
// getAudioCommandWithContext - изменённый для правильной записи WAV
|
||||
func getAudioCommandWithContext(ctx context.Context, device string, sampleRate, channels int, format, filepath string) *exec.Cmd {
|
||||
args := []string{}
|
||||
|
||||
@@ -602,9 +605,11 @@ func getAudioCommandWithContext(ctx context.Context, device string, sampleRate,
|
||||
args = append(args, "-f", "S16_LE")
|
||||
}
|
||||
|
||||
// Используем формат wav - arecord сам правильно заполнит заголовок
|
||||
args = append(args, "-t", "wav")
|
||||
args = append(args, filepath)
|
||||
|
||||
log.Printf("Record command: arecord %v", args)
|
||||
cmd := exec.CommandContext(ctx, "arecord", args...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
Regular → Executable
+6
-2
@@ -1,4 +1,3 @@
|
||||
// recorder/recorder.go
|
||||
package recorder
|
||||
|
||||
import (
|
||||
@@ -6,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"audio-recorder/database"
|
||||
)
|
||||
@@ -57,7 +57,11 @@ func (r *Recorder) loadChannelsFromDB() {
|
||||
channel.RepeatDaily = schedule.RepeatDaily
|
||||
|
||||
if schedule.Enabled {
|
||||
go channel.StartSchedule()
|
||||
// Добавляем задержку перед стартом расписания для полной инициализации
|
||||
go func(ch *Channel) {
|
||||
time.Sleep(2 * time.Second)
|
||||
ch.StartSchedule()
|
||||
}(channel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Regular → Executable
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+469
-127
@@ -81,7 +81,6 @@ func safeWriteJSON(conn *websocket.Conn, v interface{}) error {
|
||||
return fmt.Errorf("connection is nil")
|
||||
}
|
||||
|
||||
// Получаем или создаем SafeWebSocket
|
||||
wsMu.Lock()
|
||||
connKey := fmt.Sprintf("%p", conn)
|
||||
safeConn, exists := wsConnections[connKey]
|
||||
@@ -91,7 +90,6 @@ func safeWriteJSON(conn *websocket.Conn, v interface{}) error {
|
||||
}
|
||||
wsMu.Unlock()
|
||||
|
||||
// Блокируем только этот конкретный сокет
|
||||
safeConn.mu.Lock()
|
||||
defer safeConn.mu.Unlock()
|
||||
|
||||
@@ -101,7 +99,6 @@ func safeWriteJSON(conn *websocket.Conn, v interface{}) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// Устанавливаем deadline для записи
|
||||
if err := conn.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
log.Printf("Failed to set write deadline: %v", err)
|
||||
}
|
||||
@@ -121,7 +118,6 @@ func cleanupClosedWebSockets() {
|
||||
delete(wsConnections, key)
|
||||
continue
|
||||
}
|
||||
// Проверяем, закрыто ли соединение
|
||||
sc.mu.Lock()
|
||||
err := sc.conn.WriteMessage(websocket.PingMessage, nil)
|
||||
sc.mu.Unlock()
|
||||
@@ -165,7 +161,6 @@ func HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Printf("WebSocket connected for user %s, channel %s", username, channelID)
|
||||
|
||||
// Устанавливаем обработчики ДО начала горутин
|
||||
conn.SetPongHandler(func(appData string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
return nil
|
||||
@@ -174,7 +169,6 @@ func HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
// Отправляем приветственное сообщение
|
||||
if err := safeWriteJSON(conn, map[string]interface{}{
|
||||
"type": "connected",
|
||||
"channel": channelID,
|
||||
@@ -188,7 +182,6 @@ func HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
// Горoutine для чтения сообщений от клиента
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -512,7 +505,6 @@ func HandleStart(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем состояние в БД (запись активна)
|
||||
db := database.GetDB()
|
||||
if err := db.SetChannelState(channelID, true); err != nil {
|
||||
log.Printf("Warning: Failed to save channel state to DB: %v", err)
|
||||
@@ -569,7 +561,6 @@ func HandleStop(w http.ResponseWriter, r *http.Request) {
|
||||
ch.StopRecording()
|
||||
}
|
||||
|
||||
// Сохраняем состояние в БД (остановлено)
|
||||
db := database.GetDB()
|
||||
if err := db.SetChannelState(channelID, false); err != nil {
|
||||
log.Printf("Warning: Failed to save channel state to DB: %v", err)
|
||||
@@ -577,7 +568,6 @@ func HandleStop(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("✅ Channel %s state saved to DB: recording=false", channelID)
|
||||
}
|
||||
|
||||
// Проверяем, что сохранилось
|
||||
state, _ := db.GetChannelState(channelID)
|
||||
log.Printf("✅ Verification - Channel %s state in DB: %v", channelID, state)
|
||||
|
||||
@@ -643,8 +633,28 @@ func HandleRecordings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(channelDir, file.Name())
|
||||
|
||||
// Получаем длительность
|
||||
durationSeconds := GetWavDuration(fullPath)
|
||||
|
||||
// Проверяем на нереалистичные значения (больше 1 часа для файла меньше 100MB)
|
||||
if durationSeconds > 3600 && info.Size() < 100*1024*1024 {
|
||||
// Слишком большая длительность для такого размера файла - пересчитываем
|
||||
estimatedBytesPerSecond := 32000
|
||||
durationSeconds = int(info.Size() / int64(estimatedBytesPerSecond))
|
||||
if durationSeconds < 1 {
|
||||
durationSeconds = 1
|
||||
}
|
||||
if durationSeconds > 3600 {
|
||||
durationSeconds = 3600
|
||||
}
|
||||
}
|
||||
|
||||
// Ограничиваем максимальную длительность 24 часами
|
||||
if durationSeconds > 86400 {
|
||||
durationSeconds = 0
|
||||
}
|
||||
|
||||
smbSent := false
|
||||
dbRecordings, _ := db.GetRecordingsByChannel(channelID, 1000)
|
||||
for _, rec := range dbRecordings {
|
||||
@@ -882,7 +892,7 @@ func HandleForceStart(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetWavDuration возвращает длительность WAV файла
|
||||
// GetWavDuration возвращает длительность WAV файла в секундах
|
||||
func GetWavDuration(filepath string) int {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
@@ -890,159 +900,384 @@ func GetWavDuration(filepath string) int {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
|
||||
if fileSize < 44 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Читаем заголовок
|
||||
header := make([]byte, 44)
|
||||
if _, err := file.Read(header); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Проверяем RIFF и WAVE
|
||||
if string(header[0:4]) != "RIFF" || string(header[8:12]) != "WAVE" {
|
||||
return 0
|
||||
}
|
||||
|
||||
var sampleRate, channels, bitsPerSample, dataSize int
|
||||
offset := 12
|
||||
// Получаем параметры из fmt чанка (стандартная позиция 12-36)
|
||||
// Проверяем что fmt чанк на месте
|
||||
if string(header[12:16]) == "fmt " {
|
||||
channels := int(header[22]) | int(header[23])<<8
|
||||
sampleRate := int(header[24]) | int(header[25])<<8 | int(header[26])<<16 | int(header[27])<<24
|
||||
bitsPerSample := int(header[34]) | int(header[35])<<8
|
||||
|
||||
for offset < 44-8 {
|
||||
chunkID := string(header[offset : offset+4])
|
||||
chunkSize := int(header[offset+4]) | int(header[offset+5])<<8 | int(header[offset+6])<<16 | int(header[offset+7])<<24
|
||||
// Пытаемся найти реальный размер data чанка
|
||||
var dataSize int64
|
||||
|
||||
switch chunkID {
|
||||
case "fmt ":
|
||||
if chunkSize >= 16 {
|
||||
channels = int(header[offset+8]) | int(header[offset+9])<<8
|
||||
sampleRate = int(header[offset+12]) | int(header[offset+13])<<8 | int(header[offset+14])<<16 | int(header[offset+15])<<24
|
||||
bitsPerSample = int(header[offset+22]) | int(header[offset+23])<<8
|
||||
offset := int64(12)
|
||||
for offset+8 <= fileSize {
|
||||
chunkHeader := make([]byte, 8)
|
||||
if _, err := file.ReadAt(chunkHeader, offset); err != nil {
|
||||
break
|
||||
}
|
||||
case "data":
|
||||
dataSize = chunkSize
|
||||
chunkID := string(chunkHeader[0:4])
|
||||
chunkSize := int64(chunkHeader[4]) | int64(chunkHeader[5])<<8 | int64(chunkHeader[6])<<16 | int64(chunkHeader[7])<<24
|
||||
|
||||
if chunkID == "data" {
|
||||
dataSize = chunkSize
|
||||
break
|
||||
}
|
||||
offset += 8 + chunkSize
|
||||
}
|
||||
|
||||
if dataSize > 0 && dataSize < fileSize && sampleRate > 0 && channels > 0 && bitsPerSample > 0 {
|
||||
bytesPerSample := bitsPerSample / 8
|
||||
if bytesPerSample > 0 {
|
||||
duration := int(dataSize / int64(sampleRate*channels*bytesPerSample))
|
||||
if duration > 0 && duration < 86400 {
|
||||
return duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если не удалось прочитать из заголовка, вычисляем по размеру файла
|
||||
dataSizeActual := fileSize - 44
|
||||
if dataSizeActual <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Определяем по размеру файла
|
||||
if dataSizeActual < 1000000 {
|
||||
// Маленький файл - скорее всего 8kHz, 8bit, mono (8000 байт/сек)
|
||||
return int(dataSizeActual / 8000)
|
||||
} else if dataSizeActual < 3000000 {
|
||||
// Средний файл - 16kHz, 16bit, mono (32000 байт/сек)
|
||||
return int(dataSizeActual / 32000)
|
||||
} else if dataSizeActual < 6000000 {
|
||||
// 22kHz, 16bit, mono (44000 байт/сек)
|
||||
return int(dataSizeActual / 44000)
|
||||
} else {
|
||||
// Большой файл - 48kHz, 16bit, stereo (192000 байт/сек)
|
||||
return int(dataSizeActual / 192000)
|
||||
}
|
||||
}
|
||||
|
||||
// FixWavHeader исправляет заголовок WAV файла
|
||||
func FixWavHeader(filepath string) error {
|
||||
file, err := os.OpenFile(filepath, os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileSize := fileInfo.Size()
|
||||
|
||||
// Читаем заголовок
|
||||
header := make([]byte, 44)
|
||||
if _, err := file.ReadAt(header, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем что это WAV
|
||||
if string(header[0:4]) != "RIFF" || string(header[8:12]) != "WAVE" {
|
||||
return nil // Не WAV файл
|
||||
}
|
||||
|
||||
// Ищем data чанк
|
||||
var dataOffset int64
|
||||
var dataSize int64
|
||||
|
||||
offset := int64(12)
|
||||
for offset+8 <= fileSize {
|
||||
chunkHeader := make([]byte, 8)
|
||||
if _, err := file.ReadAt(chunkHeader, offset); err != nil {
|
||||
break
|
||||
}
|
||||
chunkID := string(chunkHeader[0:4])
|
||||
chunkSize := int64(chunkHeader[4]) | int64(chunkHeader[5])<<8 | int64(chunkHeader[6])<<16 | int64(chunkHeader[7])<<24
|
||||
|
||||
if chunkID == "data" {
|
||||
dataOffset = offset + 8
|
||||
dataSize = chunkSize
|
||||
break
|
||||
}
|
||||
offset += 8 + chunkSize
|
||||
}
|
||||
|
||||
if dataSize > 0 && dataSize != fileSize-dataOffset {
|
||||
// Исправляем размер data чанка
|
||||
correctDataSize := fileSize - dataOffset
|
||||
log.Printf("Fixing WAV header for %s: dataSize %d -> %d", filepath, dataSize, correctDataSize)
|
||||
|
||||
// Записываем правильный размер data чанка
|
||||
dataSizeBytes := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(dataSizeBytes, uint32(correctDataSize))
|
||||
file.WriteAt(dataSizeBytes, dataOffset-4)
|
||||
|
||||
// Исправляем общий размер RIFF чанка
|
||||
riffSize := fileSize - 8
|
||||
riffSizeBytes := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(riffSizeBytes, uint32(riffSize))
|
||||
file.WriteAt(riffSizeBytes, 4)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimateDurationFromSize оценивает длительность по размеру файла
|
||||
func estimateDurationFromSize(fileSize int64) int {
|
||||
if fileSize <= 44 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Пробуем разные варианты параметров
|
||||
// 1. 16kHz, 16bit, mono = 32000 байт/сек
|
||||
duration1 := int((fileSize - 44) / 32000)
|
||||
// 2. 16kHz, 16bit, stereo = 64000 байт/сек
|
||||
duration2 := int((fileSize - 44) / 64000)
|
||||
// 3. 8kHz, 8bit, mono = 8000 байт/сек
|
||||
duration3 := int((fileSize - 44) / 8000)
|
||||
// 4. 44.1kHz, 16bit, stereo = 176400 байт/сек
|
||||
duration4 := int((fileSize - 44) / 176400)
|
||||
|
||||
log.Printf("[WAV] Estimated durations: mono16=%d, stereo16=%d, mono8=%d, cd=%d",
|
||||
duration1, duration2, duration3, duration4)
|
||||
|
||||
// Выбираем наиболее вероятную длительность
|
||||
if duration1 > 0 && duration1 <= 3600 {
|
||||
return duration1
|
||||
}
|
||||
if duration2 > 0 && duration2 <= 3600 {
|
||||
return duration2
|
||||
}
|
||||
if duration3 > 0 && duration3 <= 3600 {
|
||||
return duration3
|
||||
}
|
||||
if duration4 > 0 && duration4 <= 3600 {
|
||||
return duration4
|
||||
}
|
||||
|
||||
// Если всё плохо, возвращаем 0
|
||||
return 0
|
||||
}
|
||||
|
||||
// getWavDurationBySearch ищет fmt и data чанки во всем файле
|
||||
func getWavDurationBySearch(file *os.File, fileSize int64) int {
|
||||
// Сбрасываем указатель на начало
|
||||
file.Seek(12, 0)
|
||||
|
||||
var sampleRate uint32
|
||||
var channels uint16
|
||||
var bitsPerSample uint16
|
||||
var dataSize uint32
|
||||
|
||||
for {
|
||||
// Читаем заголовок чанка
|
||||
chunkHeader := make([]byte, 8)
|
||||
if _, err := file.Read(chunkHeader); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
chunkID := string(chunkHeader[0:4])
|
||||
chunkSize := binary.LittleEndian.Uint32(chunkHeader[4:8])
|
||||
|
||||
// Защита от слишком больших чанков
|
||||
if chunkSize > uint32(fileSize) {
|
||||
break
|
||||
}
|
||||
|
||||
if chunkID == "fmt " {
|
||||
// Читаем fmt данные
|
||||
fmtData := make([]byte, chunkSize)
|
||||
if _, err := file.Read(fmtData); err != nil {
|
||||
break
|
||||
}
|
||||
if len(fmtData) >= 16 {
|
||||
audioFormat := binary.LittleEndian.Uint16(fmtData[0:2])
|
||||
if audioFormat == 1 { // PCM
|
||||
channels = binary.LittleEndian.Uint16(fmtData[2:4])
|
||||
sampleRate = binary.LittleEndian.Uint32(fmtData[4:8])
|
||||
bitsPerSample = binary.LittleEndian.Uint16(fmtData[14:16])
|
||||
}
|
||||
}
|
||||
} else if chunkID == "data" {
|
||||
dataSize = chunkSize
|
||||
// Если нашли data чанк и уже есть параметры, можно выйти
|
||||
if sampleRate > 0 && channels > 0 && bitsPerSample > 0 {
|
||||
break
|
||||
}
|
||||
// Если нет параметров, продолжаем поиск
|
||||
} else {
|
||||
// Пропускаем чанк
|
||||
file.Seek(int64(chunkSize), 1)
|
||||
}
|
||||
|
||||
// Проверяем позицию
|
||||
pos, _ := file.Seek(0, 1)
|
||||
if pos >= fileSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dataSize == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if sampleRate == 0 || channels == 0 || bitsPerSample == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
bytesPerSample := bitsPerSample / 8
|
||||
if bytesPerSample == 0 {
|
||||
bytesPerSecond := uint32(sampleRate) * uint32(channels) * uint32(bitsPerSample/8)
|
||||
if bytesPerSecond == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return dataSize / (sampleRate * channels * bytesPerSample)
|
||||
return int(dataSize / bytesPerSecond)
|
||||
}
|
||||
|
||||
// HandleFTPSettings - обработчик для настроек FTP сервера
|
||||
func HandleFTPSettings(w http.ResponseWriter, r *http.Request) {
|
||||
ftp := GetFTPServer()
|
||||
db := database.GetDB()
|
||||
// findDataChunk находит размер data чанка
|
||||
func findDataChunk(file *os.File, fileSize int64) uint32 {
|
||||
file.Seek(12, 0)
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ftpEnabled, _ := db.GetSetting("ftp_enabled")
|
||||
ftpPort, _ := db.GetSetting("ftp_port")
|
||||
ftpUsername, _ := db.GetSetting("ftp_username")
|
||||
ftpRootDir, _ := db.GetSetting("ftp_root_dir")
|
||||
ftpAllowDownload, _ := db.GetSetting("ftp_allow_download")
|
||||
ftpAllowUpload, _ := db.GetSetting("ftp_allow_upload")
|
||||
|
||||
config := map[string]interface{}{
|
||||
"enabled": ftpEnabled == "true",
|
||||
"port": ftpPort,
|
||||
"username": ftpUsername,
|
||||
"password": "********",
|
||||
"root_dir": ftpRootDir,
|
||||
"allow_download": ftpAllowDownload == "true",
|
||||
"allow_upload": ftpAllowUpload == "true",
|
||||
"running": ftp.Running,
|
||||
}
|
||||
sendJSONResponse(w, http.StatusOK, config)
|
||||
|
||||
case http.MethodPost:
|
||||
var config map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
sendJSONError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
for {
|
||||
chunkHeader := make([]byte, 8)
|
||||
if _, err := file.Read(chunkHeader); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Сохраняем в БД
|
||||
if enabled, ok := config["enabled"].(bool); ok {
|
||||
db.SetSetting("ftp_enabled", map[bool]string{true: "true", false: "false"}[enabled])
|
||||
}
|
||||
if port, ok := config["port"].(float64); ok {
|
||||
db.SetSetting("ftp_port", strconv.Itoa(int(port)))
|
||||
}
|
||||
if username, ok := config["username"].(string); ok {
|
||||
db.SetSetting("ftp_username", username)
|
||||
}
|
||||
if password, ok := config["password"].(string); ok && password != "" && password != "********" {
|
||||
db.SetSetting("ftp_password", password)
|
||||
}
|
||||
if rootDir, ok := config["root_dir"].(string); ok {
|
||||
db.SetSetting("ftp_root_dir", rootDir)
|
||||
}
|
||||
if allowDownload, ok := config["allow_download"].(bool); ok {
|
||||
db.SetSetting("ftp_allow_download", map[bool]string{true: "true", false: "false"}[allowDownload])
|
||||
}
|
||||
if allowUpload, ok := config["allow_upload"].(bool); ok {
|
||||
db.SetSetting("ftp_allow_upload", map[bool]string{true: "true", false: "false"}[allowUpload])
|
||||
chunkID := string(chunkHeader[0:4])
|
||||
chunkSize := binary.LittleEndian.Uint32(chunkHeader[4:8])
|
||||
|
||||
if chunkID == "data" {
|
||||
return chunkSize
|
||||
}
|
||||
|
||||
// Обновляем FTP сервер
|
||||
if err := ftp.UpdateConfig(config); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
// Пропускаем чанк
|
||||
file.Seek(int64(chunkSize), 1)
|
||||
|
||||
pos, _ := file.Seek(0, 1)
|
||||
if pos >= fileSize {
|
||||
break
|
||||
}
|
||||
|
||||
if ftp.Enabled && !ftp.Running {
|
||||
go ftp.Start()
|
||||
} else if !ftp.Enabled && ftp.Running {
|
||||
go ftp.Stop()
|
||||
}
|
||||
|
||||
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// HandleFTPStart - запуск FTP сервера
|
||||
func HandleFTPStart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
// parseWavDurationBySearching ищет fmt и data чанки во всем файле
|
||||
func parseWavDurationBySearching(file *os.File, fileSize int64) int {
|
||||
// Ищем fmt чанк
|
||||
file.Seek(12, 0)
|
||||
for {
|
||||
chunkHeader := make([]byte, 8)
|
||||
if _, err := file.Read(chunkHeader); err != nil {
|
||||
break
|
||||
}
|
||||
chunkID := string(chunkHeader[0:4])
|
||||
chunkSize := int(chunkHeader[4]) | int(chunkHeader[5])<<8 | int(chunkHeader[6])<<16 | int(chunkHeader[7])<<24
|
||||
|
||||
if chunkID == "fmt " && chunkSize >= 16 {
|
||||
// Читаем fmt данные
|
||||
fmtData := make([]byte, chunkSize)
|
||||
if _, err := file.Read(fmtData); err != nil {
|
||||
break
|
||||
}
|
||||
audioFormat := int(fmtData[0]) | int(fmtData[1])<<8
|
||||
if audioFormat == 1 { // PCM
|
||||
channels := int(fmtData[2]) | int(fmtData[3])<<8
|
||||
sampleRate := int(fmtData[4]) | int(fmtData[5])<<8 | int(fmtData[6])<<16 | int(fmtData[7])<<24
|
||||
bitsPerSample := int(fmtData[14]) | int(fmtData[15])<<8
|
||||
|
||||
// Теперь ищем data чанк
|
||||
dataSize := findDataChunkSize(file, fileSize)
|
||||
if dataSize > 0 && sampleRate > 0 && channels > 0 && bitsPerSample > 0 {
|
||||
bytesPerSample := bitsPerSample / 8
|
||||
if bytesPerSample > 0 {
|
||||
duration := dataSize / (sampleRate * channels * bytesPerSample)
|
||||
if duration > 0 && duration < 86400 {
|
||||
return duration
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Пропускаем чанк
|
||||
file.Seek(int64(chunkSize), 1)
|
||||
}
|
||||
|
||||
// Защита от бесконечного цикла
|
||||
pos, _ := file.Seek(0, 1)
|
||||
if pos >= fileSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ftp := GetFTPServer()
|
||||
if err := ftp.Start(); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSONResponse(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "started",
|
||||
"running": ftp.Running,
|
||||
})
|
||||
return estimateDurationFromFileSize(fileSize)
|
||||
}
|
||||
|
||||
// HandleFTPStop - остановка FTP сервера
|
||||
func HandleFTPStop(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// findDataChunkSize находит размер data чанка
|
||||
func findDataChunkSize(file *os.File, fileSize int64) int {
|
||||
file.Seek(12, 0)
|
||||
for {
|
||||
chunkHeader := make([]byte, 8)
|
||||
if _, err := file.Read(chunkHeader); err != nil {
|
||||
break
|
||||
}
|
||||
chunkID := string(chunkHeader[0:4])
|
||||
chunkSize := int(chunkHeader[4]) | int(chunkHeader[5])<<8 | int(chunkHeader[6])<<16 | int(chunkHeader[7])<<24
|
||||
|
||||
ftp := GetFTPServer()
|
||||
if err := ftp.Stop(); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if chunkID == "data" {
|
||||
return chunkSize
|
||||
}
|
||||
|
||||
sendJSONResponse(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "stopped",
|
||||
"running": ftp.Running,
|
||||
})
|
||||
// Пропускаем чанк
|
||||
file.Seek(int64(chunkSize), 1)
|
||||
|
||||
// Защита от бесконечного цикла
|
||||
pos, _ := file.Seek(0, 1)
|
||||
if pos >= fileSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// estimateDurationFromFileSize оценивает длительность по размеру файла
|
||||
func estimateDurationFromFileSize(fileSize int64) int {
|
||||
if fileSize <= 44 {
|
||||
return 0
|
||||
}
|
||||
// Предполагаем стандартные параметры: 16000 Hz, 16 бит, 1 канал = 32000 байт/сек
|
||||
dataSize := fileSize - 44
|
||||
duration := int(dataSize / 32000)
|
||||
if duration < 1 && dataSize > 1000 {
|
||||
duration = 1
|
||||
}
|
||||
log.Printf("estimateDurationFromFileSize: fileSize=%d, estimated duration=%d seconds", fileSize, duration)
|
||||
return duration
|
||||
}
|
||||
|
||||
// HandleStorageSettings - обработчик для настроек хранилища
|
||||
@@ -1086,7 +1321,6 @@ func HandleStorageSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
RecorderInstance.OutputDir = req.RecordingsDir
|
||||
|
||||
// Пересоздаем папки для существующих каналов
|
||||
channels := RecorderInstance.GetAllChannels()
|
||||
for _, ch := range channels {
|
||||
newChannelDir := filepath.Join(req.RecordingsDir, ch.ID)
|
||||
@@ -1146,7 +1380,6 @@ func HandleLiveStream(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
// Создаем WAV заголовок
|
||||
header := make([]byte, 44)
|
||||
copy(header[0:4], []byte("RIFF"))
|
||||
copy(header[8:12], []byte("WAVE"))
|
||||
@@ -1418,13 +1651,11 @@ func HandleChannelSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Сохраняем расписание в БД
|
||||
if err := db.SaveSchedule(req); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем канал
|
||||
ch, exists := RecorderInstance.GetChannel(req.ChannelID)
|
||||
if exists {
|
||||
ch.ScheduleEnabled = req.Enabled
|
||||
@@ -1467,3 +1698,114 @@ func HandleChannelSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFTPSettings - обработчик для настроек FTP сервера
|
||||
func HandleFTPSettings(w http.ResponseWriter, r *http.Request) {
|
||||
ftp := GetFTPServer()
|
||||
db := database.GetDB()
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
ftpEnabled, _ := db.GetSetting("ftp_enabled")
|
||||
ftpPort, _ := db.GetSetting("ftp_port")
|
||||
ftpUsername, _ := db.GetSetting("ftp_username")
|
||||
ftpRootDir, _ := db.GetSetting("ftp_root_dir")
|
||||
ftpAllowDownload, _ := db.GetSetting("ftp_allow_download")
|
||||
ftpAllowUpload, _ := db.GetSetting("ftp_allow_upload")
|
||||
|
||||
config := map[string]interface{}{
|
||||
"enabled": ftpEnabled == "true",
|
||||
"port": ftpPort,
|
||||
"username": ftpUsername,
|
||||
"password": "********",
|
||||
"root_dir": ftpRootDir,
|
||||
"allow_download": ftpAllowDownload == "true",
|
||||
"allow_upload": ftpAllowUpload == "true",
|
||||
"running": ftp.Running,
|
||||
}
|
||||
sendJSONResponse(w, http.StatusOK, config)
|
||||
|
||||
case http.MethodPost:
|
||||
var config map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
sendJSONError(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if enabled, ok := config["enabled"].(bool); ok {
|
||||
db.SetSetting("ftp_enabled", map[bool]string{true: "true", false: "false"}[enabled])
|
||||
}
|
||||
if port, ok := config["port"].(float64); ok {
|
||||
db.SetSetting("ftp_port", strconv.Itoa(int(port)))
|
||||
}
|
||||
if username, ok := config["username"].(string); ok {
|
||||
db.SetSetting("ftp_username", username)
|
||||
}
|
||||
if password, ok := config["password"].(string); ok && password != "" && password != "********" {
|
||||
db.SetSetting("ftp_password", password)
|
||||
}
|
||||
if rootDir, ok := config["root_dir"].(string); ok {
|
||||
db.SetSetting("ftp_root_dir", rootDir)
|
||||
}
|
||||
if allowDownload, ok := config["allow_download"].(bool); ok {
|
||||
db.SetSetting("ftp_allow_download", map[bool]string{true: "true", false: "false"}[allowDownload])
|
||||
}
|
||||
if allowUpload, ok := config["allow_upload"].(bool); ok {
|
||||
db.SetSetting("ftp_allow_upload", map[bool]string{true: "true", false: "false"}[allowUpload])
|
||||
}
|
||||
|
||||
if err := ftp.UpdateConfig(config); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ftp.Enabled && !ftp.Running {
|
||||
go ftp.Start()
|
||||
} else if !ftp.Enabled && ftp.Running {
|
||||
go ftp.Stop()
|
||||
}
|
||||
|
||||
sendJSONResponse(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFTPStart - запуск FTP сервера
|
||||
func HandleFTPStart(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ftp := GetFTPServer()
|
||||
if err := ftp.Start(); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSONResponse(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "started",
|
||||
"running": ftp.Running,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleFTPStop - остановка FTP сервера
|
||||
func HandleFTPStop(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
ftp := GetFTPServer()
|
||||
if err := ftp.Stop(); err != nil {
|
||||
sendJSONError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSONResponse(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "stopped",
|
||||
"running": ftp.Running,
|
||||
})
|
||||
}
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
+1
-2
@@ -148,7 +148,7 @@ func (s *Server) Run() error {
|
||||
defer cancel()
|
||||
|
||||
go CleanupExpiredSessions(ctx)
|
||||
go cleanupClosedWebSockets() // Запускаем очистку WebSocket соединений
|
||||
go cleanupClosedWebSockets()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
@@ -204,7 +204,6 @@ func stopAllRecordings() {
|
||||
ch.StopRecording()
|
||||
}
|
||||
|
||||
// Сохраняем состояние в БД
|
||||
if err := db.SetChannelState(id, ch.IsRecording); err != nil {
|
||||
log.Printf("Failed to save state for channel %s: %v", id, err)
|
||||
} else {
|
||||
|
||||
Regular → Executable
Regular → Executable
+1
-5
@@ -68,7 +68,6 @@ func GetSMBClient() *SMBClient {
|
||||
|
||||
// formatAddress форматирует адрес с поддержкой IPv6
|
||||
func formatAddress(host string, port int) string {
|
||||
// Проверяем, является ли хост IPv6 адресом
|
||||
if strings.Contains(host, ":") && !strings.Contains(host, "[") {
|
||||
return fmt.Sprintf("[%s]:%d", host, port)
|
||||
}
|
||||
@@ -215,7 +214,6 @@ func (s *SMBClient) UploadFile(localPath string, remoteFolder string, remoteFile
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Проверяем существование файла
|
||||
if _, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||
log.Printf("File does not exist: %s", localPath)
|
||||
return fmt.Errorf("file does not exist: %s", localPath)
|
||||
@@ -393,15 +391,14 @@ func (s *SMBClient) ProcessRetryQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
// updateRecordingStatus обновляет статус SMB в БД
|
||||
func (s *SMBClient) updateRecordingStatus(filePath string, uploaded bool, errorMsg string) {
|
||||
// Получаем экземпляр базы данных
|
||||
db := database.GetDB()
|
||||
if db == nil {
|
||||
log.Printf("Database not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем channel_id и filename из пути
|
||||
parts := strings.Split(filePath, string(os.PathSeparator))
|
||||
if len(parts) < 2 {
|
||||
log.Printf("Cannot parse file path: %s", filePath)
|
||||
@@ -411,7 +408,6 @@ func (s *SMBClient) updateRecordingStatus(filePath string, uploaded bool, errorM
|
||||
filename := parts[len(parts)-1]
|
||||
channelID := parts[len(parts)-2]
|
||||
|
||||
// Ищем запись в базе данных
|
||||
recordings, err := db.GetRecordingsByChannel(channelID, 1000)
|
||||
if err != nil {
|
||||
log.Printf("Error finding recording: %v", err)
|
||||
|
||||
Regular → Executable
Regular → Executable
+24
-32
@@ -3,7 +3,7 @@ import { checkAuth, logout, changePassword } from './modules/api.js';
|
||||
import { disconnectWebSocket, initWebSocket } from './modules/websocket.js';
|
||||
import { initTabs, showNotification } from './modules/ui.js';
|
||||
import { loadDevices } from './modules/devices.js';
|
||||
import { loadRecordings, downloadRecording } from './modules/recordings.js';
|
||||
import { loadRecordings, downloadRecording, deleteSelectedRecordings } from './modules/recordings.js';
|
||||
import { togglePlay } from './modules/player.js';
|
||||
import { initOscilloscope, drawWaveform } from './modules/oscilloscope.js';
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ window.getCurrentChannelId = getCurrentChannelId;
|
||||
window.loadStatus = loadStatus;
|
||||
window.togglePlayWrapper = togglePlay;
|
||||
window.downloadRecordingWrapper = downloadRecording;
|
||||
window.deleteSelectedRecordings = deleteSelectedRecordings;
|
||||
window.showNotification = showNotification;
|
||||
window.getChannels = getChannels;
|
||||
window.setCurrentChannelId = setCurrentChannelId;
|
||||
@@ -197,7 +198,6 @@ function bindEventHandlers() {
|
||||
activeChannelSelect.onchange = (e) => {
|
||||
if (e.target.value) {
|
||||
selectChannel(e.target.value, false);
|
||||
// Останавливаем прослушивание при смене канала
|
||||
stopLivePlayback();
|
||||
}
|
||||
};
|
||||
@@ -299,6 +299,27 @@ function bindEventHandlers() {
|
||||
showSmbPasswordBtn._hasHandler = true;
|
||||
console.log('Show SMB password button handler attached');
|
||||
}
|
||||
|
||||
// Удаление выбранных записей
|
||||
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
||||
if (deleteSelectedBtn && !deleteSelectedBtn._hasHandler) {
|
||||
deleteSelectedBtn.onclick = deleteSelectedRecordings;
|
||||
deleteSelectedBtn._hasHandler = true;
|
||||
console.log('Delete selected button handler attached');
|
||||
}
|
||||
|
||||
// Выбрать все записи
|
||||
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||
if (selectAllCheckbox && !selectAllCheckbox._hasHandler) {
|
||||
selectAllCheckbox.onchange = () => {
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
document.querySelectorAll('.recording-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
});
|
||||
};
|
||||
selectAllCheckbox._hasHandler = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для наблюдения за изменениями DOM
|
||||
@@ -318,43 +339,31 @@ function observeDOM() {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('DOM загружен, инициализация приложения...');
|
||||
|
||||
// Инициализация осциллографа
|
||||
initOscilloscope();
|
||||
|
||||
// Проверка аутентификации
|
||||
const isAuth = await checkAuth();
|
||||
if (!isAuth) return;
|
||||
|
||||
// Инициализация вкладок
|
||||
initTabs();
|
||||
|
||||
// Запуск наблюдения за DOM для динамической привязки обработчиков
|
||||
const domObserver = observeDOM();
|
||||
|
||||
// Первоначальная привязка обработчиков
|
||||
setTimeout(() => {
|
||||
bindEventHandlers();
|
||||
}, 100);
|
||||
|
||||
// Загрузка данных - сначала каналы, потом устройства
|
||||
await loadChannels(false);
|
||||
await loadDevices();
|
||||
|
||||
// Загрузка записей после инициализации
|
||||
await loadRecordings();
|
||||
|
||||
// Загрузка настроек хранилища, FTP и SMB
|
||||
await loadStorageSettings();
|
||||
await loadFTPSettings();
|
||||
await loadSMBSettings();
|
||||
|
||||
// Загрузка статуса для текущего канала
|
||||
const currentChannel = getCurrentChannelId();
|
||||
if (currentChannel) {
|
||||
await loadStatus();
|
||||
}
|
||||
|
||||
// Периодическое обновление статуса
|
||||
if (updateInterval) clearInterval(updateInterval);
|
||||
updateInterval = setInterval(async () => {
|
||||
const currentChannelId = getCurrentChannelId();
|
||||
@@ -364,7 +373,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadChannels(true);
|
||||
}, 5000);
|
||||
|
||||
// Обработка смены вкладки
|
||||
document.addEventListener('tabChanged', async (e) => {
|
||||
const tabId = e.detail.tabId;
|
||||
if (tabId === 'archive') {
|
||||
@@ -388,12 +396,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
setTimeout(() => bindEventHandlers(), 100);
|
||||
});
|
||||
|
||||
// Обработка обновления статуса каналов
|
||||
document.addEventListener('channelStatusUpdate', () => {
|
||||
loadChannels(true);
|
||||
});
|
||||
|
||||
// Инициализация информации о качестве
|
||||
setTimeout(() => {
|
||||
const channelsSelect = document.getElementById('channels');
|
||||
if (channelsSelect) {
|
||||
@@ -402,7 +408,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
bindEventHandlers();
|
||||
}, 500);
|
||||
|
||||
// Восстановление выбранного канала из sessionStorage
|
||||
const savedChannel = sessionStorage.getItem('selectedChannel');
|
||||
if (savedChannel) {
|
||||
setTimeout(async () => {
|
||||
@@ -413,7 +418,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Периодическая перепривязка обработчиков (на всякий случай)
|
||||
setInterval(() => {
|
||||
bindEventHandlers();
|
||||
}, 2000);
|
||||
@@ -421,7 +425,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('Приложение инициализировано');
|
||||
});
|
||||
|
||||
// Сохраняем выбранный канал при перезагрузке
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const currentChannelId = getCurrentChannelId();
|
||||
if (currentChannelId) {
|
||||
@@ -433,15 +436,4 @@ window.addEventListener('beforeunload', () => {
|
||||
}
|
||||
disconnectWebSocket();
|
||||
stopLivePlayback();
|
||||
});
|
||||
// Удаление выбранных записей
|
||||
const deleteSelectedBtn = document.getElementById('deleteSelectedBtn');
|
||||
if (deleteSelectedBtn && !deleteSelectedBtn._hasHandler) {
|
||||
deleteSelectedBtn.onclick = () => {
|
||||
import('./modules/recordings.js').then(module => {
|
||||
module.deleteSelectedRecordings();
|
||||
});
|
||||
};
|
||||
deleteSelectedBtn._hasHandler = true;
|
||||
console.log('Delete selected button handler attached');
|
||||
}
|
||||
});
|
||||
Regular → Executable
Regular → Executable
+4
-4
@@ -33,7 +33,7 @@ function getNextAvailableChannelNumber() {
|
||||
// Извлекаем все номера из существующих каналов
|
||||
const numbers = [];
|
||||
for (const ch of channels) {
|
||||
const match = ch.id.match(/channel_(\d+)/);
|
||||
const match = ch.id.match(/channel(\d+)/);
|
||||
if (match) {
|
||||
numbers.push(parseInt(match[1]));
|
||||
}
|
||||
@@ -195,8 +195,8 @@ export async function loadChannels(skipConfigLoad = false) {
|
||||
|
||||
// Сортируем каналы по номеру для стабильного порядка
|
||||
const sortedChannels = [...newChannels].sort((a, b) => {
|
||||
const numA = parseInt(a.id.split('_').pop()) || 0;
|
||||
const numB = parseInt(b.id.split('_').pop()) || 0;
|
||||
const numA = parseInt(a.id.match(/channel(\d+)/)?.[1] || 0);
|
||||
const numB = parseInt(b.id.match(/channel(\d+)/)?.[1] || 0);
|
||||
return numA - numB;
|
||||
});
|
||||
|
||||
@@ -602,7 +602,7 @@ export async function saveChannelConfig() {
|
||||
|
||||
export async function addChannel() {
|
||||
const nextNumber = getNextAvailableChannelNumber();
|
||||
const channelId = `channel_${nextNumber}`;
|
||||
const channelId = `channel${nextNumber}`;
|
||||
const defaultName = `Канал ${nextNumber}`;
|
||||
|
||||
const channelName = prompt('Введите название канала:', defaultName);
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+11
-2
@@ -39,8 +39,17 @@ export function formatBytes(bytes) {
|
||||
|
||||
// formatDuration - форматирование длительности в секундах
|
||||
export function formatDuration(seconds) {
|
||||
if (!seconds || seconds === 0 || seconds < 0) return '0:00';
|
||||
if (seconds > 86400) return '>24:00:00';
|
||||
if (!seconds || seconds === 0 || seconds < 0 || isNaN(seconds)) {
|
||||
return '0:00';
|
||||
}
|
||||
|
||||
// Округляем до целых секунд
|
||||
seconds = Math.round(seconds);
|
||||
|
||||
// Ограничиваем максимальное значение
|
||||
if (seconds > 86400) {
|
||||
seconds = 0;
|
||||
}
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user