annie-miqt/main.go

518 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package main
import (
"bufio"
"flag"
"fmt"
"io"
"net/url"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"time"
"golang.org/x/text/language"
"github.com/cloudfoundry-attic/jibber_jabber"
"github.com/golang/glog"
qt "github.com/mappu/miqt/qt6"
"github.com/iawia002/annie/config"
"github.com/iawia002/annie/downloader"
"github.com/iawia002/annie/extractors/bcy"
"github.com/iawia002/annie/extractors/bilibili"
"github.com/iawia002/annie/extractors/douyin"
"github.com/iawia002/annie/extractors/douyu"
"github.com/iawia002/annie/extractors/facebook"
"github.com/iawia002/annie/extractors/geekbang"
"github.com/iawia002/annie/extractors/instagram"
"github.com/iawia002/annie/extractors/iqiyi"
"github.com/iawia002/annie/extractors/mgtv"
"github.com/iawia002/annie/extractors/miaopai"
"github.com/iawia002/annie/extractors/netease"
"github.com/iawia002/annie/extractors/pixivision"
"github.com/iawia002/annie/extractors/pornhub"
"github.com/iawia002/annie/extractors/qq"
"github.com/iawia002/annie/extractors/tangdou"
"github.com/iawia002/annie/extractors/tiktok"
"github.com/iawia002/annie/extractors/tumblr"
"github.com/iawia002/annie/extractors/twitter"
"github.com/iawia002/annie/extractors/udn"
"github.com/iawia002/annie/extractors/universal"
"github.com/iawia002/annie/extractors/vimeo"
"github.com/iawia002/annie/extractors/weibo"
"github.com/iawia002/annie/extractors/xvideos"
"github.com/iawia002/annie/extractors/yinyuetai"
"github.com/iawia002/annie/extractors/youku"
"github.com/iawia002/annie/extractors/youtube"
"github.com/iawia002/annie/utils"
)
const appName = "annie-mingui"
const appAuthor = "Zhiming Wang"
const appAuthorDomain = "zhimingwang.org"
const about = `<p><strong>annie-mingui</strong> v2020.02.09</p>
<p>Copyright (c) 2020 Zhiming Wang</p>
<p>annie-mingui is a Qt wrapper for <a href="https://github.com/iawia002/annie">iawia002/annie</a> the video downloader. Credits:</p>
<ul>
<li>iawia002/annie, MIT license;</li>
<li><a href="https://qt.io">Qt</a>, LGPLv3 license;</li>
<li><a href="https://github.com/therecipe/qt">therecipe/qt</a>, Qt bindings for golang, LGPLv3 license.</li>
</ul>
<p>Project URL: <a href="https://github.com/fanaticscripter/annie-mingui">github.com/fanaticscripter/annie-mingui</a>.</p>
`
// 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("destinationFolder", qt.NewQVariant12(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("playlistEnabled", qt.NewQVariant9(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("destinationFolder", qt.NewQVariant12(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("playlistEnabled", qt.NewQVariant9(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
var locales = map[string]map[string]string{
"zh-Hans": {
"About": "关于",
"Application": "应用",
"Awaiting user input": "等待用户输入",
"Destination folder": "目标文件夹",
"Download": "下载",
"Download playlists": "下载完整播单",
"Download started": "下载已开始",
"On network errors, e.g. HTTP 403, please retry a few times.": "如遇HTTP 403等网络错误请重试几次。",
"Pick another folder": "选择文件夹",
"Video URL": "视频链接",
},
}
var locale map[string]string
// Poor man's gettext.
func tr(s string) string {
t, ok := locale[s]
if ok {
return t
}
return s
}
// ----- START ANNIE CODE -----
//
// The following code is slightly modified from
//
// https://github.com/iawia002/annie/blob/master/main.go
func printError(url string, err error) {
fmt.Printf(
"Downloading %s error:\n%s\n", url, err,
)
}
func download(videoURL string) bool {
var (
domain string
err error
data []downloader.Data
)
bilibiliShortLink := utils.MatchOneOf(videoURL, `^(av|ep)\d+`)
if bilibiliShortLink != nil && len(bilibiliShortLink) > 1 {
bilibiliURL := map[string]string{
"av": "https://www.bilibili.com/video/",
"ep": "https://www.bilibili.com/bangumi/play/",
}
domain = "bilibili"
videoURL = bilibiliURL[bilibiliShortLink[1]] + videoURL
} else {
u, err := url.ParseRequestURI(videoURL)
if err != nil {
printError(videoURL, err)
return false
}
domain = utils.Domain(u.Host)
}
switch domain {
case "douyin", "iesdouyin":
data, err = douyin.Extract(videoURL)
case "bilibili":
data, err = bilibili.Extract(videoURL)
case "bcy":
data, err = bcy.Extract(videoURL)
case "pixivision":
data, err = pixivision.Extract(videoURL)
case "youku":
data, err = youku.Extract(videoURL)
case "youtube", "youtu": // youtu.be
data, err = youtube.Extract(videoURL)
case "iqiyi":
data, err = iqiyi.Extract(videoURL)
case "mgtv":
data, err = mgtv.Extract(videoURL)
case "tangdou":
data, err = tangdou.Extract(videoURL)
case "tumblr":
data, err = tumblr.Extract(videoURL)
case "vimeo":
data, err = vimeo.Extract(videoURL)
case "facebook":
data, err = facebook.Extract(videoURL)
case "douyu":
data, err = douyu.Extract(videoURL)
case "miaopai":
data, err = miaopai.Extract(videoURL)
case "163":
data, err = netease.Extract(videoURL)
case "weibo":
data, err = weibo.Extract(videoURL)
case "instagram":
data, err = instagram.Extract(videoURL)
case "twitter":
data, err = twitter.Extract(videoURL)
case "qq":
data, err = qq.Extract(videoURL)
case "yinyuetai":
data, err = yinyuetai.Extract(videoURL)
case "geekbang":
data, err = geekbang.Extract(videoURL)
case "pornhub":
data, err = pornhub.Extract(videoURL)
case "xvideos":
data, err = xvideos.Extract(videoURL)
case "udn":
data, err = udn.Extract(videoURL)
case "tiktok":
data, err = tiktok.Extract(videoURL)
default:
data, err = universal.Extract(videoURL)
}
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(videoURL, err)
return false
}
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.Download(item, videoURL, config.ChunkSizeMB)
if err != nil {
printError(item.URL, err)
isErr = true
}
}
return !isErr
}
// ----- END ANNIE CODE -----
// PlainTextEdit extends QPlainTextEdit.
type PlainTextEdit struct {
qt.QPlainTextEdit
_ func() `constructor:"init"`
_ func(string) `slot:"addLine"`
carriageReturnInAction bool
}
func (p *PlainTextEdit) init() {
p.carriageReturnInAction = false
p.ConnectAddLine(p.addLine)
}
// addLines 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 (p *PlainTextEdit) addLine(line string) {
scrollBar := p.VerticalScrollBar()
currentScroll := scrollBar.Value()
userScrolledBack := currentScroll != scrollBar.Maximum()
cursor := p.TextCursor()
cursor.MovePosition(qt.QTextCursor__End, qt.QTextCursor__MoveAnchor, 1)
if p.carriageReturnInAction {
// Remove last line.
cursor.Select(qt.QTextCursor__LineUnderCursor)
cursor.RemoveSelectedText()
}
p.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])
p.carriageReturnInAction = true
default:
cursor.InsertText(line + "\n")
}
} else {
cursor.InsertText("\n")
}
if userScrolledBack {
scrollBar.SetValue(currentScroll)
} else {
scrollBar.SetValue(scrollBar.Maximum())
}
}
type outputBuffer struct {
textEdit *PlainTextEdit
reader *bufio.Reader
scanner *bufio.Scanner
}
func newOutputBuffer(textEdit *PlainTextEdit) *outputBuffer {
return &outputBuffer{
textEdit: textEdit,
reader: nil,
scanner: nil,
}
}
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) {
b.textEdit.AddLine(line)
}
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 init() {
flag.Parse()
userLang, err := jibber_jabber.DetectIETF()
if err != nil {
userLang = "en"
}
availableLangs := []string{"en"}
availableTags := []language.Tag{language.English}
for l := range locales {
availableLangs = append(availableLangs, l)
availableTags = append(availableTags, language.Make(l))
}
matcher := language.NewMatcher(availableTags)
_, index, _ := matcher.Match(language.Make(userLang))
locale = locales[availableLangs[index]]
qt.QCoreApplication_SetOrganizationName(appAuthor)
qt.QCoreApplication_SetOrganizationDomain(appAuthorDomain)
settings = qt.NewQSettings5(nil)
guiConfig = LoadGUIConfig()
}
func initAboutWindow() *qt.QDialog {
w := qt.NewQDialog(nil, 0)
label := qt.NewQLabel2(about, nil, 0)
label.SetOpenExternalLinks(true)
layout := qt.NewQVBoxLayout()
layout.AddWidget(label, 0, 0)
w.SetLayout(layout)
return w
}
func main() {
qt.NewQApplication(len(os.Args), os.Args)
window := qt.NewQMainWindow(nil, 0)
window.SetWindowTitle(appName)
window.ConnectCloseEvent(func(*qt.QCloseEvent) {
settings.SetValue("_geometry", qt.NewQVariant13(window.SaveGeometry()))
settings.SetValue("_windowState", qt.NewQVariant13(window.SaveState(0)))
})
window.RestoreGeometry(settings.Value("_geometry", qt.NewQVariant()).ToByteArray())
window.RestoreState(settings.Value("_windowState", qt.NewQVariant()).ToByteArray(), 0)
centralWidget := qt.NewQWidget(window, 0)
window.SetCentralWidget(centralWidget)
menuBar := window.MenuBar()
applicationMenu := menuBar.AddMenu2(tr("Application"))
aboutWindow := initAboutWindow()
aboutAction := applicationMenu.AddAction(tr("About"))
aboutAction.SetMenuRole(qt.QAction__AboutRole)
aboutAction.ConnectTriggered(func(bool) {
aboutWindow.Show()
aboutWindow.Raise()
})
urlLineEdit := qt.NewQLineEdit(nil)
folderLineEdit := qt.NewQLineEdit2(guiConfig.DestinationFolder, nil)
folderLineEdit.SetReadOnly(true)
folderLineEdit.SetMinimumWidth(250)
folderButton := qt.NewQPushButton2(tr("Pick another folder"), nil)
folderDialog := qt.NewQFileDialog2(nil, tr("Destination folder"), guiConfig.DestinationFolder, "")
folderDialog.SetFileMode(qt.QFileDialog__DirectoryOnly)
folderButton.ConnectClicked(func(bool) {
if folderDialog.Exec() != int(qt.QDialog__Accepted) {
return
}
destinationFolder := qt.QDir_ToNativeSeparators(folderDialog.SelectedFiles()[0])
folderLineEdit.SetText(destinationFolder)
guiConfig.SetDestinationFolder(destinationFolder)
})
folderHBoxLayout := qt.NewQHBoxLayout()
folderHBoxLayout.AddWidget(folderLineEdit, 1, 0)
folderHBoxLayout.AddWidget(folderButton, 0, 0)
playlistCheckBox := qt.NewQCheckBox(nil)
playlistCheckBox.SetChecked(guiConfig.PlaylistEnabled)
playlistCheckBox.ConnectToggled(func(checked bool) {
guiConfig.SetPlaylistEnabled(checked)
})
inputFormLayout := qt.NewQFormLayout(nil)
inputFormLayout.SetFieldGrowthPolicy(qt.QFormLayout__AllNonFixedFieldsGrow)
inputFormLayout.AddRow3(tr("Video URL"), urlLineEdit)
inputFormLayout.AddRow4(tr("Destination folder"), folderHBoxLayout)
inputFormLayout.AddRow3(tr("Download playlists"), playlistCheckBox)
outputTextEdit := NewPlainTextEdit(nil)
outputTextEdit.SetReadOnly(true)
outputTextEdit.SetMinimumHeight(400)
outputTextEdit.SetMinimumWidth(1000)
outputTextEdit.SetLineWrapMode(qt.QPlainTextEdit__NoWrap)
monospaceFont := qt.NewQFont2("Courier", -1, -1, false)
monospaceFont.SetStyleHint(qt.QFont__Monospace, 0)
outputTextEdit.SetFont(monospaceFont)
output := newOutputBuffer(outputTextEdit)
output.addLine(tr("Awaiting user input"))
downloadButton := qt.NewQPushButton2(tr("Download"), nil)
downloadButton.ConnectClicked(func(bool) {
url := strings.TrimSpace(urlLineEdit.Text())
if len(url) > 0 {
output.addLine(time.Now().Format("15:04:05 ") + tr("Download started"))
config.OutputPath = guiConfig.DestinationFolder
config.Playlist = guiConfig.PlaylistEnabled
glog.Infof("downloading %s => %s", url, config.OutputPath)
savedStdout := os.Stdout
r, w, _ := os.Pipe()
output.attachReader(r)
os.Stdout = w
go func() {
for {
_, err := output.readLineAndUpdate()
if err != nil {
if err == io.EOF {
break
}
glog.Fatal(err)
}
// fmt.Fprint(savedStdout, line)
}
output.addLine("")
}()
go func() {
if !download(url) {
fmt.Println(tr("On network errors, e.g. HTTP 403, please retry a few times."))
}
w.Close()
os.Stdout = savedStdout
}()
}
})
layout := qt.NewQVBoxLayout()
layout.AddLayout(inputFormLayout, 0)
layout.AddWidget(downloadButton, 0, 0)
layout.AddWidget(outputTextEdit, 1, 0)
centralWidget.SetLayout(layout)
window.Show()
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGSEGV, syscall.SIGABRT)
go func() {
for {
sig := <-sigs
glog.Error(sig)
}
}()
qt.QApplication_Exec()
}