diff --git a/catalog/book/tests.py b/catalog/book/tests.py
index 667bf8d3..62bfb3d0 100644
--- a/catalog/book/tests.py
+++ b/catalog/book/tests.py
@@ -363,6 +363,37 @@ class DoubanBookTestCase(TestCase):
self.assertEqual(editions[1].display_title, "黄金时代")
+class AO3TestCase(TestCase):
+ databases = "__all__"
+
+ def test_parse(self):
+ t_type = IdType.AO3
+ t_id = "2080878"
+ t_url = "https://archiveofourown.org/works/2080878"
+ t_url2 = "https://archiveofourown.org/works/2080878?test"
+ p1 = SiteManager.get_site_by_url(t_url)
+ p2 = SiteManager.get_site_by_url(t_url2)
+ self.assertEqual(p1.url, t_url)
+ self.assertEqual(p1.ID_TYPE, t_type)
+ self.assertEqual(p1.id_value, t_id)
+ self.assertEqual(p2.url, t_url)
+ self.assertEqual(p2.ID_TYPE, t_type)
+ self.assertEqual(p2.id_value, t_id)
+
+ @use_local_response
+ def test_scrape(self):
+ t_url = "https://archiveofourown.org/works/2080878"
+ site = SiteManager.get_site_by_url(t_url)
+ self.assertEqual(site.ready, False)
+ site.get_resource_ready()
+ self.assertEqual(site.ready, True)
+ self.assertEqual(site.resource.site_name, SiteName.AO3)
+ self.assertEqual(site.resource.id_type, IdType.AO3)
+ self.assertEqual(site.resource.id_value, "2080878")
+ self.assertEqual(site.resource.item.display_title, "I Am Groot")
+ self.assertEqual(site.resource.item.author[0], "sherlocksmyth")
+
+
class QidianTestCase(TestCase):
databases = "__all__"
diff --git a/catalog/common/models.py b/catalog/common/models.py
index 2c290460..e020f3f6 100644
--- a/catalog/common/models.py
+++ b/catalog/common/models.py
@@ -52,6 +52,7 @@ class SiteName(models.TextChoices):
Fediverse = "fedi", _("Fediverse") # type:ignore[reportCallIssue]
Qidian = "qidian", _("Qidian") # type:ignore[reportCallIssue]
Ypshuo = "ypshuo", _("Ypshuo") # type:ignore[reportCallIssue]
+ AO3 = "ao3", _("Archive of Our Own") # type:ignore[reportCallIssue]
class IdType(models.TextChoices):
@@ -115,6 +116,7 @@ class IdType(models.TextChoices):
Fediverse = "fedi", _("Fediverse") # type:ignore[reportCallIssue]
Qidian = "qidian", _("Qidian") # type:ignore[reportCallIssue]
Ypshuo = "ypshuo", _("Ypshuo") # type:ignore[reportCallIssue]
+ AO3 = "ao3", _("Archive of Our Own") # type:ignore[reportCallIssue]
IdealIdTypes = [
diff --git a/catalog/models.py b/catalog/models.py
index 8ebca194..a32bba28 100644
--- a/catalog/models.py
+++ b/catalog/models.py
@@ -38,7 +38,7 @@ from .tv.models import (
TVShowSchema,
)
-from .search.models import Indexer # isort:skip
+from .search.models import Indexer, ExternalSearchResultItem # isort:skip
# class Exhibition(Item):
diff --git a/catalog/sites/__init__.py b/catalog/sites/__init__.py
index 1e84cf65..38904089 100644
--- a/catalog/sites/__init__.py
+++ b/catalog/sites/__init__.py
@@ -1,4 +1,5 @@
from ..common.sites import SiteManager
+from .ao3 import ArchiveOfOurOwn
from .apple_music import AppleMusic
from .bandcamp import Bandcamp
from .bangumi import Bangumi
diff --git a/catalog/sites/ao3.py b/catalog/sites/ao3.py
new file mode 100644
index 00000000..1e79c39c
--- /dev/null
+++ b/catalog/sites/ao3.py
@@ -0,0 +1,64 @@
+import logging
+
+from catalog.book.models import *
+from catalog.common import *
+
+
+@SiteManager.register
+class ArchiveOfOurOwn(AbstractSite):
+ SITE_NAME = SiteName.AO3
+ ID_TYPE = IdType.AO3
+ URL_PATTERNS = [
+ r"\w+://archiveofourown\.org/works/(\d+)",
+ ]
+ WIKI_PROPERTY_ID = "?"
+ DEFAULT_MODEL = Edition
+
+ @classmethod
+ def id_to_url(cls, id_value):
+ return "https://archiveofourown.org/works/" + id_value
+
+ def scrape(self):
+ if not self.url:
+ raise ParseError(self, "url")
+ content = BasicDownloader(self.url + "?view_adult=true").download().html()
+
+ title = content.xpath("string(//h2[@class='title heading'])")
+ if not title:
+ raise ParseError(self, "title")
+ authors = content.xpath("//h3[@class='byline heading']/a/text()")
+ summary = content.xpath(
+ "string(//div[@class='summary module']//blockquote[@class='userstuff'])"
+ )
+ language = [
+ s.strip()
+ for s in content.xpath("//dd[@class='language']/text()") # type:ignore
+ ]
+
+ published = content.xpath("string(//dd[@class='published']/text())")
+ if published:
+ pub_date = published.split("-") # type:ignore
+ pub_year = int(pub_date[0])
+ pub_month = int(pub_date[1])
+ else:
+ pub_year = None
+ pub_month = None
+ data = {
+ "localized_title": [{"lang": "en", "text": title.strip()}], # type:ignore
+ "localized_description": (
+ [
+ {"lang": "en", "text": summary.strip()} # type:ignore
+ ]
+ if summary
+ else []
+ ),
+ "author": authors,
+ "language": language,
+ "pub_year": pub_year,
+ "pub_month": pub_month,
+ "format": Edition.BookFormat.WEB,
+ # "words": words,
+ }
+
+ pd = ResourceContent(metadata=data)
+ return pd
diff --git a/common/static/scss/_sitelabel.scss b/common/static/scss/_sitelabel.scss
index c946e353..2081a9c6 100644
--- a/common/static/scss/_sitelabel.scss
+++ b/common/static/scss/_sitelabel.scss
@@ -18,6 +18,12 @@
white-space: nowrap;
}
+ .ao3 {
+ border: none;
+ color: white;
+ background-color: #900;
+ }
+
.qidian, .ypshuo {
border: none;
color: white;
diff --git a/test_data/https___archiveofourown_org_works_2080878_view_adult_true b/test_data/https___archiveofourown_org_works_2080878_view_adult_true
new file mode 100644
index 00000000..7cca5d4d
--- /dev/null
+++ b/test_data/https___archiveofourown_org_works_2080878_view_adult_true
@@ -0,0 +1,600 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I Am Groot - sherlocksmyth - Multifandom [Archive of Our Own]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
While we've done our best to make the core functionality of this site accessible without JavaScript, it will work better with it enabled. Please consider turning it on!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Actions
+
+
+
+
+
Work Header
+
+
+
+
+
+
+ Rating:
+
+
+
+
+
+
+
+ Archive Warning :
+
+
+
+
+
+
+
+ Fandoms:
+
+
+
+
+
+
+
+ Character:
+
+
+
+
+
+
+
+ Additional Tags:
+
+
+
+
+
+
+
+ Language:
+
+
+ English
+
+
+
+
+ Stats:
+
+
+
+ Published: 2014-08-04 Words: 1,308 Chapters: 1/1 Kudos: 156,230 Bookmarks: 6,177 Hits: 1,167,642
+
+
+
+
+
+
+
+
+
+
+ I Am Groot
+
+
+
+
+
+
Summary:
+
+ EXTREMELY NSFW fic told from the perspective of Groot.
EDIT 04/05/2021: Wow. I just found out this is the top fic on AO3. I’m losing my mind. Thank you guys for the kudos & views! If you want you can follow me on TikTok (@supercolm), Twitter (@super_colm) and Twitch! twitch.tv/ColmTheeGamer
+
+
+
+
+
Notes:
+
+
+
+
+
+
+ Had to take a break halfway through writing this fic because the raw emotion overpowered me.
+
+
+
+
+
+
+
+
+
+
+
+
+
Work Text:
+
ACTUAL CONTENT REMOVED
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Actions
+
+
+ ↑ Top
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Kudos
+
+
+ Chelik , BobTheLobster , Lowe_is_cool , amajortragedy , ughhdotpng , Icantbelievewerenotalldeadyet , Ayelen , joeidkman , Lycone , Scarlettdragon27 , Jassiemil , MarLeb , Lizepubhorder , Hedwig649 , FloweringCat , XerexisSar , bernadineisreborn , candlecrow , Amuleto , sanctimonia , WordsmithDee , Sludgelord , irishbeings , Mintze , FulGurkan , Athyna , JenCollins , Yayah414 , onacloudedmistynight , Angels_dust , Didianita , demonicfaerie2009 , kradihsoy , Pomelo_sacredwater , pickledRatchet , Alisexoxo , NopeZone , BlueberryMoons , LadyJay15 , Clxarke , Mad_mags_patuti , wolfxe , Depressed_Ghost5 , Gavilan , beebo_beebo , beyoursledgehammer , Mojioji , sucksfierro , Lynnie_7 , archiveofadreamer , and 77693 more users
+ as well as
+ 78487 guests
+ left kudos on this work!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+