Refactor and copyrights
Move domains to internal/services, remove unnecessary versioning for services. Change copyright year.
This commit is contained in:
79
internal/services/fetcher/exported.go
Normal file
79
internal/services/fetcher/exported.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// NNM-Club torrent files mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/rs/zerolog"
|
||||
"gitlab.com/pztrn/flagger"
|
||||
|
||||
"gitlab.com/fat0troll/uploader_tools/internal/context"
|
||||
)
|
||||
|
||||
var (
|
||||
c *context.Context
|
||||
dclient http.Client
|
||||
dlog zerolog.Logger
|
||||
forumPages map[int]*goquery.Document
|
||||
forumPagesLinks map[string]string
|
||||
forumTopics map[int]*forumTopic
|
||||
forumTopicInProgress int
|
||||
outputDirPath string
|
||||
totalLength int64
|
||||
uberDebug bool
|
||||
)
|
||||
|
||||
// New initializes package
|
||||
func New(cc *context.Context) {
|
||||
c = cc
|
||||
dlog = c.Logger.With().Str("модуль", "fetcher").Int("версия", 1).Logger()
|
||||
|
||||
_ = c.Flagger.AddFlag(&flagger.Flag{
|
||||
Name: "forum",
|
||||
Description: "Номер форума, торренты с которого нужно скачать",
|
||||
Type: "int",
|
||||
DefaultValue: 0,
|
||||
})
|
||||
|
||||
_ = c.Flagger.AddFlag(&flagger.Flag{
|
||||
Name: "fetcherDebug",
|
||||
Description: "Запустить модуль fetcher в дебаг-режиме",
|
||||
Type: "bool",
|
||||
DefaultValue: false,
|
||||
})
|
||||
|
||||
_ = c.Flagger.AddFlag(&flagger.Flag{
|
||||
Name: "outputDir",
|
||||
Description: "Директория, в которую будут помещены скачанные торрент-файлы",
|
||||
Type: "string",
|
||||
DefaultValue: "./",
|
||||
})
|
||||
|
||||
forumPages = make(map[int]*goquery.Document)
|
||||
forumPagesLinks = make(map[string]string)
|
||||
forumTopics = make(map[int]*forumTopic)
|
||||
|
||||
dlog.Info().Msg("Модуль инициализирован")
|
||||
}
|
||||
|
||||
// Process handles authorization
|
||||
func Process() {
|
||||
uberDebug, _ = c.Flagger.GetBoolValue("fetcherDebug")
|
||||
forumID, _ := c.Flagger.GetIntValue("forum")
|
||||
|
||||
if forumID == 0 {
|
||||
dlog.Fatal().Msg("Номер форума не указан. Используйте ключ -forum XXX, чтобы указать номер форума")
|
||||
}
|
||||
|
||||
outputDirPathPrefix, _ := c.Flagger.GetStringValue("outputDir")
|
||||
|
||||
dlog.Info().Int("forum ID", forumID).Msg("Получен ID форума, начинаем работу...")
|
||||
|
||||
createOutputDir(outputDirPathPrefix, forumID)
|
||||
fetch(forumID)
|
||||
printStats()
|
||||
}
|
132
internal/services/fetcher/fetcher.go
Normal file
132
internal/services/fetcher/fetcher.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// NNM-Club torrent filess mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2020 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/kennygrant/sanitize"
|
||||
)
|
||||
|
||||
func createOutputDir(prefix string, forumID int) {
|
||||
path, _ := filepath.Abs(filepath.Join(prefix, strconv.Itoa(forumID)))
|
||||
if err := os.Mkdir(path, os.ModePerm); err != nil {
|
||||
dlog.Fatal().Str("путь создания", path).Err(err).Msg("Невозможно создать поддиректорию для торрент-файлов")
|
||||
}
|
||||
|
||||
outputDirPath = path
|
||||
}
|
||||
|
||||
func download(topic *forumTopic) {
|
||||
forumTopicInProgress++
|
||||
|
||||
dlog.Info().Str("название топика", topic.Name).Int("количество топиков", len(forumTopics)).
|
||||
Int("номер топика", forumTopicInProgress).Msg("Скачивается топик")
|
||||
|
||||
fileName, err := downloadFile(topic.Link, true)
|
||||
if err != nil {
|
||||
dlog.Error().Err(err).Str("название топика", topic.Name).Msg("Не удалось загрузить страницу топика. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
querier, err := obtainQuerier(fileName)
|
||||
if err != nil {
|
||||
dlog.Error().Err(err).Str("название топика", topic.Name).Str("ссылка на топик", topic.Link).
|
||||
Msg("Не удалось получить обработчик для страницы топика. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
lastModified := getLastModeratedDate(querier)
|
||||
if lastModified == "" {
|
||||
dlog.Warn().Str("название топика", topic.Name).Str("ссылка на топик", topic.Link).
|
||||
Msg("Топик не проверен модератором. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
downloadLink := getDownloadLink(querier)
|
||||
if downloadLink == "" {
|
||||
dlog.Warn().Str("название топика", topic.Name).Str("ссылка на топик", topic.Link).
|
||||
Msg("Не найдена ссылка для скачивания торрента. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
tempF, err := downloadFile("https://"+c.Config.URL+"/forum/"+downloadLink, false)
|
||||
if err != nil {
|
||||
dlog.Warn().Err(err).Str("название топика", topic.Name).Str("ссылка на топик", topic.Link).
|
||||
Msg("Не удалось скачать торрент-файл через несколько попыток. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
result := outputDirPath + "/" + lastModified + "-" + sanitize.BaseName(topic.Name) + ".torrent"
|
||||
|
||||
err = moveFile(tempF, result)
|
||||
if err != nil {
|
||||
dlog.Warn().Err(err).Str("название топика", topic.Name).Str("ссылка на топик", topic.Link).
|
||||
Msg("Не удалось сохранить торрент-файл. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
f, _ := os.Open(result)
|
||||
defer f.Close()
|
||||
|
||||
// Check downloaded torrent and get some metadata
|
||||
metadata, err := metainfo.Load(f)
|
||||
if err != nil {
|
||||
dlog.Warn().Err(err).Str("название топика", topic.Name).Str("ссылка на топик", topic.Link).
|
||||
Msg("Скачанный файл не является торрент-файлом. Пропуск")
|
||||
return
|
||||
}
|
||||
|
||||
unmarshalledMetadata, _ := metadata.UnmarshalInfo()
|
||||
totalLength += unmarshalledMetadata.TotalLength()
|
||||
}
|
||||
|
||||
func fetch(forumID int) {
|
||||
startPage := "https://" + c.Config.URL + "/forum/viewforum.php?f=" + strconv.Itoa(forumID)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
startPageFile, err := downloadFile(startPage, true)
|
||||
if err != nil {
|
||||
dlog.Error().Err(err).Msg("Не удалось получить данные с форума")
|
||||
}
|
||||
|
||||
if uberDebug {
|
||||
dlog.Info().Str("имя файла", startPageFile).Msg("Получена стартовая страница форума")
|
||||
}
|
||||
|
||||
err = setQuerier(startPageFile, "forumPage", 1)
|
||||
if err != nil {
|
||||
dlog.Fatal().Err(err).Msg("Не удалось создать обработчик для страницы")
|
||||
}
|
||||
|
||||
dlog.Info().Str("название форума", getForumName(forumPages[1])).Msg("Определён форум для загрузки")
|
||||
|
||||
getNavigation(forumPages[1])
|
||||
|
||||
downloadAdditionalPages()
|
||||
|
||||
for _, forumPage := range forumPages {
|
||||
getTopics(forumPage)
|
||||
}
|
||||
|
||||
dlog.Info().Int("топиков для скачивания", len(forumTopics)).Msg("Определены все топики для скачивания")
|
||||
|
||||
for _, forumTopic := range forumTopics {
|
||||
download(forumTopic)
|
||||
}
|
||||
}
|
||||
|
||||
func printStats() {
|
||||
dlog.Info().Int("топиков обработано", len(forumTopics)).
|
||||
Str("объём раздела при полном скачивании", humanize.Bytes(uint64(totalLength))).
|
||||
Str("директория с торрент-файлами", outputDirPath).
|
||||
Msg("Работа завершена успешно. Загруженные торрент-файлы расположены в директории, указанной при старте.")
|
||||
}
|
47
internal/services/fetcher/mover.go
Normal file
47
internal/services/fetcher/mover.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// NNM-Club torrent filess mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2020 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Source: https://gist.github.com/var23rav/23ae5d0d4d830aff886c3c970b8f6c6b
|
||||
|
||||
/*
|
||||
GoLang: os.Rename() give error "invalid cross-device link" for Docker container with Volumes.
|
||||
moveFile(source, destination) will work moving file between folders
|
||||
*/
|
||||
|
||||
func moveFile(sourcePath, destPath string) error {
|
||||
inputFile, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't open source file: %s", err)
|
||||
}
|
||||
|
||||
outputFile, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
inputFile.Close()
|
||||
return fmt.Errorf("couldn't open destination file: %s", err)
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
_, err = io.Copy(outputFile, inputFile)
|
||||
inputFile.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing to output file failed: %s", err)
|
||||
}
|
||||
|
||||
// The copy was successful, so now delete the original file
|
||||
err = os.Remove(sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed removing original file: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
121
internal/services/fetcher/parser.go
Normal file
121
internal/services/fetcher/parser.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// NNM-Club torrent files mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
var (
|
||||
// I haven't found internal solution in stdlib
|
||||
russianMonths = map[string]string{
|
||||
"Янв": "Jan",
|
||||
"Фев": "Feb",
|
||||
"Мар": "Mar",
|
||||
"Апр": "Apr",
|
||||
"Май": "May",
|
||||
"Июн": "Jun",
|
||||
"Июл": "Jul",
|
||||
"Авг": "Aug",
|
||||
"Сен": "Sep",
|
||||
"Окт": "Oct",
|
||||
"Ноя": "Nov",
|
||||
"Дек": "Dec",
|
||||
}
|
||||
)
|
||||
|
||||
// checkLoginness checks if downloaded page belongs to user
|
||||
func checkLoginness(querier *goquery.Document) bool {
|
||||
authorized := false
|
||||
|
||||
querier.Find(".mainmenu").Each(func(i int, sel *goquery.Selection) {
|
||||
if strings.Contains(sel.Text(), "Выход") {
|
||||
if strings.Contains(sel.Text(), c.Config.Username) {
|
||||
authorized = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return authorized
|
||||
}
|
||||
|
||||
func getDownloadLink(querier *goquery.Document) string {
|
||||
var downloadLink string
|
||||
|
||||
querier.Find("a[rel=nofollow]").Each(func(i int, sel *goquery.Selection) {
|
||||
if strings.Contains(sel.Text(), "Скачать") {
|
||||
href, _ := sel.Attr("href")
|
||||
downloadLink = href
|
||||
}
|
||||
})
|
||||
|
||||
return downloadLink
|
||||
}
|
||||
|
||||
func getForumName(querier *goquery.Document) string {
|
||||
return querier.Find("h1").First().Text()
|
||||
}
|
||||
|
||||
func getLastModeratedDate(querier *goquery.Document) string {
|
||||
var dateValue time.Time
|
||||
var err error
|
||||
|
||||
haveDate := false
|
||||
|
||||
querier.Find("table.btTbl td.genmed").Each(func(i int, sel *goquery.Selection) {
|
||||
if strings.Contains(sel.Text(), "модератором") {
|
||||
date := strings.TrimPrefix(sel.Text(), " Оформление проверено модератором ")
|
||||
// Fix russian months names
|
||||
for i := range russianMonths {
|
||||
date = strings.Replace(date, i, russianMonths[i], -1)
|
||||
}
|
||||
dateValue, err = time.Parse("02 Jan 2006 15:04:05", date)
|
||||
if err != nil {
|
||||
dlog.Error().Err(err).Str("строка поиска даты", sel.Text()).
|
||||
Msg("Не удалось получить дату проверки торрента")
|
||||
} else {
|
||||
haveDate = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if !haveDate {
|
||||
return ""
|
||||
}
|
||||
|
||||
return dateValue.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func getNavigation(querier *goquery.Document) {
|
||||
querier.Find("td[align=right] .nav a").Each(func(i int, sel *goquery.Selection) {
|
||||
if !strings.Contains(sel.Text(), "След") {
|
||||
href, _ := sel.Attr("href")
|
||||
forumPagesLinks[sel.Text()] = href
|
||||
}
|
||||
})
|
||||
|
||||
dlog.Info().Int("количество страниц", len(forumPagesLinks)+1).Msg("Определено количество страниц")
|
||||
}
|
||||
|
||||
func getTopics(querier *goquery.Document) {
|
||||
querier.Find(".forumline tr").Each(func(i int, sel *goquery.Selection) {
|
||||
if strings.Contains(sel.Text(), "DL:") {
|
||||
link := sel.Find("h2 a")
|
||||
topicName := link.Text()
|
||||
href, _ := link.Attr("href")
|
||||
href = "https://" + c.Config.URL + "/forum/" + href
|
||||
if uberDebug {
|
||||
dlog.Debug().Str("название", topicName).Str("ссылка", href).Msg("Найден элемент для загрузки")
|
||||
}
|
||||
forumTopics[len(forumTopics)] = &forumTopic{
|
||||
Link: href,
|
||||
Name: topicName,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
48
internal/services/fetcher/querier.go
Normal file
48
internal/services/fetcher/querier.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// NNM-Club torrent files mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func obtainQuerier(pageFile string) (*goquery.Document, error) {
|
||||
f, err := os.Open(pageFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
querier, err := goquery.NewDocumentFromReader(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return querier, nil
|
||||
}
|
||||
|
||||
func setQuerier(pageFile string, pageType string, page int) error {
|
||||
querier, err := obtainQuerier(pageFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch pageType {
|
||||
case "forumPage":
|
||||
if !checkLoginness(querier) {
|
||||
return errors.New("получена анонимная страница")
|
||||
}
|
||||
|
||||
forumPages[page] = querier
|
||||
|
||||
default:
|
||||
return errors.New("неизвестный тип страницы")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
90
internal/services/fetcher/request.go
Normal file
90
internal/services/fetcher/request.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// NNM-Club torrent files mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func downloadAdditionalPages() {
|
||||
for i := range forumPagesLinks {
|
||||
forumPage, _ := strconv.Atoi(i)
|
||||
dlog.Info().Int("номер страницы", forumPage).Msg("Скачивается ещё одна страница форума")
|
||||
|
||||
pageFile, err := downloadFile("https://"+c.Config.URL+"/forum/"+forumPagesLinks[i], true)
|
||||
if err != nil {
|
||||
dlog.Fatal().Err(err).Msg("Не удалось загрузить страницу форума")
|
||||
}
|
||||
|
||||
_ = setQuerier(pageFile, "forumPage", forumPage)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(url string, transformEncoding bool) (string, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := range c.Cookies {
|
||||
req.AddCookie(c.Cookies[i])
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var retryCount int = 0
|
||||
|
||||
for {
|
||||
if retryCount < 5 {
|
||||
resp, err = dclient.Do(req)
|
||||
if err != nil {
|
||||
if uberDebug {
|
||||
dlog.Debug().Err(err).Int("попытка", retryCount+1).Msg("Не удалось получить данные, пытаемся ещё раз")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
retryCount++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
tempF, err := ioutil.TempFile("", "massdl-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tempF.Close()
|
||||
|
||||
if transformEncoding {
|
||||
respInUTF8 := transform.NewReader(resp.Body, charmap.Windows1251.NewDecoder())
|
||||
|
||||
_, err = io.Copy(tempF, respInUTF8)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
_, err = io.Copy(tempF, resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if uberDebug {
|
||||
dlog.Debug().Str("расположение", tempF.Name()).Msg("Загружен файл из сети")
|
||||
}
|
||||
|
||||
return tempF.Name(), nil
|
||||
}
|
10
internal/services/fetcher/structs.go
Normal file
10
internal/services/fetcher/structs.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// NNM-Club torrent files mass downloader
|
||||
// Created for Uploaders group
|
||||
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
|
||||
|
||||
package fetcher
|
||||
|
||||
type forumTopic struct {
|
||||
Link string
|
||||
Name string
|
||||
}
|
Reference in New Issue
Block a user