1+ package dropzone
2+
3+ import (
4+ "fmt"
5+ "io"
6+ "log"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "time"
11+
12+ "scriberr/internal/config"
13+ "scriberr/internal/database"
14+ "scriberr/internal/models"
15+
16+ "github.com/fsnotify/fsnotify"
17+ "github.com/google/uuid"
18+ )
19+
20+ // TaskQueue interface for enqueueing transcription jobs
21+ type TaskQueue interface {
22+ EnqueueJob (jobID string ) error
23+ }
24+
25+ // Service manages the dropzone file monitoring
26+ type Service struct {
27+ config * config.Config
28+ watcher * fsnotify.Watcher
29+ dropzonePath string
30+ taskQueue TaskQueue
31+ }
32+
33+ // NewService creates a new dropzone service
34+ func NewService (cfg * config.Config , taskQueue TaskQueue ) * Service {
35+ return & Service {
36+ config : cfg ,
37+ taskQueue : taskQueue ,
38+ dropzonePath : filepath .Join ("data" , "dropzone" ),
39+ }
40+ }
41+
42+ // Start initializes the dropzone directory and starts file monitoring
43+ func (s * Service ) Start () error {
44+ log .Printf ("Starting dropzone service..." )
45+
46+ // Create dropzone directory if it doesn't exist
47+ if err := os .MkdirAll (s .dropzonePath , 0755 ); err != nil {
48+ return fmt .Errorf ("failed to create dropzone directory: %v" , err )
49+ }
50+
51+ log .Printf ("Dropzone directory created/verified at: %s" , s .dropzonePath )
52+
53+ // Initialize file watcher
54+ watcher , err := fsnotify .NewWatcher ()
55+ if err != nil {
56+ return fmt .Errorf ("failed to create file watcher: %v" , err )
57+ }
58+ s .watcher = watcher
59+
60+ // Add dropzone directory and all subdirectories to watcher recursively
61+ if err := s .addDirectoryRecursively (s .dropzonePath ); err != nil {
62+ s .watcher .Close ()
63+ return fmt .Errorf ("failed to add directories to watcher: %v" , err )
64+ }
65+
66+ // Process existing files recursively on startup
67+ if err := s .processExistingFiles (); err != nil {
68+ log .Printf ("Warning: failed to process some existing files: %v" , err )
69+ }
70+
71+ // Start monitoring in a goroutine
72+ go s .watchFiles ()
73+
74+ log .Printf ("Dropzone service started, monitoring recursively: %s" , s .dropzonePath )
75+ return nil
76+ }
77+
78+ // Stop stops the dropzone service
79+ func (s * Service ) Stop () error {
80+ if s .watcher != nil {
81+ log .Printf ("Stopping dropzone service..." )
82+ return s .watcher .Close ()
83+ }
84+ return nil
85+ }
86+
87+ // addDirectoryRecursively adds a directory and all its subdirectories to the watcher
88+ func (s * Service ) addDirectoryRecursively (root string ) error {
89+ return filepath .Walk (root , func (path string , info os.FileInfo , err error ) error {
90+ if err != nil {
91+ log .Printf ("Warning: error accessing path %s: %v" , path , err )
92+ return nil // Continue walking despite errors
93+ }
94+
95+ // Only add directories to the watcher
96+ if info .IsDir () {
97+ if err := s .watcher .Add (path ); err != nil {
98+ log .Printf ("Warning: failed to watch directory %s: %v" , path , err )
99+ return nil // Continue despite individual directory failures
100+ }
101+ log .Printf ("Added directory to watcher: %s" , path )
102+ }
103+
104+ return nil
105+ })
106+ }
107+
108+ // processExistingFiles processes all existing audio files in the dropzone on startup
109+ func (s * Service ) processExistingFiles () error {
110+ return filepath .Walk (s .dropzonePath , func (path string , info os.FileInfo , err error ) error {
111+ if err != nil {
112+ log .Printf ("Warning: error accessing path %s: %v" , path , err )
113+ return nil // Continue walking despite errors
114+ }
115+
116+ // Only process files, not directories
117+ if ! info .IsDir () {
118+ filename := filepath .Base (path )
119+ if s .isAudioFile (filename ) {
120+ log .Printf ("Processing existing audio file: %s" , path )
121+ s .processFile (path )
122+ }
123+ }
124+
125+ return nil
126+ })
127+ }
128+
129+ // watchFiles monitors the dropzone directory for new files
130+ func (s * Service ) watchFiles () {
131+ for {
132+ select {
133+ case event , ok := <- s .watcher .Events :
134+ if ! ok {
135+ return
136+ }
137+
138+ // Handle creation events for both files and directories
139+ if event .Op & fsnotify .Create == fsnotify .Create {
140+ // Check if the created item is a directory
141+ if info , err := os .Stat (event .Name ); err == nil && info .IsDir () {
142+ log .Printf ("Detected new directory in dropzone: %s" , event .Name )
143+ // Add the new directory to the watcher recursively
144+ if err := s .addDirectoryRecursively (event .Name ); err != nil {
145+ log .Printf ("Failed to watch new directory %s: %v" , event .Name , err )
146+ }
147+ } else {
148+ log .Printf ("Detected new file in dropzone: %s" , event .Name )
149+ s .processFile (event .Name )
150+ }
151+ }
152+
153+ case err , ok := <- s .watcher .Errors :
154+ if ! ok {
155+ return
156+ }
157+ log .Printf ("Dropzone watcher error: %v" , err )
158+ }
159+ }
160+ }
161+
162+ // isAudioFile checks if the file is a valid audio file based on extension
163+ func (s * Service ) isAudioFile (filename string ) bool {
164+ ext := strings .ToLower (filepath .Ext (filename ))
165+ audioExtensions := []string {
166+ ".mp3" , ".wav" , ".flac" , ".m4a" , ".aac" , ".ogg" ,
167+ ".wma" , ".mp4" , ".avi" , ".mov" , ".mkv" , ".webm" ,
168+ }
169+
170+ for _ , validExt := range audioExtensions {
171+ if ext == validExt {
172+ return true
173+ }
174+ }
175+ return false
176+ }
177+
178+ // processFile handles a newly detected file in the dropzone
179+ func (s * Service ) processFile (filePath string ) {
180+ // Small delay to ensure file is fully written
181+ time .Sleep (500 * time .Millisecond )
182+
183+ filename := filepath .Base (filePath )
184+
185+ // Check if it's an audio file
186+ if ! s .isAudioFile (filename ) {
187+ log .Printf ("Skipping non-audio file: %s" , filename )
188+ return
189+ }
190+
191+ // Check if file exists and is accessible
192+ fileInfo , err := os .Stat (filePath )
193+ if err != nil {
194+ log .Printf ("Error accessing file %s: %v" , filePath , err )
195+ return
196+ }
197+
198+ // Skip if it's a directory
199+ if fileInfo .IsDir () {
200+ return
201+ }
202+
203+ log .Printf ("Processing audio file: %s" , filename )
204+
205+ // Upload the file using the same logic as the API handler
206+ if err := s .uploadFile (filePath , filename ); err != nil {
207+ log .Printf ("Failed to upload file %s: %v" , filename , err )
208+ return
209+ }
210+
211+ // Delete the original file from dropzone after successful upload
212+ if err := os .Remove (filePath ); err != nil {
213+ log .Printf ("Warning: Failed to delete file from dropzone %s: %v" , filePath , err )
214+ } else {
215+ log .Printf ("Successfully processed and removed file: %s" , filename )
216+ }
217+ }
218+
219+ // uploadFile uploads the file using the existing pipeline logic
220+ func (s * Service ) uploadFile (sourcePath , originalFilename string ) error {
221+ // Create upload directory
222+ uploadDir := s .config .UploadDir
223+ if err := os .MkdirAll (uploadDir , 0755 ); err != nil {
224+ return fmt .Errorf ("failed to create upload directory: %v" , err )
225+ }
226+
227+ // Generate unique filename
228+ jobID := uuid .New ().String ()
229+ ext := filepath .Ext (originalFilename )
230+ filename := fmt .Sprintf ("%s%s" , jobID , ext )
231+ destPath := filepath .Join (uploadDir , filename )
232+
233+ // Copy file from dropzone to upload directory
234+ if err := s .copyFile (sourcePath , destPath ); err != nil {
235+ return fmt .Errorf ("failed to copy file: %v" , err )
236+ }
237+
238+ // Create job record with "uploaded" status
239+ job := models.TranscriptionJob {
240+ ID : jobID ,
241+ AudioPath : destPath ,
242+ Status : models .StatusUploaded ,
243+ Title : & originalFilename , // Use original filename as title
244+ }
245+
246+ // Save to database
247+ if err := database .DB .Create (& job ).Error ; err != nil {
248+ os .Remove (destPath ) // Clean up file on database error
249+ return fmt .Errorf ("failed to create job record: %v" , err )
250+ }
251+
252+ // Check if auto-transcription is enabled
253+ if s .isAutoTranscriptionEnabled () {
254+ log .Printf ("Auto-transcription enabled, enqueueing job %s" , jobID )
255+
256+ // Update job status to pending before enqueueing
257+ if err := database .DB .Model (& job ).Update ("status" , models .StatusPending ).Error ; err != nil {
258+ log .Printf ("Warning: Failed to update job status to pending: %v" , err )
259+ }
260+
261+ // Enqueue the job for transcription
262+ if err := s .taskQueue .EnqueueJob (jobID ); err != nil {
263+ log .Printf ("Failed to enqueue job %s for transcription: %v" , jobID , err )
264+ } else {
265+ log .Printf ("Job %s enqueued for auto-transcription" , jobID )
266+ }
267+ }
268+
269+ log .Printf ("Successfully uploaded file %s as job %s" , originalFilename , jobID )
270+ return nil
271+ }
272+
273+ // isAutoTranscriptionEnabled checks if auto-transcription is enabled for any user
274+ func (s * Service ) isAutoTranscriptionEnabled () bool {
275+ var count int64
276+
277+ // Check if there are any users with auto-transcription enabled
278+ err := database .DB .Model (& models.User {}).
279+ Where ("auto_transcription_enabled = ?" , true ).
280+ Count (& count ).Error
281+
282+ if err != nil {
283+ log .Printf ("Error checking auto-transcription settings: %v" , err )
284+ return false
285+ }
286+
287+ return count > 0
288+ }
289+
290+ // copyFile copies a file from source to destination
291+ func (s * Service ) copyFile (src , dst string ) error {
292+ sourceFile , err := os .Open (src )
293+ if err != nil {
294+ return err
295+ }
296+ defer sourceFile .Close ()
297+
298+ destFile , err := os .Create (dst )
299+ if err != nil {
300+ return err
301+ }
302+ defer destFile .Close ()
303+
304+ _ , err = io .Copy (destFile , sourceFile )
305+ if err != nil {
306+ return err
307+ }
308+
309+ return destFile .Sync ()
310+ }
0 commit comments