aboutsummaryrefslogtreecommitdiff
path: root/internal/reader/processor/bilibili.go
blob: b207ff03186278c8a1a5eddd6c085d68a0a3e709 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package processor

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"regexp"

	"miniflux.app/v2/internal/config"
	"miniflux.app/v2/internal/model"
	"miniflux.app/v2/internal/reader/fetcher"
)

var (
	bilibiliURLRegex     = regexp.MustCompile(`bilibili\.com/video/(.*)$`)
	bilibiliVideoIdRegex = regexp.MustCompile(`/video/(?:av(\d+)|BV([a-zA-Z0-9]+))`)
)

func shouldFetchBilibiliWatchTime(entry *model.Entry) bool {
	if !config.Opts.FetchBilibiliWatchTime() {
		return false
	}
	matches := bilibiliURLRegex.FindStringSubmatch(entry.URL)
	urlMatchesBilibiliPattern := len(matches) == 2
	return urlMatchesBilibiliPattern
}

func extractBilibiliVideoID(websiteURL string) (string, string, error) {
	matches := bilibiliVideoIdRegex.FindStringSubmatch(websiteURL)
	if matches == nil {
		return "", "", fmt.Errorf("no video ID found in URL: %s", websiteURL)
	}
	if matches[1] != "" {
		return "aid", matches[1], nil
	}
	if matches[2] != "" {
		return "bvid", matches[2], nil
	}
	return "", "", fmt.Errorf("unexpected regex match result for URL: %s", websiteURL)
}

func fetchBilibiliWatchTime(websiteURL string) (int, error) {
	requestBuilder := fetcher.NewRequestBuilder()
	requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
	requestBuilder.WithProxy(config.Opts.HTTPClientProxy())

	idType, videoID, extractErr := extractBilibiliVideoID(websiteURL)
	if extractErr != nil {
		return 0, extractErr
	}
	bilibiliApiURL := fmt.Sprintf("https://api.bilibili.com/x/web-interface/view?%s=%s", idType, videoID)

	responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(bilibiliApiURL))
	defer responseHandler.Close()

	if localizedError := responseHandler.LocalizedError(); localizedError != nil {
		slog.Warn("Unable to fetch Bilibili API",
			slog.String("website_url", websiteURL),
			slog.String("api_url", bilibiliApiURL),
			slog.Any("error", localizedError.Error()))
		return 0, localizedError.Error()
	}

	var result map[string]interface{}
	doc := json.NewDecoder(responseHandler.Body(config.Opts.HTTPClientMaxBodySize()))
	if docErr := doc.Decode(&result); docErr != nil {
		return 0, fmt.Errorf("failed to decode API response: %v", docErr)
	}

	if code, ok := result["code"].(float64); !ok || code != 0 {
		return 0, fmt.Errorf("API returned error code: %v", result["code"])
	}

	data, ok := result["data"].(map[string]interface{})
	if !ok {
		return 0, fmt.Errorf("data field not found or not an object")
	}

	duration, ok := data["duration"].(float64)
	if !ok {
		return 0, fmt.Errorf("duration not found or not a number")
	}
	intDuration := int(duration)
	durationMin := intDuration / 60
	if intDuration%60 != 0 {
		durationMin++
	}
	return durationMin, nil
}