380 lines
9.9 KiB
Go
380 lines
9.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/iawia002/lux/downloader"
|
|
"github.com/iawia002/lux/extractors"
|
|
qt "github.com/mappu/miqt/qt6"
|
|
"github.com/mappu/miqt/qt6/mainthread"
|
|
)
|
|
|
|
// GUIConfig carries all application configurations.
|
|
type GUIConfig struct {
|
|
DestinationFolder string
|
|
PlaylistEnabled bool
|
|
}
|
|
|
|
// GetDestinationFolder attempts to retrieve (if not yet) and returns the
|
|
// configured destination folder.
|
|
func (c *GUIConfig) GetDestinationFolder() string {
|
|
fallback := DefaultDownloadsFolder()
|
|
val := settings.Value(*qt.NewQAnyStringView3("destinationFolder"), qt.NewQVariant11(fallback)).ToString()
|
|
c.DestinationFolder = val
|
|
return val
|
|
}
|
|
|
|
// GetPlaylistEnabled attempts to retrieve (if not yet) and returns the
|
|
// configured value of whether playlist download is enabled.
|
|
func (c *GUIConfig) GetPlaylistEnabled() bool {
|
|
fallback := true
|
|
val := settings.Value(*qt.NewQAnyStringView3("playlistEnabled"), qt.NewQVariant8(fallback)).ToBool()
|
|
c.PlaylistEnabled = val
|
|
return val
|
|
}
|
|
|
|
// SetDestinationFolder sets and persists the configured destination folder.
|
|
func (c *GUIConfig) SetDestinationFolder(val string) {
|
|
c.DestinationFolder = val
|
|
go func() { settings.SetValue(*qt.NewQAnyStringView3("destinationFolder"), qt.NewQVariant11(val)) }()
|
|
}
|
|
|
|
// SetPlaylistEnabled sets and persists the configured value of whether playlist
|
|
// download is enabled.
|
|
func (c *GUIConfig) SetPlaylistEnabled(val bool) {
|
|
c.PlaylistEnabled = val
|
|
go func() { settings.SetValue(*qt.NewQAnyStringView3("playlistEnabled"), qt.NewQVariant8(val)) }()
|
|
}
|
|
|
|
// LoadGUIConfig loads config values from persisted settings if possible, or
|
|
// loads defaults otherwise.
|
|
func LoadGUIConfig() *GUIConfig {
|
|
c := &GUIConfig{}
|
|
c.GetDestinationFolder()
|
|
c.GetPlaylistEnabled()
|
|
return c
|
|
}
|
|
|
|
var settings *qt.QSettings
|
|
var guiConfig *GUIConfig
|
|
|
|
//
|
|
|
|
type outputBuffer struct {
|
|
textEdit *qt.QPlainTextEdit
|
|
reader *bufio.Reader
|
|
scanner *bufio.Scanner
|
|
carriageReturnInAction bool
|
|
}
|
|
|
|
func newOutputBuffer(textEdit *qt.QPlainTextEdit) *outputBuffer {
|
|
return &outputBuffer{
|
|
textEdit: textEdit,
|
|
reader: nil,
|
|
scanner: nil,
|
|
carriageReturnInAction: false,
|
|
}
|
|
}
|
|
|
|
func (b *outputBuffer) attachReader(r io.Reader) {
|
|
b.reader = bufio.NewReaderSize(r, 64*1024)
|
|
b.scanner = bufio.NewScanner(b.reader)
|
|
re := regexp.MustCompile(`^[^\r\n]*(\r\n|\r|\n)`)
|
|
b.scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
token = re.Find(data)
|
|
if token != nil {
|
|
return len(token), token, nil
|
|
}
|
|
if !atEOF {
|
|
return 0, nil, nil
|
|
}
|
|
return 0, data, bufio.ErrFinalToken
|
|
})
|
|
}
|
|
|
|
func (b *outputBuffer) addLine(line string) {
|
|
mainthread.Start(func() {
|
|
b.addLine_nothread(line)
|
|
})
|
|
}
|
|
|
|
// addLine adds a line that might end in LF, CRLF, CR, or none of the above (in
|
|
// which case an LF is appended). Aware of CR and scroll position.
|
|
func (b *outputBuffer) addLine_nothread(line string) {
|
|
scrollBar := b.textEdit.VerticalScrollBar()
|
|
currentScroll := scrollBar.Value()
|
|
userScrolledBack := currentScroll != scrollBar.Maximum()
|
|
|
|
cursor := b.textEdit.TextCursor()
|
|
cursor.MovePosition3(qt.QTextCursor__End, qt.QTextCursor__MoveAnchor, 1)
|
|
if b.carriageReturnInAction {
|
|
// Remove last line.
|
|
cursor.Select(qt.QTextCursor__LineUnderCursor)
|
|
cursor.RemoveSelectedText()
|
|
}
|
|
b.carriageReturnInAction = false
|
|
if len(line) > 0 {
|
|
switch lastCh := line[len(line)-1]; lastCh {
|
|
case '\n':
|
|
cursor.InsertText(line)
|
|
case '\r':
|
|
cursor.InsertText(line[:len(line)-1])
|
|
b.carriageReturnInAction = true
|
|
default:
|
|
cursor.InsertText(line + "\n")
|
|
}
|
|
} else {
|
|
cursor.InsertText("\n")
|
|
}
|
|
|
|
if userScrolledBack {
|
|
scrollBar.SetValue(currentScroll)
|
|
} else {
|
|
scrollBar.SetValue(scrollBar.Maximum())
|
|
}
|
|
}
|
|
|
|
func (b *outputBuffer) readLineAndUpdate() (fullLine string, err error) {
|
|
if !b.scanner.Scan() {
|
|
err = b.scanner.Err()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = io.EOF
|
|
}
|
|
fullLine = b.scanner.Text()
|
|
if len(fullLine) > 0 {
|
|
b.addLine(fullLine)
|
|
}
|
|
return
|
|
}
|
|
|
|
func initAboutWindow(parent *qt.QWidget) *qt.QDialog {
|
|
w := qt.NewQDialog(parent)
|
|
label := qt.NewQLabel3(about)
|
|
label.SetOpenExternalLinks(true)
|
|
layout := qt.NewQVBoxLayout2()
|
|
layout.AddWidget(label.QWidget)
|
|
w.SetLayout(layout.QLayout)
|
|
return w
|
|
}
|
|
|
|
type AnnieGuiApp struct {
|
|
*qt.QMainWindow
|
|
|
|
urlLineEdit *qt.QLineEdit
|
|
output *outputBuffer
|
|
}
|
|
|
|
func NewAnnieGuiApp() *AnnieGuiApp {
|
|
|
|
window := qt.NewQMainWindow2()
|
|
window.SetWindowTitle(appName)
|
|
|
|
app := AnnieGuiApp{
|
|
QMainWindow: window,
|
|
}
|
|
|
|
const WindowStateVersion = 0
|
|
|
|
window.OnCloseEvent(func(super func(event *qt.QCloseEvent), event *qt.QCloseEvent) {
|
|
settings.SetValue(*qt.NewQAnyStringView3("_geometry"), qt.NewQVariant12(window.SaveGeometry()))
|
|
settings.SetValue(*qt.NewQAnyStringView3("_windowState"), qt.NewQVariant12(window.SaveState1(WindowStateVersion)))
|
|
|
|
super(event)
|
|
})
|
|
|
|
{
|
|
prevGeometry := settings.Value(*qt.NewQAnyStringView3("_geometry"), qt.NewQVariant()).ToByteArray()
|
|
if len(prevGeometry) > 0 {
|
|
window.RestoreGeometry(prevGeometry)
|
|
}
|
|
|
|
prevWindowState := settings.Value(*qt.NewQAnyStringView3("_windowState"), qt.NewQVariant()).ToByteArray()
|
|
if len(prevWindowState) > 0 {
|
|
window.RestoreState2(prevWindowState, WindowStateVersion)
|
|
}
|
|
}
|
|
|
|
centralWidget := qt.NewQWidget(window.QWidget)
|
|
window.SetCentralWidget(centralWidget)
|
|
|
|
menuBar := window.MenuBar()
|
|
applicationMenu := menuBar.AddMenuWithTitle(tr("Application"))
|
|
aboutWindow := initAboutWindow(window.QWidget)
|
|
aboutAction := applicationMenu.AddActionWithText(tr("About"))
|
|
aboutAction.SetMenuRole(qt.QAction__AboutRole)
|
|
aboutAction.OnTriggered(func() {
|
|
aboutWindow.Show()
|
|
aboutWindow.Raise()
|
|
})
|
|
|
|
app.urlLineEdit = qt.NewQLineEdit(nil)
|
|
|
|
folderLineEdit := qt.NewQLineEdit3(guiConfig.DestinationFolder)
|
|
folderLineEdit.SetReadOnly(true)
|
|
folderLineEdit.SetMinimumWidth(250)
|
|
folderButton := qt.NewQPushButton3(tr("Pick another folder"))
|
|
folderDialog := qt.NewQFileDialog6(window.QWidget, tr("Destination folder"), guiConfig.DestinationFolder, "")
|
|
folderDialog.SetFileMode(qt.QFileDialog__Directory)
|
|
folderButton.OnClicked(func() {
|
|
if folderDialog.Exec() != int(qt.QDialog__Accepted) { // FIXME blocking call
|
|
return
|
|
}
|
|
destinationFolder := qt.QDir_ToNativeSeparators(folderDialog.SelectedFiles()[0])
|
|
folderLineEdit.SetText(destinationFolder)
|
|
guiConfig.SetDestinationFolder(destinationFolder)
|
|
})
|
|
folderHBoxLayout := qt.NewQHBoxLayout2()
|
|
folderHBoxLayout.AddWidget3(folderLineEdit.QWidget, 1, 0)
|
|
folderHBoxLayout.AddWidget3(folderButton.QWidget, 0, 0)
|
|
|
|
playlistCheckBox := qt.NewQCheckBox(nil)
|
|
playlistCheckBox.SetChecked(guiConfig.PlaylistEnabled)
|
|
playlistCheckBox.OnToggled(func(checked bool) {
|
|
guiConfig.SetPlaylistEnabled(checked)
|
|
})
|
|
|
|
inputFormLayout := qt.NewQFormLayout(nil)
|
|
inputFormLayout.SetFieldGrowthPolicy(qt.QFormLayout__AllNonFixedFieldsGrow)
|
|
inputFormLayout.AddRow3(tr("Video URL"), app.urlLineEdit.QWidget)
|
|
inputFormLayout.AddRow4(tr("Destination folder"), folderHBoxLayout.QLayout)
|
|
inputFormLayout.AddRow3(tr("Download playlists"), playlistCheckBox.QWidget)
|
|
|
|
outputTextEdit := qt.NewQPlainTextEdit2()
|
|
outputTextEdit.SetReadOnly(true)
|
|
outputTextEdit.SetMinimumHeight(400)
|
|
outputTextEdit.SetMinimumWidth(1000)
|
|
outputTextEdit.SetLineWrapMode(qt.QPlainTextEdit__NoWrap)
|
|
monospaceFont := qt.NewQFont2("Courier")
|
|
monospaceFont.SetStyleHint(qt.QFont__Monospace)
|
|
outputTextEdit.SetFont(monospaceFont)
|
|
|
|
app.output = newOutputBuffer(outputTextEdit)
|
|
app.output.addLine(tr("Awaiting user input"))
|
|
|
|
downloadButton := qt.NewQPushButton3(tr("Download"))
|
|
downloadButton.OnClicked(app.StartDownload)
|
|
|
|
layout := qt.NewQVBoxLayout2()
|
|
layout.AddLayout2(inputFormLayout.QLayout, 0)
|
|
layout.AddWidget3(downloadButton.QWidget, 0, 0)
|
|
layout.AddWidget3(outputTextEdit.QWidget, 1, 0)
|
|
centralWidget.SetLayout(layout.QLayout)
|
|
|
|
//
|
|
|
|
return &app
|
|
}
|
|
|
|
func (app *AnnieGuiApp) StartDownload() {
|
|
url := strings.TrimSpace(app.urlLineEdit.Text())
|
|
if len(url) == 0 {
|
|
return
|
|
}
|
|
|
|
app.output.addLine(time.Now().Format("15:04:05 ") + tr("Download started"))
|
|
|
|
savedStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
app.output.attachReader(r)
|
|
os.Stdout = w
|
|
|
|
go func() {
|
|
for {
|
|
_, err := app.output.readLineAndUpdate()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
log.Fatal(err)
|
|
}
|
|
// fmt.Fprint(savedStdout, line)
|
|
}
|
|
app.output.addLine("")
|
|
}()
|
|
|
|
printError := func(url string, err error) {
|
|
app.output.addLine(fmt.Sprintf(
|
|
"Downloading %s error:\n%s\n", url, err,
|
|
))
|
|
}
|
|
|
|
go func() {
|
|
|
|
data, err := extractors.Extract(url, extractors.Options{
|
|
Playlist: guiConfig.PlaylistEnabled,
|
|
})
|
|
if err != nil {
|
|
// if this error occurs, it means that an error occurred before actually starting to extract data
|
|
// (there is an error in the preparation step), and the data list is empty.
|
|
printError(url, err)
|
|
return
|
|
}
|
|
|
|
var isErr bool
|
|
for _, item := range data {
|
|
if item.Err != nil {
|
|
// if this error occurs, the preparation step is normal, but the data extraction is wrong.
|
|
// the data is an empty struct.
|
|
printError(item.URL, item.Err)
|
|
isErr = true
|
|
continue
|
|
}
|
|
|
|
err = downloader.New(downloader.Options{
|
|
OutputPath: guiConfig.DestinationFolder,
|
|
}).Download(item)
|
|
if err != nil {
|
|
printError(item.URL, err)
|
|
isErr = true
|
|
}
|
|
}
|
|
|
|
if isErr {
|
|
app.output.addLine(tr("On network errors, e.g. HTTP 403, please retry a few times."))
|
|
}
|
|
w.Close()
|
|
os.Stdout = savedStdout
|
|
}()
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
initLanguages()
|
|
|
|
qt.QCoreApplication_SetOrganizationName(appAuthor)
|
|
qt.QCoreApplication_SetOrganizationDomain(appAuthorDomain)
|
|
|
|
settings = qt.NewQSettings5()
|
|
guiConfig = LoadGUIConfig()
|
|
|
|
qt.NewQApplication(os.Args)
|
|
|
|
app := NewAnnieGuiApp()
|
|
|
|
app.Show()
|
|
|
|
sigs := make(chan os.Signal)
|
|
signal.Notify(sigs, syscall.SIGSEGV, syscall.SIGABRT)
|
|
go func() {
|
|
for {
|
|
sig := <-sigs
|
|
log.Print(sig)
|
|
}
|
|
}()
|
|
|
|
qt.QApplication_Exec()
|
|
}
|