Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions quasarr/downloads/sources/dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,7 @@ def get_download_links(self, shared_state, url, mirrors, title, password):
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")

article = soup.find("article")
if not article:
info(f"Could not find article block for {title}")
mark_hostname_issue(
Source.initials, "download", "Could not find article block"
)
return None

body = article.find("div", class_="full-story-body")
body = soup.find("div", class_="full-story-body")
if not body:
info(f"Could not find download section for {title}")
mark_hostname_issue(
Expand Down
2 changes: 1 addition & 1 deletion quasarr/providers/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re
import sys

__version__ = "4.7.8"
__version__ = "4.7.9"


def get_version():
Expand Down
39 changes: 22 additions & 17 deletions quasarr/search/sources/dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def feed(

for article in feed.find_all("article"):
try:
link_tag = article.select_one("h1.dt-flexible-title a")
link_tag = article.select_one(".dt-flexible-title a")
if not link_tag:
# ad/promo articles lack a release title link; skip quietly
debug(f"Link tag not found in article: {article}")
Expand Down Expand Up @@ -238,7 +238,7 @@ def search(
for article in page.find_all("article"):
try:
# search-result articles use h1.font-weight-bold, while
# the feed listing uses h1.dt-flexible-title
# the feed listing uses .dt-flexible-title
link_tag = article.select_one("h1.font-weight-bold a")
if not link_tag:
debug(f"No title link in search-article: {article}")
Expand Down Expand Up @@ -340,24 +340,29 @@ def _extract_size(text):


def _parse_published_datetime(article):
mon = article.select_one("small.dt-fs-11").text.strip()
day = article.find("h4", class_="m-0").text.strip()
year = article.find("h6", class_="m-0").text.strip()
month_num = datetime.datetime.strptime(mon, "%b").month

time_icon = article.select_one("i.bi-clock")
if time_icon:
# its parent <span> contains e.g. "19:12"
raw = time_icon.parent.get_text(strip=True)
m = re.search(r"(\d{1,2}:\d{2})", raw)
if m:
hh, mm = map(int, m.group(1).split(":"))
else:
hh, mm = 0, 0
month_tag = article.select_one("small.dt-fs-11")
if not month_tag:
# feed listings show "29.10.2026 07:47" instead of the calendar block
m = re.search(
r"\b(\d{2})\.(\d{2})\.(\d{4})\s+(\d{1,2}):(\d{2})\b",
article.get_text(" "),
)
day, month_num, year, hh, mm = map(int, m.groups())
else:
day = int(article.find("h4", class_="m-0").text.strip())
year = int(article.find("h6", class_="m-0").text.strip())
month_num = datetime.datetime.strptime(month_tag.text.strip(), "%b").month

time_icon = article.select_one("i.bi-clock")
hh, mm = 0, 0
if time_icon:
# its parent <span> contains e.g. "19:12"
raw = time_icon.parent.get_text(strip=True)
m = re.search(r"(\d{1,2}:\d{2})", raw)
if m:
hh, mm = map(int, m.group(1).split(":"))

# this timezone is fixed to CET+1 and might be wrong
cet = timezone(timedelta(hours=1))
dt = datetime.datetime(int(year), month_num, int(day), hh, mm, tzinfo=cet)
dt = datetime.datetime(year, month_num, day, hh, mm, tzinfo=cet)
return dt.isoformat()
13 changes: 13 additions & 0 deletions quasarr/search/sources/he.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ def search(
search_type,
str(e) if "e" in dir() else "Error occurred",
)
# the relative "Posted N days ago" drifts with every search
published = _parse_published_meta(soup) or published
release_imdb_id = None
try:
imdb_link = soup.find(
Expand Down Expand Up @@ -245,6 +247,17 @@ def search(
return releases


def _parse_published_meta(soup):
meta = soup.find("meta", attrs={"itemprop": "datePublished"})
try:
published = datetime.fromisoformat(meta["content"])
return published.astimezone(timezone.utc).strftime(
"%a, %d %b %Y %H:%M:%S +0000"
)
except Exception:
return ""


def _parse_posted_ago(txt):
try:
m = re.search(
Expand Down
103 changes: 103 additions & 0 deletions tests/test_dt_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-

import unittest
from unittest.mock import MagicMock, patch

from bs4 import BeautifulSoup

from quasarr.downloads.sources import dt
from quasarr.search.sources.dt import _parse_published_datetime


def _response(html):
response = MagicMock()
response.text = html
return response


class DtDownloadLinksTests(unittest.TestCase):
def _get_links(self, html):
shared_state = MagicMock()
shared_state.values = {"user_agent": "test-agent"}
session = MagicMock()
session.get.return_value = _response(html)
with (
patch.object(dt.requests, "Session", return_value=session),
patch.object(dt, "clear_hostname_issue"),
patch.object(dt, "mark_hostname_issue"),
):
return dt.Source().get_download_links(
shared_state,
"https://source.invalid/post.html",
None,
"Example.Show.S01E01.1080p.WEB.h264-GROUP",
"source.invalid",
)

def test_finds_story_body_outside_article(self):
result = self._get_links(
"""
<article><div class="h1 dt-flexible-title">Example</div></article>
<section><div class="card-body"><div class="full-story-body">
<a href="https://rapidgator.invalid/file/one">one</a>
<a href="https://www.imdb.com/title/tt0000001/">imdb</a>
</div></div></section>
"""
)

self.assertEqual(
{"links": [["https://rapidgator.invalid/file/one", "rapidgator"]]},
result,
)

def test_finds_story_body_inside_article(self):
result = self._get_links(
"""
<article><div class="full-story-body">
<a href="https://ddownload.invalid/file/two">two</a>
</div></article>
"""
)

self.assertEqual(
{"links": [["https://ddownload.invalid/file/two", "ddownload"]]},
result,
)


class DtPublishedDateTests(unittest.TestCase):
def _parse(self, html):
article = BeautifulSoup(html, "html.parser").find("article")
return _parse_published_datetime(article)

def test_parses_feed_listing_date(self):
self.assertEqual(
"2026-10-29T07:47:00+01:00",
self._parse(
"""
<article>
<span>Today, 07:47</span>
<span>29.10.2026 07:47</span>
</article>
"""
),
)

def test_parses_search_calendar_block(self):
self.assertEqual(
"2026-10-24T14:39:00+01:00",
self._parse(
"""
<article>
<small class="dt-fs-11">Oct</small>
<h4 class="m-0">24</h4>
<h6 class="m-0">2026</h6>
<span><i class="bi bi-clock"></i>14:39</span>
</article>
"""
),
)


if __name__ == "__main__":
unittest.main()
59 changes: 59 additions & 0 deletions tests/test_he_published_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-

import time
import unittest
from unittest.mock import MagicMock, patch

from quasarr.search.sources import he

POST_URL = "https://source.invalid/synthetic-show-s01e01/"

SEARCH_HTML = f"""
<div class="item"><div class="data">
<h5><a href="{POST_URL}">Synthetic.Show.S01E01.1080p.WEB.h264-GRP – 1.2 GB</a></h5>
<span class="post-time">Posted 2 days ago</span>
</div></div>
"""


def _response(html):
response = MagicMock()
response.content = html.encode("utf-8")
return response


class HePublishedDateTests(unittest.TestCase):
def _search(self, post_html):
shared_state = MagicMock()
shared_state.values = {
"user_agent": "test-agent",
"config": lambda section: {"he": "source.invalid"},
}

def get(url, **kwargs):
return _response(post_html if url == POST_URL else SEARCH_HTML)

with (
patch.object(he.requests, "get", side_effect=get),
patch.object(he, "generate_download_link", return_value="link"),
patch.object(he, "clear_hostname_issue"),
patch.object(he, "mark_hostname_issue"),
):
releases = he.Source().search(shared_state, time.time(), 5000)
return [release["details"]["date"] for release in releases]

def test_uses_absolute_published_date_from_post(self):
self.assertEqual(
["Thu, 23 Oct 2031 10:11:40 +0000"],
self._search(
'<meta itemprop="datePublished" content="2031-10-23T13:41:40+03:30"/>'
),
)

def test_falls_back_to_relative_post_time(self):
with patch.object(he, "_parse_posted_ago", return_value="relative"):
self.assertEqual(["relative"], self._search("<div></div>"))


if __name__ == "__main__":
unittest.main()
Loading