Compare commits
No commits in common. "5a468140395fff4e8b09337e26cc18daa3f363ca" and "eeda6976ec1d4b297015e64d4ceff94af1e743a1" have entirely different histories.
5a46814039
...
eeda6976ec
Binary file not shown.
@ -1,8 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
// backend\cookies_crypto.go
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
// backend\cors.go
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -65,8 +65,6 @@ type RecordJob struct {
|
|||||||
VideoHeight int `json:"videoHeight,omitempty"`
|
VideoHeight int `json:"videoHeight,omitempty"`
|
||||||
FPS float64 `json:"fps,omitempty"`
|
FPS float64 `json:"fps,omitempty"`
|
||||||
Meta *videoMeta `json:"meta,omitempty"`
|
Meta *videoMeta `json:"meta,omitempty"`
|
||||||
ModelImageURL string `json:"modelImageUrl,omitempty"`
|
|
||||||
Avatar string `json:"avatar,omitempty"`
|
|
||||||
|
|
||||||
Hidden bool `json:"-"`
|
Hidden bool `json:"-"`
|
||||||
|
|
||||||
@ -121,8 +119,6 @@ type jobEventSnapshot struct {
|
|||||||
PreviewState string
|
PreviewState string
|
||||||
PostWorkKey string
|
PostWorkKey string
|
||||||
PostWork *PostWorkKeyStatus
|
PostWork *PostWorkKeyStatus
|
||||||
ModelImageURL string
|
|
||||||
Avatar string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dummyResponseWriter struct {
|
type dummyResponseWriter struct {
|
||||||
@ -182,8 +178,6 @@ func publishJobUpsertSnapshot(s jobEventSnapshot) {
|
|||||||
"previewState": s.PreviewState,
|
"previewState": s.PreviewState,
|
||||||
"postWorkKey": s.PostWorkKey,
|
"postWorkKey": s.PostWorkKey,
|
||||||
"ts": time.Now().UnixMilli(),
|
"ts": time.Now().UnixMilli(),
|
||||||
"modelImageUrl": s.ModelImageURL,
|
|
||||||
"avatar": s.Avatar,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.PostWork != nil {
|
if s.PostWork != nil {
|
||||||
@ -238,8 +232,6 @@ func publishJobSnapshot(jobID string) bool {
|
|||||||
PreviewState: job.PreviewState,
|
PreviewState: job.PreviewState,
|
||||||
PostWorkKey: job.PostWorkKey,
|
PostWorkKey: job.PostWorkKey,
|
||||||
PostWork: pw,
|
PostWork: pw,
|
||||||
ModelImageURL: job.ModelImageURL,
|
|
||||||
Avatar: job.Avatar,
|
|
||||||
}
|
}
|
||||||
jobsMu.RUnlock()
|
jobsMu.RUnlock()
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,7 @@ func getWantedResolutionPlaylistWithHeaders(
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, appErrorf("HTTP %d beim Abruf der m3u8: %s", resp.StatusCode, playlistURL)
|
return nil, appErrorf("HTTP %d beim Abruf der m3u8", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawBody, err := io.ReadAll(resp.Body)
|
rawBody, err := io.ReadAll(resp.Body)
|
||||||
|
|||||||
@ -3,16 +3,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
"github.com/grafov/m3u8"
|
"github.com/grafov/m3u8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,34 +38,18 @@ func RecordStreamMFC(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
st, err := mfc.GetStatus(ctx, hc.userAgent)
|
st, err := mfc.GetStatus()
|
||||||
if err == nil && st == StatusPublic {
|
if err == nil && st == StatusPublic {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
if err != nil {
|
|
||||||
return appErrorf("mfc status: %w", err)
|
|
||||||
}
|
|
||||||
return context.DeadlineExceeded
|
return context.DeadlineExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
if job != nil {
|
|
||||||
img := strings.TrimSpace(mfc.PreviewImageURL())
|
|
||||||
if img != "" {
|
|
||||||
jobsMu.Lock()
|
|
||||||
job.ModelImageURL = img
|
|
||||||
job.Avatar = img
|
|
||||||
job.PreviewState = "public"
|
|
||||||
jobsMu.Unlock()
|
|
||||||
|
|
||||||
_ = publishJobSnapshot(job.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ erst jetzt die Video URL holen (weil public)
|
// ✅ erst jetzt die Video URL holen (weil public)
|
||||||
stream, err := mfc.GetSelectedStream(ctx, false, hc.userAgent)
|
stream, err := mfc.GetSelectedStream(ctx, false, hc.userAgent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -84,128 +70,20 @@ func RecordStreamMFC(
|
|||||||
job.PreviewM3U8 = previewURL
|
job.PreviewM3U8 = previewURL
|
||||||
job.PreviewCookie = ""
|
job.PreviewCookie = ""
|
||||||
job.PreviewUA = hc.userAgent
|
job.PreviewUA = hc.userAgent
|
||||||
|
|
||||||
if img := strings.TrimSpace(mfc.PreviewImageURL()); img != "" {
|
|
||||||
job.ModelImageURL = img
|
|
||||||
job.Avatar = img
|
|
||||||
job.PreviewState = "public"
|
|
||||||
}
|
|
||||||
|
|
||||||
jobsMu.Unlock()
|
jobsMu.Unlock()
|
||||||
|
|
||||||
_ = publishJobSnapshot(job.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if job != nil {
|
if job != nil {
|
||||||
_ = publishJob(job.ID)
|
_ = publishJob(job.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return recordHLSPlaylistSegments(ctx, stream, outputPath, job, "", hc.userAgent, mfc.GetWebsiteURL())
|
return handleM3U8Mode(ctx, stream, outputPath, job, "", hc.userAgent, mfc.GetWebsiteURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
type MyFreeCams struct {
|
type MyFreeCams struct {
|
||||||
Username string
|
Username string
|
||||||
Attrs map[string]string
|
Attrs map[string]string
|
||||||
VideoURL string
|
VideoURL string
|
||||||
|
|
||||||
ModelID string
|
|
||||||
ServerID string
|
|
||||||
Phase string
|
|
||||||
AvatarURL string
|
|
||||||
|
|
||||||
RawUserID int
|
|
||||||
SnapURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
type mfcUsernameLookupResp struct {
|
|
||||||
Result struct {
|
|
||||||
User struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Avatar string `json:"avatar"`
|
|
||||||
|
|
||||||
Sessions []struct {
|
|
||||||
ServerName string `json:"server_name"`
|
|
||||||
Phase string `json:"phase"`
|
|
||||||
SnapURL string `json:"snap_url"`
|
|
||||||
} `json:"sessions"`
|
|
||||||
} `json:"user"`
|
|
||||||
} `json:"result"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeMFCSnapURL(raw string) string {
|
|
||||||
raw = strings.TrimSpace(raw)
|
|
||||||
if raw == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(raw, "//") {
|
|
||||||
raw = "https:" + raw
|
|
||||||
} else if strings.HasPrefix(raw, "/") {
|
|
||||||
raw = "https://snap.mfcimg.com" + raw
|
|
||||||
} else if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
|
|
||||||
raw = "https://snap.mfcimg.com/" + strings.TrimLeft(raw, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(raw)
|
|
||||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
q := u.Query()
|
|
||||||
q.Set("no-cache", strconv.FormatInt(time.Now().UnixNano(), 10))
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMFCSnapimgURL(serverID string, rawUserID int) string {
|
|
||||||
serverID = strings.TrimSpace(serverID)
|
|
||||||
if serverID == "" || rawUserID <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizeMFCSnapURL(fmt.Sprintf(
|
|
||||||
"https://snap.mfcimg.com/snapimg/%s/853x480/mfc_%d",
|
|
||||||
serverID,
|
|
||||||
rawUserID,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MyFreeCams) PreviewImageURL() string {
|
|
||||||
if s := strings.TrimSpace(m.SnapURL); s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(m.AvatarURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mfcDigitsOnly(s string) string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, r := range strings.TrimSpace(s) {
|
|
||||||
if r >= '0' && r <= '9' {
|
|
||||||
b.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeMFCPhase(phase string) string {
|
|
||||||
phase = strings.TrimSpace(phase)
|
|
||||||
|
|
||||||
// API liefert meistens schon "a_" oder "".
|
|
||||||
if phase == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if phase == "a" {
|
|
||||||
return "a_"
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(phase, "a") {
|
|
||||||
return "a_"
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(phase, "_") {
|
|
||||||
return phase + "_"
|
|
||||||
}
|
|
||||||
return phase
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMyFreeCams(username string) *MyFreeCams {
|
func NewMyFreeCams(username string) *MyFreeCams {
|
||||||
@ -219,204 +97,145 @@ func (m *MyFreeCams) GetWebsiteURL() string {
|
|||||||
return "https://www.myfreecams.com/#" + m.Username
|
return "https://www.myfreecams.com/#" + m.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueStrings(in []string) []string {
|
|
||||||
seen := map[string]struct{}{}
|
|
||||||
out := make([]string, 0, len(in))
|
|
||||||
|
|
||||||
for _, s := range in {
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
if s == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[s]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[s] = struct{}{}
|
|
||||||
out = append(out, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MyFreeCams) GetVideoURLCandidates() ([]string, error) {
|
|
||||||
serverID := strings.TrimSpace(m.ServerID)
|
|
||||||
modelID := strings.TrimSpace(m.ModelID)
|
|
||||||
phase := normalizeMFCPhase(m.Phase)
|
|
||||||
|
|
||||||
if serverID == "" || modelID == "" {
|
|
||||||
return nil, errors.New("mfc: lookup daten fehlen; GetStatus() wurde nicht erfolgreich ausgeführt")
|
|
||||||
}
|
|
||||||
|
|
||||||
streamName := phase + modelID
|
|
||||||
|
|
||||||
candidates := []string{
|
|
||||||
fmt.Sprintf(
|
|
||||||
"https://edgevideo.myfreecams.com/llhls/NxServer/%s/ngrp:mfc_%s.f4v_cmaf/playlist.m3u8?nc=%d&v=1.97.23",
|
|
||||||
serverID,
|
|
||||||
streamName,
|
|
||||||
time.Now().UnixNano(),
|
|
||||||
),
|
|
||||||
|
|
||||||
fmt.Sprintf(
|
|
||||||
"https://edgevideo.myfreecams.com/llhls/NxServer/%s/ngrp:mfc_%s.f4v_cmaf/playlist.m3u8",
|
|
||||||
serverID,
|
|
||||||
streamName,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueStrings(candidates), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MyFreeCams) GetVideoURL(refresh bool) (string, error) {
|
func (m *MyFreeCams) GetVideoURL(refresh bool) (string, error) {
|
||||||
if !refresh && strings.TrimSpace(m.VideoURL) != "" {
|
if !refresh && m.VideoURL != "" {
|
||||||
return m.VideoURL, nil
|
return m.VideoURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates, err := m.GetVideoURLCandidates()
|
// Prüfen, ob alle benötigten Attribute vorhanden sind
|
||||||
|
if _, ok := m.Attrs["data-cam-preview-model-id-value"]; !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
sid := m.Attrs["data-cam-preview-server-id-value"]
|
||||||
|
midBase := m.Attrs["data-cam-preview-model-id-value"]
|
||||||
|
isWzobs := strings.ToLower(m.Attrs["data-cam-preview-is-wzobs-value"]) == "true"
|
||||||
|
|
||||||
|
midInt, err := strconv.Atoi(midBase)
|
||||||
|
if err != nil {
|
||||||
|
return "", appErrorf("model-id parse error: %w", err)
|
||||||
|
}
|
||||||
|
mid := 100000000 + midInt
|
||||||
|
a := ""
|
||||||
|
if isWzobs {
|
||||||
|
a = "a_"
|
||||||
|
}
|
||||||
|
|
||||||
|
playlistURL := fmt.Sprintf(
|
||||||
|
"https://previews.myfreecams.com/hls/NxServer/%s/ngrp:mfc_%s%d.f4v_mobile_mhp1080_previewurl/playlist.m3u8",
|
||||||
|
sid, a, mid,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validieren (HTTP 200) & ggf. auf gewünschte Auflösung verlinken
|
||||||
|
u, err := getWantedResolutionPlaylist(playlistURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(candidates) == 0 {
|
m.VideoURL = u
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.VideoURL = candidates[0]
|
|
||||||
return m.VideoURL, nil
|
return m.VideoURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MyFreeCams) GetSelectedStream(ctx context.Context, refresh bool, userAgent string) (*selectedHLSStream, error) {
|
func (m *MyFreeCams) GetSelectedStream(ctx context.Context, refresh bool, userAgent string) (*selectedHLSStream, error) {
|
||||||
candidates, err := m.GetVideoURLCandidates()
|
m3u8URL, err := m.GetVideoURL(refresh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(candidates) == 0 {
|
if strings.TrimSpace(m3u8URL) == "" {
|
||||||
return nil, errors.New("keine mfc m3u8 kandidaten gefunden")
|
return nil, errors.New("keine m3u8 URL gefunden")
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastErr error
|
return getWantedResolutionPlaylistWithHeaders(
|
||||||
tried := make([]string, 0, len(candidates))
|
|
||||||
|
|
||||||
for _, candidate := range candidates {
|
|
||||||
tried = append(tried, candidate)
|
|
||||||
|
|
||||||
stream, err := getWantedResolutionPlaylistWithHeaders(
|
|
||||||
ctx,
|
ctx,
|
||||||
candidate,
|
m3u8URL,
|
||||||
"",
|
"", // MFC normalerweise ohne Cookies
|
||||||
userAgent,
|
userAgent,
|
||||||
m.GetWebsiteURL(),
|
m.GetWebsiteURL(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == nil && stream != nil && strings.TrimSpace(stream.VideoURL) != "" {
|
|
||||||
m.VideoURL = candidate
|
|
||||||
appLogln("✅ mfc playlist ok:", candidate)
|
|
||||||
appLogln("✅ mfc video playlist:", stream.VideoURL)
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
appLogln("⚠️ mfc playlist candidate failed:", candidate, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastErr != nil {
|
|
||||||
return nil, appErrorf(
|
|
||||||
"keine mfc m3u8 abrufbar; letzter fehler: %w; getestete urls: %s",
|
|
||||||
lastErr,
|
|
||||||
strings.Join(tried, " | "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, appErrorf("keine mfc m3u8 abrufbar; getestete urls: %s", strings.Join(tried, " | "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MyFreeCams) GetStatus(ctx context.Context, userAgent string) (Status, error) {
|
func (m *MyFreeCams) GetStatus() (Status, error) {
|
||||||
username := strings.TrimSpace(m.Username)
|
// 1) share-Seite prüfen (existiert/nicht existiert)
|
||||||
if username == "" {
|
shareURL := "https://share.myfreecams.com/" + m.Username
|
||||||
return StatusUnknown, errors.New("mfc: username fehlt")
|
resp, err := http.Get(shareURL)
|
||||||
}
|
|
||||||
|
|
||||||
lookupURL := "https://api-edge.myfreecams.com/usernameLookup/" + url.PathEscape(username)
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, lookupURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return StatusUnknown, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ua := strings.TrimSpace(userAgent); ua != "" {
|
|
||||||
req.Header.Set("User-Agent", ua)
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
req.Header.Set("Referer", "https://www.myfreecams.com/#"+username)
|
|
||||||
req.Header.Set("Origin", "https://www.myfreecams.com")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StatusUnknown, err
|
return StatusUnknown, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNotFound {
|
if resp.StatusCode == 404 {
|
||||||
return StatusNotExist, nil
|
return StatusNotExist, nil
|
||||||
}
|
}
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != 200 {
|
||||||
return StatusUnknown, appErrorf("mfc usernameLookup HTTP %d: %s", resp.StatusCode, lookupURL)
|
return StatusUnknown, appErrorf("HTTP %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var data mfcUsernameLookupResp
|
// wir brauchen sowohl Bytes (für Suche) als auch Reader (für HTML)
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
return StatusUnknown, appErrorf("mfc usernameLookup json: %w", err)
|
if err != nil {
|
||||||
|
return StatusUnknown, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Result.User.ID <= 0 {
|
// 2) „tracking.php?“ suchen und prüfen, ob model_id vorhanden ist
|
||||||
|
start := bytes.Index(bodyBytes, []byte("https://www.myfreecams.com/php/tracking.php?"))
|
||||||
|
if start == -1 {
|
||||||
|
// ohne tracking Parameter -> behandeln wie nicht existent
|
||||||
|
return StatusNotExist, nil
|
||||||
|
}
|
||||||
|
end := bytes.IndexByte(bodyBytes[start:], '"')
|
||||||
|
if end == -1 {
|
||||||
|
return StatusUnknown, errors.New("tracking url parse failed")
|
||||||
|
}
|
||||||
|
raw := string(bodyBytes[start : start+end])
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return StatusUnknown, appErrorf("tracking url invalid: %w", err)
|
||||||
|
}
|
||||||
|
qs := u.Query()
|
||||||
|
if qs.Get("model_id") == "" {
|
||||||
return StatusNotExist, nil
|
return StatusNotExist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.RawUserID = data.Result.User.ID
|
// 3) HTML parsen und <div class="campreview" ...> Attribute auslesen
|
||||||
m.AvatarURL = strings.TrimSpace(data.Result.User.Avatar)
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return StatusUnknown, err
|
||||||
|
}
|
||||||
|
|
||||||
if len(data.Result.User.Sessions) == 0 {
|
params := doc.Find(".campreview").First()
|
||||||
|
if params.Length() == 0 {
|
||||||
|
// keine campreview -> offline
|
||||||
return StatusOffline, nil
|
return StatusOffline, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := data.Result.User.Sessions[0]
|
attrs := map[string]string{}
|
||||||
|
params.Each(func(_ int, s *goquery.Selection) {
|
||||||
serverID := mfcDigitsOnly(sess.ServerName)
|
for _, a := range []string{
|
||||||
if serverID == "" {
|
"data-cam-preview-server-id-value",
|
||||||
return StatusUnknown, appErrorf("mfc: server_name ohne server-id: %q", sess.ServerName)
|
"data-cam-preview-model-id-value",
|
||||||
|
"data-cam-preview-is-wzobs-value",
|
||||||
|
} {
|
||||||
|
if v, ok := s.Attr(a); ok {
|
||||||
|
attrs[a] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SnapURL = buildMFCSnapimgURL(serverID, data.Result.User.ID)
|
|
||||||
|
|
||||||
if strings.TrimSpace(m.SnapURL) == "" && strings.TrimSpace(sess.SnapURL) != "" {
|
|
||||||
m.SnapURL = normalizeMFCSnapURL(sess.SnapURL)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
m.Attrs = attrs
|
||||||
|
|
||||||
modelID := data.Result.User.ID
|
// 4) Versuchen, VideoURL (Preview-HLS) zu ermitteln
|
||||||
if modelID < 100000000 {
|
uStr, err := m.GetVideoURL(true)
|
||||||
modelID += 100000000
|
if err != nil {
|
||||||
|
return StatusUnknown, err
|
||||||
}
|
}
|
||||||
|
if uStr != "" {
|
||||||
m.ModelID = strconv.Itoa(modelID)
|
|
||||||
m.ServerID = serverID
|
|
||||||
m.Phase = normalizeMFCPhase(sess.Phase)
|
|
||||||
|
|
||||||
appLogln("✅ mfc lookup:",
|
|
||||||
"user=", username,
|
|
||||||
"modelID=", m.ModelID,
|
|
||||||
"serverID=", m.ServerID,
|
|
||||||
"phase=", m.Phase,
|
|
||||||
"serverName=", sess.ServerName,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StatusPublic, nil
|
return StatusPublic, nil
|
||||||
|
}
|
||||||
|
// campreview vorhanden, aber keine playable url -> „PRIVATE“
|
||||||
|
return StatusPrivate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMFC(ctx context.Context, username string, outArg string) error {
|
func runMFC(ctx context.Context, username string, outArg string) error {
|
||||||
mfc := NewMyFreeCams(username)
|
mfc := NewMyFreeCams(username)
|
||||||
|
|
||||||
st, err := mfc.GetStatus(ctx, "")
|
st, err := mfc.GetStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
||||||
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
||||||
"recordDir": "C:\\Users\\Chris\\Desktop\\privat\\records",
|
"recordDir": "C:\\Users\\Rother\\Desktop\\Privat\\records",
|
||||||
"doneDir": "C:\\Users\\Chris\\Desktop\\privat\\records\\done",
|
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
|
||||||
"ffmpegPath": "",
|
"ffmpegPath": "",
|
||||||
"autoAddToDownloadList": true,
|
"autoAddToDownloadList": true,
|
||||||
"enableConcurrentDownloadsLimit": true,
|
"enableConcurrentDownloadsLimit": true,
|
||||||
@ -13,7 +13,7 @@
|
|||||||
"autoDeleteSmallDownloadsBelowMB": 300,
|
"autoDeleteSmallDownloadsBelowMB": 300,
|
||||||
"autoDeleteSmallDownloadsKeepFavorites": false,
|
"autoDeleteSmallDownloadsKeepFavorites": false,
|
||||||
"lowDiskPauseBelowGB": 5,
|
"lowDiskPauseBelowGB": 5,
|
||||||
"blurPreviews": false,
|
"blurPreviews": true,
|
||||||
"teaserPlayback": "all",
|
"teaserPlayback": "all",
|
||||||
"teaserAudio": true,
|
"teaserAudio": true,
|
||||||
"enableNotifications": true,
|
"enableNotifications": true,
|
||||||
@ -25,5 +25,5 @@
|
|||||||
"enrichPostworkEnabled": false,
|
"enrichPostworkEnabled": false,
|
||||||
"trainingRecognitionEnabled": true,
|
"trainingRecognitionEnabled": true,
|
||||||
"trainingDetectorEpochs": 60,
|
"trainingDetectorEpochs": 60,
|
||||||
"encryptedCookies": "p0aRfigWn3+TfypNAV7vPky13RW+gwhTlhNOGKKAED0FSMf0kdGV6LgIizzTeDMD5Ck+9SGGf6EfVg+RPnEr3ZjkkkjOTVGOJ9XNzpuczHqzi0ifmt0l9/lnNTaqh8oTzrk09uQC9YYe0CtsY6gjpYFAdVEPGimCPdULqn+8Jln1CSDiBGRGF0uZGxfmAZ1T/YQym/wjiCCdIRfcjTF4f+sz6PMBQOOnTA74if1OFNA6PxpGoXfHS7xvMeV6RX3J/qcTyMmFWr+uD2h1UKs7N9w4Y1sfw4heB10jzy/VL+9gWg2o4AKE5tGqOmlVYpv8KculMWfJHNxNpRonPNizPR4jO2jZYdr3AikuXTTeILyN7hb4MtO4fXblJ+9of1ABR6LvDe1dAXmbkNdqEqRo0k1WS6jaCdNkrleP8vGuH56H7jZMnEeJAcQxS99kVjFo0qMAEJ2ai8HPkdYURC3dWSu9uIbGmiqi6hgva2WM6jPG7TqZDyqUS7hjJwAmPa3aLPnsEWHqkcE="
|
"encryptedCookies": "FEPj/igIPaDy8xr709QjHKOqXWVGesegmwzhTP1Whnusetrl1WenN7t0V1Rm/lw2nLkyis78kz+4yjsDtVgO3MXLU4Uq85AhAJQOE7SMVD+YMNenCS8Qr0JLkmI6cr/kR6sKrx7Jm6x6DY1BolFFMX1Ii3EO+8b4Re2GYaFi1iuh97oI8Zg4r5sMYmt7FIMRkr1uy0u0ToH8hsO0iDoExcxubQNHIZTQ07kkWP70Up78tYs59INxzosijATbiVhOT1H0YwS23iWC3bwLgeo2lOzMejJWpEG16CN55CoEAymFd2ktlQZ4Lmv0FlnBJ48H27Cj0TKR1yRbBxpGjgch9x0421C5qYGeXNB9jgczEKh28oYdt8ljXejoDsV7/oKYNaAFfIlICsC/Zz97ZpPRrKiRTSTkTLmIXjnWtVdwGRfCvJrYjJpoe3ZOaubulqCQs6Ug501b6JTzdZujKAl68tL5stZD7hH9XGQSpquZWGeSUGOrI5jU8dULbxCLpkGtiOEmxGb93OLYdHQ8JJg6zHeExVtPSqANHoKX3NpFc33fqOX1vOQVeg8Ei4ymxntIMOwQWs8xnbuKXmnNx6GTBSqHiD7X/a8oeEkMSw1+bCODvwuXlNBcdxHlf3/wkvHb9lhnh231BD1+Ufrnij65Apko8A=="
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,10 +94,28 @@ const countryOfPending = (
|
|||||||
).trim()
|
).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isChaturbateJob = (
|
||||||
|
job: RecordJob,
|
||||||
|
modelsByKey?: Record<string, { imageUrl?: string }>
|
||||||
|
): boolean => {
|
||||||
|
const anyJ = job as any
|
||||||
|
const src = String(anyJ?.sourceUrl ?? anyJ?.SourceURL ?? '').toLowerCase()
|
||||||
|
const chat = String(anyJ?.modelChatRoomUrl ?? '').toLowerCase()
|
||||||
|
const img = onlineImageUrlOfJob(job, modelsByKey).toLowerCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
src.includes('chaturbate.com') ||
|
||||||
|
chat.includes('chaturbate.com') ||
|
||||||
|
img.includes('highwebmedia') ||
|
||||||
|
img.includes('chaturbate')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const previewInitialSrcOfJob = (
|
const previewInitialSrcOfJob = (
|
||||||
job: RecordJob,
|
job: RecordJob,
|
||||||
modelsByKey?: Record<string, { imageUrl?: string }>
|
modelsByKey?: Record<string, { imageUrl?: string }>
|
||||||
): string => {
|
): string => {
|
||||||
|
if (!isChaturbateJob(job, modelsByKey)) return ''
|
||||||
return onlineImageUrlOfJob(job, modelsByKey)
|
return onlineImageUrlOfJob(job, modelsByKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,9 +192,6 @@ const onlineImageUrlOfJob = (
|
|||||||
const direct =
|
const direct =
|
||||||
String(
|
String(
|
||||||
anyJ?.modelImageUrl ??
|
anyJ?.modelImageUrl ??
|
||||||
anyJ?.avatar ??
|
|
||||||
anyJ?.previewImageUrl ??
|
|
||||||
anyJ?.imageUrl ??
|
|
||||||
anyJ?.model?.imageUrl ??
|
anyJ?.model?.imageUrl ??
|
||||||
''
|
''
|
||||||
).trim()
|
).trim()
|
||||||
|
|||||||
@ -130,10 +130,28 @@ const modelKeyFromJob = (job: RecordJob): string => {
|
|||||||
return String(modelNameFromOutput(j.output || '')).trim().toLowerCase()
|
return String(modelNameFromOutput(j.output || '')).trim().toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isChaturbateJob = (
|
||||||
|
job: RecordJob,
|
||||||
|
modelsByKey?: Record<string, { imageUrl?: string }>
|
||||||
|
): boolean => {
|
||||||
|
const anyJ = job as any
|
||||||
|
const src = String(anyJ?.sourceUrl ?? anyJ?.SourceURL ?? '').toLowerCase()
|
||||||
|
const chat = String(anyJ?.modelChatRoomUrl ?? '').toLowerCase()
|
||||||
|
const img = onlineImageUrlOfJob(job, modelsByKey).toLowerCase()
|
||||||
|
|
||||||
|
return (
|
||||||
|
src.includes('chaturbate.com') ||
|
||||||
|
chat.includes('chaturbate.com') ||
|
||||||
|
img.includes('highwebmedia') ||
|
||||||
|
img.includes('chaturbate')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const previewInitialSrcOfJob = (
|
const previewInitialSrcOfJob = (
|
||||||
job: RecordJob,
|
job: RecordJob,
|
||||||
modelsByKey?: Record<string, { imageUrl?: string }>
|
modelsByKey?: Record<string, { imageUrl?: string }>
|
||||||
): string => {
|
): string => {
|
||||||
|
if (!isChaturbateJob(job, modelsByKey)) return ''
|
||||||
return onlineImageUrlOfJob(job, modelsByKey)
|
return onlineImageUrlOfJob(job, modelsByKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,9 +164,6 @@ const onlineImageUrlOfJob = (
|
|||||||
const direct =
|
const direct =
|
||||||
String(
|
String(
|
||||||
anyJ?.modelImageUrl ??
|
anyJ?.modelImageUrl ??
|
||||||
anyJ?.avatar ??
|
|
||||||
anyJ?.previewImageUrl ??
|
|
||||||
anyJ?.imageUrl ??
|
|
||||||
anyJ?.model?.imageUrl ??
|
anyJ?.model?.imageUrl ??
|
||||||
''
|
''
|
||||||
).trim()
|
).trim()
|
||||||
|
|||||||
@ -197,11 +197,6 @@ export type RecordJob = {
|
|||||||
exitCode?: number
|
exitCode?: number
|
||||||
error?: string
|
error?: string
|
||||||
logTail?: string
|
logTail?: string
|
||||||
|
|
||||||
modelImageUrl?: string
|
|
||||||
avatar?: string
|
|
||||||
previewImageUrl?: string
|
|
||||||
imageUrl?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParsedModel = {
|
export type ParsedModel = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user