diff --git a/app.go b/app.go new file mode 100644 index 0000000..d3cbf91 --- /dev/null +++ b/app.go @@ -0,0 +1,430 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "time" + + . "ytd/constants" + . "ytd/db" + . "ytd/models" + . "ytd/plugins" + + "github.com/mitchellh/mapstructure" + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options/dialog" + "github.com/xujiajun/nutsdb" + + tb "gopkg.in/tucnak/telebot.v2" +) + +var wailsRuntime *wails.Runtime + +type AppState struct { + runtime *wails.Runtime + db *nutsdb.DB + plugins []Plugin + Entries []GenericEntry `json:"entries"` + Config *AppConfig `json:"config"` + Stats *AppStats +} + +func (state *AppState) WailsInit(runtime *wails.Runtime) { + // Save runtime + state.runtime = runtime + // Do some other initialisation + state.db = InitializeDb() + state.Entries = DbGetAllEntries() + state.Config = state.Config.Init() + state.Stats = &AppStats{} + appState = state + + // this is sync so it blocks until finished and wails:loaded are not dispatched until this finishes + if runtime.System.AppType() == "default" { // wails serve & ng serve + runtime.Events.On("wails:loaded", func(...interface{}) { + time.Sleep(100 * time.Millisecond) + fmt.Println("EMIT YTD:ONLOAD") + runtime.Events.Emit("ytd:onload", state) + }) + } else { // dekstop build + go func() { + runtime.Events.Emit("ytd:onload", state) + }() + } + + for _, plugin := range plugins { + plugin.SetWailsRuntime(runtime) + plugin.SetAppConfig(state.Config) + plugin.SetAppStats(state.Stats) + } + fmt.Println("APP STATE INITIALIZED") + + /* go func() { + for { + time.Sleep(10 * time.Second) + state.checkForTracksToDownload() + } + }() */ + + /* go func() { + for { + restart := make(chan int) + time.Sleep(3 * time.Second) + go state.telegramShareTracks(restart) + + for { + select { + case <-restart: + fmt.Println("Share tracks...") + go state.telegramShareTracks(restart) + } + + } + } + }() */ + + go func() { + restart := make(chan int) + time.Sleep(3 * time.Second) + go state.convertToMp3(restart) + + for { + select { + case <-restart: + fmt.Println("Restart converting...") + go state.convertToMp3(restart) + } + + } + }() +} + +func (s *WailsRuntime) WailsShutdown() { + CloseDb() +} + +func (state *AppState) GetAppConfig() *AppConfig { + return state.Config +} + +func (state *AppState) SelectDirectory() (string, error) { + selectedDirectory, err := state.runtime.Dialog.OpenDirectory(&dialog.OpenDialog{ + AllowFiles: false, + CanCreateDirectories: true, + AllowDirectories: true, + Title: "Choose directory", + }) + return selectedDirectory, err +} + +func (state *AppState) GetEntryById(entry GenericEntry) *GenericEntry { + for _, t := range state.Entries { + if t.Type == "track" && t.Track.ID == entry.Track.ID { + return &t + } + } + return nil +} + +func (state *AppState) checkForTracksToDownload() error { + fmt.Printf("Check for tracks to start downloads...%d/%d\n", state.Stats.DownloadingCount, state.Config.MaxParrallelDownloads) + if state.Stats.DownloadingCount >= state.Config.MaxParrallelDownloads { + return nil + } + + fmt.Printf("Checking...%d entries\n", len(state.Entries)) + // range over DbGetAllEntries() + for _, t := range DbGetAllEntries() { + var freeSlots uint = state.Config.MaxParrallelDownloads - state.Stats.DownloadingCount + entry := t + // auto download only tracks with processing status + // if track has pending/failed status it means that something goes wrong so user have to download it manually from UI + if entry.Type == "track" && entry.Track.Status == TrackStatusProcessing { + if freeSlots == 0 { + return nil + } + + // start download for track + fmt.Printf("Found %s to download\n", entry.Track.Name) + plugin := getPluginFor(entry.Source) + // make chan GenericEntry + // goriutine scrive li dentro + if plugin != nil { + freeSlots-- + go func(entry GenericEntry) { + // storedEntry := state.GetEntryById(entry) + // fmt.Printf("Stored entry %v\n", storedEntry) + // fmt.Println(entry.Track.Url) + // fmt.Println(storedEntry.Track.Url) + plugin.StartDownload(&entry) + // storedEntry.Track = entry.Track + }(entry) + } + } + } + + // qui un for che legge dal channel e ogni volta che riceve una entry downloadata + return nil +} + +type recipientString string + +func (r recipientString) Recipient() string { + return r.Recipient() +} + +func (state *AppState) telegramShareTracks(restart chan<- int) error { + if !state.Config.Telegram.Share { + // if option is not enabled restart check after 30s + time.Sleep(30 * time.Second) + restart <- 1 + return nil + } + + b, err := tb.NewBot(tb.Settings{ + Token: "1903196088:AAHhWGvfhQfS_MlhvohFvQYnrg3z7GsBPOM", + Poller: &tb.LongPoller{Timeout: 10 * time.Second}, + }) + + if err != nil { + fmt.Println(err) + return nil + } + + audio := &tb.Audio{File: tb.FromDisk("/Users/oskarmarciniak/songs/youtube/2vOU4nVI_DA.mp3"), FileName: "Track name", Title: "Track name title", Duration: 207, Caption: "Caption tracks"} + b.Send(&tb.Chat{ID: 903612486}, audio) + + b.Handle("/hello", func(m *tb.Message) { + fmt.Println(m.Chat) + b.Send(m.Sender, "Hello World!") + // cercare il primo msg che come username ha quello impostato nell'app e salvarsi la chat_id + }) + + b.Start() + + return nil +} + +func (state *AppState) convertToMp3(restart chan<- int) error { + if !state.Config.ConvertToMp3 { + // if option is not enabled restart check after 30s + time.Sleep(30 * time.Second) + restart <- 1 + return nil + } + + fmt.Println("Converting....") + ffmpeg, _ := state.IsFFmpegInstalled() + + for _, t := range DbGetAllEntries() { + entry := t + plugin := getPluginFor(entry.Source) + + if entry.Type == "track" && entry.Track.Status == TrackStatusDownladed && !entry.Track.IsConvertedToMp3 && plugin.IsTrackFileExists(entry.Track, "webm") { + fmt.Printf("Extracting audio for %s...\n", entry.Track.Name) + + // ffmpeg -i "41qC3w3UUkU.webm" -vn -ab 128k -ar 44100 -y "41qC3w3UUkU.mp3" + cmd := exec.Command( + ffmpeg, + "-loglevel", "quiet", + "-i", fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID), + "-vn", + "-ab", "128k", + "-ar", "44100", + "-y", fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID), + ) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + fmt.Println("Failed to extract audio:", err) + } else { + entry.Track.IsConvertedToMp3 = true + DbWriteEntry(entry.Track.ID, entry) + state.runtime.Events.Emit("ytd:track", entry) // track:converted:mp3 + + // remove webm + if state.Config.CleanWebmFiles && plugin.IsTrackFileExists(entry.Track, "webm") { + err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID)) + if err != nil && !os.IsNotExist(err) { + fmt.Printf("Cannot remove %s.webm file after successfull converting to mp3\n", entry.Track.ID) + } + } + } + restart <- 1 + return nil + } + } + + // if there are no tracks to convert delay between restart + time.Sleep(30 * time.Second) + restart <- 1 + return nil +} + +func (state *AppState) SaveSettingBoolValue(name string, val bool) (err error) { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovering from panic saveSettingValue:", r) + switch x := r.(type) { + case string: + err = errors.New(x) + case error: + err = x + default: + // Fallback err (per specs, error strings should be lowercase w/o punctuation + err = errors.New("unknown panic") + } + } + }() + + error := DbSaveSettingBoolValue(name, val) + if err != nil { + return error + } + + appState.Config.Set(name, val) + return nil +} + +func (state *AppState) SaveSettingValue(name string, val string) (err error) { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovering from panic saveSettingValue:", r) + switch x := r.(type) { + case string: + err = errors.New(x) + case error: + err = x + default: + // Fallback err (per specs, error strings should be lowercase w/o punctuation + err = errors.New("unknown panic") + } + } + }() + + error := DbWriteSetting(name, val) + if err != nil { + return error + } + + appState.Config.Set(name, val) + return nil +} + +func (state *AppState) ReadSettingBoolValue(name string) (bool, error) { + return DbReadSettingBoolValue(name) +} + +func (state *AppState) ReadSettingValue(name string) (string, error) { + return DbReadSetting(name) +} + +func (state *AppState) RemoveEntry(record map[string]interface{}) error { + var err error + var entry GenericEntry + err = mapstructure.Decode(record, &entry) + if err != nil { + return err + } + + if entry.Type == "track" { + if err = DbDeleteEntry(entry.Track.ID); err == nil { + plugin := getPluginFor(entry.Source) + if plugin.IsTrackFileExists(entry.Track, "webm") { + err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID)) + fmt.Println(err) + if err != nil && !os.IsNotExist(err) { + return err + } + } + // remove mp3 if file has been already converted + if plugin.IsTrackFileExists(entry.Track, "mp3") { + err = os.Remove(fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID)) + fmt.Println(err) + if err != nil && !os.IsNotExist(err) { + return err + } + } + } + } + return err +} + +func (state *AppState) AddToDownload(url string, isFromClipboard bool) error { + for _, plugin := range plugins { + if support := plugin.Supports(url); support { + if appState.GetAppConfig().ConcurrentDownloads { + go func() { + plugin.Fetch(url, isFromClipboard) + }() + } else { + plugin.Fetch(url, isFromClipboard) + } + continue + } + } + return nil +} + +func (state *AppState) StartDownload(record map[string]interface{}) error { + var err error + var entry GenericEntry + err = mapstructure.Decode(record, &entry) + if err != nil { + return err + } + + if appState.Stats.DownloadingCount >= appState.Config.MaxParrallelDownloads { + return errors.New("Max simultaneous downloads are reached please retry after some track finished downloading") + } + + for _, plugin := range plugins { + if plugin.GetName() == entry.Source && entry.Type == "track" { + if appState.GetAppConfig().ConcurrentDownloads { + go func() { + plugin.StartDownload(&entry) + }() + } else { + plugin.StartDownload(&entry) + } + continue + } + } + return nil +} + +func (state *AppState) IsSupportedUrl(url string) bool { + for _, plugin := range plugins { + if support := plugin.Supports(url); support { + return true + } + } + return false +} + +func (state *AppState) IsFFmpegInstalled() (string, error) { + ffmpeg, err := exec.LookPath("ffmpeg") + return ffmpeg, err +} + +func (state *AppState) OpenUrl(url string) error { + return state.runtime.Browser.Open(url) +} + +func getPluginFor(name string) Plugin { + for _, plugin := range plugins { + if plugin.GetName() == name { + return plugin + } + } + return nil +} + +//WailsRuntime . +type WailsRuntime struct { + runtime *wails.Runtime +} diff --git a/frontend/src/app/components/audio-player/audio-player.component.ts b/frontend/src/app/components/audio-player/audio-player.component.ts index b5606c5..44d5072 100644 --- a/frontend/src/app/components/audio-player/audio-player.component.ts +++ b/frontend/src/app/components/audio-player/audio-player.component.ts @@ -71,7 +71,7 @@ export class AudioPlayerComponent implements OnInit { } private _play(track: Track): void { - const src = track.isConvertedToMp3 ? `http://localhost:8080/youtube/${track.id}.mp3` : `http://localhost:8080/youtube/${track.id}.webm`; + const src = track.isConvertedToMp3 ? `http://localhost:8080/tracks/youtube/${track.id}.mp3` : `http://localhost:8080/tracks/youtube/${track.id}.webm`; this.audio = new Audio(src); this.audio.ontimeupdate = (e) => { diff --git a/frontend/src/app/components/settings/settings.component.html b/frontend/src/app/components/settings/settings.component.html index 9923620..dc7e3f5 100644 --- a/frontend/src/app/components/settings/settings.component.html +++ b/frontend/src/app/components/settings/settings.component.html @@ -56,14 +56,15 @@ Share downloaded tracks with your devices through telegram -
+
You have to enable "Convert to mp3" option before
Insert your telegram username - alternate_email - You have to set username into telegram's settings + @ + + You have to set username into telegram's settings Cannot be empty if enabled
diff --git a/frontend/src/app/components/settings/settings.component.scss b/frontend/src/app/components/settings/settings.component.scss index f78af2c..a52f34d 100644 --- a/frontend/src/app/components/settings/settings.component.scss +++ b/frontend/src/app/components/settings/settings.component.scss @@ -20,10 +20,26 @@ settings { background: transparent; } + .open-link { + font-weight: bold; + text-decoration: underline; + cursor: pointer; + } + + .at { + font-weight: bold; + padding-right: 8px; + } + .toggle-wrapper { + margin-left: 46px; + + .mat-slide-toggle { + margin-left: -46px; + } + .warning { font-weight: bold; - padding-left: 44px; } } } diff --git a/frontend/src/app/components/settings/settings.component.ts b/frontend/src/app/components/settings/settings.component.ts index 5c9658e..2383f3b 100644 --- a/frontend/src/app/components/settings/settings.component.ts +++ b/frontend/src/app/components/settings/settings.component.ts @@ -44,7 +44,7 @@ export class SettingsComponent implements OnInit { async ngOnInit(): Promise { this.model = { ...this.data.config }; - const [err, path] = await to(window.backend.isFFmpegInstalled()); + const [err, path] = await to(window.backend.main.AppState.IsFFmpegInstalled()); if(path) { this.isFfmpegAvailable = true; this._cdr.detectChanges(); @@ -52,10 +52,14 @@ export class SettingsComponent implements OnInit { } async changeBaseSaveDir(): Promise { - const [err, path] = await to(window.backend.AppState.SelectDirectory()); + const [err, path] = await to(window.backend.main.AppState.SelectDirectory()); console.log(err, path) } + async openUrl(url: string): Promise { + await window.backend.main.AppState.OpenUrl(url); + } + save(): void { this._dialogRef.close({ config: this.model }); } diff --git a/frontend/src/app/models/app-state.ts b/frontend/src/app/models/app-state.ts index cbf712c..d7db0c0 100644 --- a/frontend/src/app/models/app-state.ts +++ b/frontend/src/app/models/app-state.ts @@ -22,17 +22,20 @@ export interface AppState { export interface BackendCallbacks { - AppState: { - GetAppConfig: () => Promise; - SelectDirectory: () => Promise + main: { + AppState: { + GetAppConfig: () => Promise; + SelectDirectory: () => Promise + IsSupportedUrl: (url: string) => Promise; + AddToDownload: (url: string, isFromClipboard: boolean) => Promise; + StartDownload: (entry: Entry) => Promise; + ReadSettingBoolValue: (name: string) => Promise; + ReadSettingValue: (name: string) => Promise; + SaveSettingBoolValue: (name: string, val: boolean) => Promise; + SaveSettingValue: (name: string, val: string) => Promise; + RemoveEntry: (entry: Entry) => Promise; + IsFFmpegInstalled: () => Promise; + OpenUrl: (url :string) => Promise; + } } - isSupportedUrl: (url: string) => Promise; - addToDownload: (url: string, isFromClipboard: boolean) => Promise; - startDownload: (entry: Entry) => Promise; - readSettingBoolValue: (name: string) => Promise; - readSettingValue: (name: string) => Promise; - saveSettingBoolValue: (name: string, val: boolean) => Promise; - saveSettingValue: (name: string, val: string) => Promise; - removeEntry: (entry: Entry) => Promise; - isFFmpegInstalled: () => Promise; } diff --git a/frontend/src/app/pages/home.component.ts b/frontend/src/app/pages/home.component.ts index 808f4fd..577e9e4 100644 --- a/frontend/src/app/pages/home.component.ts +++ b/frontend/src/app/pages/home.component.ts @@ -132,7 +132,7 @@ export class HomeComponent implements OnInit { for (const [key, value] of Object.entries(config)) { switch(key) { case 'BaseSaveDir': - await window.backend.saveSettingValue(key, value as string); + await window.backend.main.AppState.SaveSettingValue(key, value as string); break; case 'ClipboardWatch': @@ -141,15 +141,15 @@ export class HomeComponent implements OnInit { case 'ConcurrentPlaylistDownloads': case 'ConvertToMp3': case 'CleanWebmFiles': - await window.backend.saveSettingBoolValue(key, value as boolean); + await window.backend.main.AppState.SaveSettingBoolValue(key, value as boolean); break; case 'MaxParrallelDownloads': - await window.backend.saveSettingValue(key, `${value}`); + await window.backend.main.AppState.SaveSettingValue(key, `${value}`); break; case 'Telegram': - await window.backend.saveSettingValue(key, JSON.stringify(value)); + await window.backend.main.AppState.SaveSettingValue(key, JSON.stringify(value)); break; } } @@ -159,6 +159,7 @@ export class HomeComponent implements OnInit { this._snackbar.open("Settings has been saved"); this._cdr.detectChanges(); } catch(e) { + console.log(e) this._snackbar.open("An error occured while saving settings"); } }); @@ -219,14 +220,14 @@ export class HomeComponent implements OnInit { return; } - const isSupported = await window.backend.isSupportedUrl(url); + const isSupported = await window.backend.main.AppState.IsSupportedUrl(url); if(!isSupported) { this._snackbar.open('Unsupported url'); return } try { - await window.backend.addToDownload(url, false) + await window.backend.main.AppState.AddToDownload(url, false) this.urlInput.setValue(''); this.pasteInput.nativeElement.blur(); this.pasteWrapper.nativeElement.classList.remove('focused'); @@ -239,7 +240,7 @@ export class HomeComponent implements OnInit { async startDownload(entry: Entry): Promise { try { - await window.backend.startDownload(entry); + await window.backend.main.AppState.StartDownload(entry); this._snackbar.open("Started downloading"); } catch(e) { this._snackbar.open(e); @@ -249,7 +250,7 @@ export class HomeComponent implements OnInit { async remove(entry: Entry, i: number): Promise { console.log('remove', entry, i) try { - await window.backend.removeEntry(entry); + await window.backend.main.AppState.RemoveEntry(entry); this._snackbar.open(`${entry.type} has been removed`); const idx = this.entries.findIndex(e => { if(entry.type === 'playlist') { diff --git a/frontend/src/backend/index.js b/frontend/src/backend/index.js index 64fc4e7..7d6666a 100755 --- a/frontend/src/backend/index.js +++ b/frontend/src/backend/index.js @@ -5,6 +5,15 @@ const backend = { "main": { "AppState": { + /** + * AddToDownload + * @param {string} arg1 - Go Type: string + * @param {boolean} arg2 - Go Type: bool + * @returns {Promise} - Go Type: error + */ + "AddToDownload": (arg1, arg2) => { + return window.backend.main.AppState.AddToDownload(arg1, arg2); + }, /** * GetAppConfig * @returns {Promise} - Go Type: *models.AppConfig @@ -20,6 +29,71 @@ const backend = { "GetEntryById": (arg1) => { return window.backend.main.AppState.GetEntryById(arg1); }, + /** + * IsFFmpegInstalled + * @returns {Promise} - Go Type: string + */ + "IsFFmpegInstalled": () => { + return window.backend.main.AppState.IsFFmpegInstalled(); + }, + /** + * IsSupportedUrl + * @param {string} arg1 - Go Type: string + * @returns {Promise} - Go Type: bool + */ + "IsSupportedUrl": (arg1) => { + return window.backend.main.AppState.IsSupportedUrl(arg1); + }, + /** + * OpenUrl + * @param {string} arg1 - Go Type: string + * @returns {Promise} - Go Type: error + */ + "OpenUrl": (arg1) => { + return window.backend.main.AppState.OpenUrl(arg1); + }, + /** + * ReadSettingBoolValue + * @param {string} arg1 - Go Type: string + * @returns {Promise} - Go Type: bool + */ + "ReadSettingBoolValue": (arg1) => { + return window.backend.main.AppState.ReadSettingBoolValue(arg1); + }, + /** + * ReadSettingValue + * @param {string} arg1 - Go Type: string + * @returns {Promise} - Go Type: string + */ + "ReadSettingValue": (arg1) => { + return window.backend.main.AppState.ReadSettingValue(arg1); + }, + /** + * RemoveEntry + * @param {any} arg1 - Go Type: map[string]interface {} + * @returns {Promise} - Go Type: error + */ + "RemoveEntry": (arg1) => { + return window.backend.main.AppState.RemoveEntry(arg1); + }, + /** + * SaveSettingBoolValue + * @param {string} arg1 - Go Type: string + * @param {boolean} arg2 - Go Type: bool + * @returns {Promise} - Go Type: error + */ + "SaveSettingBoolValue": (arg1, arg2) => { + return window.backend.main.AppState.SaveSettingBoolValue(arg1, arg2); + }, + /** + * SaveSettingValue + * @param {string} arg1 - Go Type: string + * @param {string} arg2 - Go Type: string + * @returns {Promise} - Go Type: error + */ + "SaveSettingValue": (arg1, arg2) => { + return window.backend.main.AppState.SaveSettingValue(arg1, arg2); + }, /** * SelectDirectory * @returns {Promise} - Go Type: string @@ -27,6 +101,14 @@ const backend = { "SelectDirectory": () => { return window.backend.main.AppState.SelectDirectory(); }, + /** + * StartDownload + * @param {any} arg1 - Go Type: map[string]interface {} + * @returns {Promise} - Go Type: error + */ + "StartDownload": (arg1) => { + return window.backend.main.AppState.StartDownload(arg1); + }, } } diff --git a/main.go b/main.go index 4b542a6..4f6dc62 100644 --- a/main.go +++ b/main.go @@ -3,58 +3,34 @@ package main import ( "context" "embed" - "errors" "fmt" "log" "net/http" "os" - "os/exec" "os/signal" "os/user" "sync" "time" . "ytd/clipboard" - . "ytd/constants" - . "ytd/db" . "ytd/models" . "ytd/plugins" _ "embed" - "github.com/mitchellh/mapstructure" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" - "github.com/wailsapp/wails/v2/pkg/options/dialog" "github.com/wailsapp/wails/v2/pkg/options/mac" - "github.com/xujiajun/nutsdb" - tb "gopkg.in/tucnak/telebot.v2" ) -var wailsRuntime *wails.Runtime var plugins []Plugin = []Plugin{&Yt{Name: "youtube"}} //go:embed frontend/dist/assets/* var static embed.FS -//go:embed frontend/dist/main.js -var js string - -//go:embed frontend/dist/styles.css -var css string - var appState *AppState var newEntries = make(chan GenericEntry) -type AppState struct { - runtime *wails.Runtime - db *nutsdb.DB - plugins []Plugin - Entries []GenericEntry `json:"entries"` - Config *AppConfig `json:"config"` - Stats *AppStats -} - func cors(fs http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // do your cors stuff @@ -64,412 +40,8 @@ func cors(fs http.Handler) http.HandlerFunc { } } -func (state *AppState) WailsInit(runtime *wails.Runtime) { - // Save runtime - state.runtime = runtime - // Do some other initialisation - state.db = InitializeDb() - state.Entries = DbGetAllEntries() - state.Config = state.Config.Init() - state.Stats = &AppStats{} - appState = state - - // this is sync so it blocks until finished and wails:loaded are not dispatched until this finishes - if runtime.System.AppType() == "default" { // wails serve & ng serve - runtime.Events.On("wails:loaded", func(...interface{}) { - time.Sleep(100 * time.Millisecond) - fmt.Println("EMIT YTD:ONLOAD") - runtime.Events.Emit("ytd:onload", state) - }) - } else { // dekstop build - go func() { - runtime.Events.Emit("ytd:onload", state) - }() - } - - for _, plugin := range plugins { - plugin.SetWailsRuntime(runtime) - plugin.SetAppConfig(state.Config) - plugin.SetAppStats(state.Stats) - } - fmt.Println("APP STATE INITIALIZED") - - /* go func() { - for { - time.Sleep(10 * time.Second) - state.checkForTracksToDownload() - } - }() */ - - /* go func() { - for { - restart := make(chan int) - time.Sleep(3 * time.Second) - go state.telegramShareTracks(restart) - - for { - select { - case <-restart: - fmt.Println("Share tracks...") - go state.telegramShareTracks(restart) - } - - } - } - }() */ - - go func() { - restart := make(chan int) - time.Sleep(3 * time.Second) - go state.convertToMp3(restart) - - for { - select { - case <-restart: - fmt.Println("Restart converting...") - go state.convertToMp3(restart) - } - - } - }() -} - -func (state *AppState) GetAppConfig() *AppConfig { - return state.Config -} - -func (state *AppState) SelectDirectory() (string, error) { - selectedDirectory, err := state.runtime.Dialog.OpenDirectory(&dialog.OpenDialog{ - AllowFiles: false, - CanCreateDirectories: true, - AllowDirectories: true, - Title: "Choose directory", - }) - return selectedDirectory, err -} - -func (state *AppState) GetEntryById(entry GenericEntry) *GenericEntry { - for _, t := range state.Entries { - if t.Type == "track" && t.Track.ID == entry.Track.ID { - return &t - } - } - return nil -} - -func (state *AppState) checkForTracksToDownload() error { - fmt.Printf("Check for tracks to start downloads...%d/%d\n", state.Stats.DownloadingCount, state.Config.MaxParrallelDownloads) - if state.Stats.DownloadingCount >= state.Config.MaxParrallelDownloads { - return nil - } - - fmt.Printf("Checking...%d entries\n", len(state.Entries)) - // range over DbGetAllEntries() - for _, t := range DbGetAllEntries() { - var freeSlots uint = state.Config.MaxParrallelDownloads - state.Stats.DownloadingCount - entry := t - // auto download only tracks with processing status - // if track has pending/failed status it means that something goes wrong so user have to download it manually from UI - if entry.Type == "track" && entry.Track.Status == TrackStatusProcessing { - if freeSlots == 0 { - return nil - } - - // start download for track - fmt.Printf("Found %s to download\n", entry.Track.Name) - plugin := getPluginFor(entry.Source) - // make chan GenericEntry - // goriutine scrive li dentro - if plugin != nil { - freeSlots-- - go func(entry GenericEntry) { - // storedEntry := state.GetEntryById(entry) - // fmt.Printf("Stored entry %v\n", storedEntry) - // fmt.Println(entry.Track.Url) - // fmt.Println(storedEntry.Track.Url) - plugin.StartDownload(&entry) - // storedEntry.Track = entry.Track - }(entry) - } - } - } - - // qui un for che legge dal channel e ogni volta che riceve una entry downloadata - return nil -} - -type recipientString string - -func (r recipientString) Recipient() string { - return r.Recipient() -} - -func (state *AppState) telegramShareTracks(restart chan<- int) error { - if !state.Config.Telegram.Share { - // if option is not enabled restart check after 30s - time.Sleep(30 * time.Second) - restart <- 1 - return nil - } - - b, err := tb.NewBot(tb.Settings{ - Token: "1903196088:AAHhWGvfhQfS_MlhvohFvQYnrg3z7GsBPOM", - Poller: &tb.LongPoller{Timeout: 10 * time.Second}, - }) - - if err != nil { - fmt.Println(err) - return nil - } - - audio := &tb.Audio{File: tb.FromDisk("/Users/oskarmarciniak/songs/youtube/2vOU4nVI_DA.mp3"), FileName: "Track name", Title: "Track name title", Duration: 207, Caption: "Caption tracks"} - b.Send(&tb.Chat{ID: 903612486}, audio) - - b.Handle("/hello", func(m *tb.Message) { - fmt.Println(m.Chat) - b.Send(m.Sender, "Hello World!") - // cercare il primo msg che come username ha quello impostato nell'app e salvarsi la chat_id - }) - - b.Start() - - return nil -} - -func (state *AppState) convertToMp3(restart chan<- int) error { - if !state.Config.ConvertToMp3 { - // if option is not enabled restart check after 30s - time.Sleep(30 * time.Second) - restart <- 1 - return nil - } - - fmt.Println("Converting....") - ffmpeg, _ := isFFmpegInstalled() - - for _, t := range DbGetAllEntries() { - entry := t - plugin := getPluginFor(entry.Source) - - if entry.Type == "track" && entry.Track.Status == TrackStatusDownladed && !entry.Track.IsConvertedToMp3 && plugin.IsTrackFileExists(entry.Track, "webm") { - fmt.Printf("Extracting audio for %s...\n", entry.Track.Name) - - // ffmpeg -i "41qC3w3UUkU.webm" -vn -ab 128k -ar 44100 -y "41qC3w3UUkU.mp3" - cmd := exec.Command( - ffmpeg, - "-loglevel", "quiet", - "-i", fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID), - "-vn", - "-ab", "128k", - "-ar", "44100", - "-y", fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID), - ) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - fmt.Println("Failed to extract audio:", err) - } else { - entry.Track.IsConvertedToMp3 = true - DbWriteEntry(entry.Track.ID, entry) - state.runtime.Events.Emit("ytd:track", entry) // track:converted:mp3 - - // remove webm - if state.Config.CleanWebmFiles && plugin.IsTrackFileExists(entry.Track, "webm") { - err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID)) - if err != nil && !os.IsNotExist(err) { - fmt.Printf("Cannot remove %s.webm file after successfull converting to mp3\n", entry.Track.ID) - } - } - } - restart <- 1 - return nil - } - } - - // if there are no tracks to convert delay between restart - time.Sleep(30 * time.Second) - restart <- 1 - return nil -} - -func saveSettingBoolValue(name string, val bool) (err error) { - defer func() { - if r := recover(); r != nil { - fmt.Println("Recovering from panic saveSettingValue:", r) - switch x := r.(type) { - case string: - err = errors.New(x) - case error: - err = x - default: - // Fallback err (per specs, error strings should be lowercase w/o punctuation - err = errors.New("unknown panic") - } - } - }() - - error := DbSaveSettingBoolValue(name, val) - if err != nil { - return error - } - - appState.Config.Set(name, val) - return nil -} - -func saveSettingValue(name string, val string) (err error) { - defer func() { - if r := recover(); r != nil { - fmt.Println("Recovering from panic saveSettingValue:", r) - switch x := r.(type) { - case string: - err = errors.New(x) - case error: - err = x - default: - // Fallback err (per specs, error strings should be lowercase w/o punctuation - err = errors.New("unknown panic") - } - } - }() - - error := DbWriteSetting(name, val) - if err != nil { - return error - } - - appState.Config.Set(name, val) - return nil -} - -func readSettingBoolValue(name string) (bool, error) { - return DbReadSettingBoolValue(name) -} - -func readSettingValue(name string) (string, error) { - return DbReadSetting(name) -} - -func removeEntry(record map[string]interface{}) error { - var err error - var entry GenericEntry - err = mapstructure.Decode(record, &entry) - if err != nil { - return err - } - - if entry.Type == "track" { - if err = DbDeleteEntry(entry.Track.ID); err == nil { - plugin := getPluginFor(entry.Source) - if plugin.IsTrackFileExists(entry.Track, "webm") { - err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID)) - fmt.Println(err) - if err != nil && !os.IsNotExist(err) { - return err - } - } - // remove mp3 if file has been already converted - if plugin.IsTrackFileExists(entry.Track, "mp3") { - err = os.Remove(fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID)) - fmt.Println(err) - if err != nil && !os.IsNotExist(err) { - return err - } - } - } - } - return err -} - -func addToDownload(url string, isFromClipboard bool) error { - for _, plugin := range plugins { - if support := plugin.Supports(url); support { - if appState.GetAppConfig().ConcurrentDownloads { - go func() { - plugin.Fetch(url, isFromClipboard) - }() - } else { - plugin.Fetch(url, isFromClipboard) - } - continue - } - } - return nil -} - -func startDownload(record map[string]interface{}) error { - var err error - var entry GenericEntry - err = mapstructure.Decode(record, &entry) - if err != nil { - return err - } - - if appState.Stats.DownloadingCount >= appState.Config.MaxParrallelDownloads { - return errors.New("Max simultaneous downloads are reached please retry after some track finished downloading") - } - - for _, plugin := range plugins { - if plugin.GetName() == entry.Source && entry.Type == "track" { - if appState.GetAppConfig().ConcurrentDownloads { - go func() { - plugin.StartDownload(&entry) - }() - } else { - plugin.StartDownload(&entry) - } - continue - } - } - return nil -} - -func isSupportedUrl(url string) bool { - for _, plugin := range plugins { - if support := plugin.Supports(url); support { - return true - } - } - return false -} - -func isFFmpegInstalled() (string, error) { - ffmpeg, err := exec.LookPath("ffmpeg") - return ffmpeg, err -} - -func getPluginFor(name string) Plugin { - for _, plugin := range plugins { - if plugin.GetName() == name { - return plugin - } - } - return nil -} - -//WailsRuntime . -type WailsRuntime struct { - runtime *wails.Runtime -} - -func (s *WailsRuntime) WailsShutdown() { - CloseDb() -} - func main() { - /* app.Bind(&AppState{}) - app.Bind(saveSettingBoolValue) - app.Bind(saveSettingValue) - app.Bind(readSettingBoolValue) - app.Bind(readSettingValue) - app.Bind(removeEntry) - app.Bind(addToDownload) - app.Bind(startDownload) - app.Bind(isSupportedUrl) - app.Bind(isFFmpegInstalled) */ - + app := &AppState{} wg := &sync.WaitGroup{} wg.Add(2) ctx, cancelCtx := context.WithCancel(context.Background()) @@ -513,7 +85,7 @@ func main() { if ok && change != "" { log.Printf("change received: '%s'", change) if appState.Config.ClipboardWatch { - addToDownload(change, true) + app.AddToDownload(change, true) } } else { log.Println("channel has been closed. exiting...") @@ -525,12 +97,11 @@ func main() { go func() { fs := http.StripPrefix("/static/", http.FileServer(http.FS(static))) - http.Handle("/tracks/", http.FileServer(http.Dir(currentDir))) + http.Handle("/tracks/", http.StripPrefix("/tracks/", http.FileServer(http.Dir(currentDir)))) http.Handle("/static/", cors(fs)) http.ListenAndServe(":8080", nil) }() - app := &AppState{} err := wails.Run(&options.App{ Width: 1024, Height: 768,