1

Refactor and copyrights

Move domains to internal/services, remove unnecessary versioning for services.
Change copyright year.
This commit is contained in:
2021-12-20 16:53:38 +04:00
parent fd184225f1
commit 32a2262e8c
19 changed files with 60 additions and 78 deletions

View 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()
}

View 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("Работа завершена успешно. Загруженные торрент-файлы расположены в директории, указанной при старте.")
}

View 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
}

View 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,
}
}
})
}

View 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
}

View 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
}

View 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
}

View File

@@ -0,0 +1,12 @@
// NNM-Club torrent files mass downloader
// Created for Uploaders group
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
package loginer
// checkConfig checks if there's some errors in config file
func checkConfig() {
if c.Config.URL == "" || c.Config.Password == "" || c.Config.Username == "" {
dlog.Fatal().Msg("Конфиг не заполнен или заполнен не до конца. Запустите программу с ключом -auth для авторизации.")
}
}

View File

@@ -0,0 +1,44 @@
// NNM-Club torrent files mass downloader
// Created for Uploaders group
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
package loginer
import (
"github.com/rs/zerolog"
"gitlab.com/pztrn/flagger"
"gitlab.com/fat0troll/uploader_tools/internal/context"
)
var (
c *context.Context
dlog zerolog.Logger
)
// New initializes package
func New(cc *context.Context) {
c = cc
dlog = c.Logger.With().Str("модуль", "loginer").Int("версия", 1).Logger()
_ = c.Flagger.AddFlag(&flagger.Flag{
Name: "auth",
Description: "Запустить аутентификацию на сайте NNM-Club с начала.",
Type: "bool",
DefaultValue: false,
})
dlog.Info().Msg("Модуль инициализирован")
}
// Process handles authorization
func Process() {
auth, _ := c.Flagger.GetBoolValue("auth")
if auth {
login()
}
checkConfig()
obtainCookies()
}

View File

@@ -0,0 +1,113 @@
// NNM-Club torrent files mass downloader
// Created for Uploaders group
// Copyright (c) 2012-2022 Vladimir "fat0troll" Hodakov
package loginer
import (
"bufio"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// login function name is self-descriptive.
// After successful login it will write cookie file and config in system's
// config directory
func login() {
dlog.Info().Msg("Введите адрес сайта NNM-Club, без протокола (к примеру, nnmclub.ro)")
// First enter is site name, second and third are user credentials
scanner := bufio.NewScanner(os.Stdin)
scanline := 0
for scanner.Scan() {
scanline++
switch {
case scanline == 1:
c.Config.URL = scanner.Text()
dlog.Info().Msg("Введите имя пользователя")
case scanline == 2:
c.Config.Username = scanner.Text()
dlog.Info().Msg("Введите пароль")
default:
c.Config.Password = scanner.Text()
}
if scanline == 3 {
break
}
}
if scanner.Err() != nil {
dlog.Fatal().Err(scanner.Err()).Msg("Не удалось прочитать пользовательский ввод.")
}
c.SaveConfig()
obtainCookies()
dlog.Info().Msg("Логин прошёл успешно. Перезапустите программу с ключём -forum НОМЕРОРУМА.")
os.Exit(0)
}
// obtainCookie tries to login to NNM-Club and saves resulting cookie
// If it can't save cookie, the program will terminate with error and you will
// need to authorize again or retry cookie obtaining.
func obtainCookies() {
// This value is obtained from login.php sources.
nnmClubCode := "58161005a04f0ee5"
client := http.Client{
// This is disables redirects for this request. Otherwise we can't obtain cookies
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
formData := url.Values{}
formData.Add("username", c.Config.Username)
formData.Add("password", c.Config.Password)
formData.Add("autologin", "on")
formData.Add("redirect", "index.php")
formData.Add("code", nnmClubCode)
formData.Add("login", "%C2%F5%EE%E4")
url := "https://" + c.Config.URL + "/forum/login.php"
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
retryCount := 0
var resp *http.Response
var err error
for {
if retryCount == 10 {
dlog.Fatal().Err(err).Msg("Не удалось отправить запрос на авторизацию в NNM-Club.")
}
resp, err = client.Do(req)
if err != nil {
dlog.Debug().Err(err).Int("попытка", retryCount).Msg("Не удалось отправить запрос, попробуем ещё раз")
retryCount++
time.Sleep(2 * time.Second)
} else {
break
}
}
defer resp.Body.Close()
if len(resp.Cookies()) == 0 {
dlog.Fatal().Msg("Не удалось получить печеньки от сайта NNM-Club. Неправильные имя/пароль или адрес сайта.")
}
c.SetCookies(resp.Cookies())
dlog.Info().Str("username", c.Config.Username).Msg("Успешный логин.")
}