Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Archive of our own Provider #173

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# QuickNovel
Adfree FOSS Android app for downloading novels. It also functions as an Epub reader.
Ad-free FOSS Android app for downloading novels. It also functions as an Epub reader.

**Discord:** https://discord.gg/5Hus6fM

Expand Down Expand Up @@ -45,7 +45,7 @@ Adfree FOSS Android app for downloading novels. It also functions as an Epub rea

- https://readnovelfull.com

**Screenshoots:**
**Screenshots:**

<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/result.jpg" height="400"/><img src="./.github/reader.jpg" height="400"/>

Expand All @@ -59,5 +59,5 @@ The app is purely for educational and personal use.

QuickNovel does not host any content on the app, and has no control over what media is put up or taken down. QuickNovel functions like any other search engine, such as Google. QuickNovel does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface.

It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use QuickNovel at your own risk.
It merely scrapes 3rd-party websites that are publicly accessible via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use QuickNovel at your own risk.

1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dependencies {
implementation "io.noties.markwon:core:4.6.2"
implementation "io.noties.markwon:image-glide:4.6.2"
implementation "io.noties.markwon:html:4.6.2"
implementation "com.github.franmontiel:PersistentCookieJar:v1.0.1"

implementation "com.facebook.shimmer:shimmer:0.5.0"
}
3 changes: 2 additions & 1 deletion app/src/main/java/com/lagradost/quicknovel/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.WindowManager
import android.webkit.CookieManager
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
Expand Down Expand Up @@ -74,7 +75,7 @@ class MainActivity : AppCompatActivity() {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!

private var _mainActivity: WeakReference<MainActivity>? = null
private var mainActivity
var mainActivity
get() = _mainActivity?.get()
private set(value) {
_mainActivity = WeakReference(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package com.lagradost.quicknovel.providers

import androidx.preference.PreferenceManager
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.lagradost.nicehttp.Requests
import com.lagradost.quicknovel.*
import com.lagradost.quicknovel.BaseApplication.Companion.context
import com.lagradost.quicknovel.mvvm.debugAssert
import com.lagradost.quicknovel.mvvm.debugException
import com.lagradost.quicknovel.mvvm.debugWarning
import okhttp3.FormBody
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import kotlin.collections.ArrayList

class ArchiveOfOurOwnProvider : MainAPI() {
private val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context))
private val app = Requests(MainActivity.app.baseClient.newBuilder().cookieJar(cookieJar).build())
override val name = "Archive of Our Own"
override val mainUrl = "https://archiveofourown.org"

override val hasMainPage = true

override val rateLimitTime: Long = 50

override val iconId = R.drawable.ic_archive_of_our_own

override val iconBackgroundId = R.color.archiveOfOurOwnColor

override val orderBys = listOf(
Pair("Latest", "latest"),
)


override suspend fun loadMainPage(
page: Int,
mainCategory: String?,
orderBy: String?,
tag: String?,
PrecociouslyDigital marked this conversation as resolved.
Show resolved Hide resolved
): HeadMainPageResponse {
tryLogIn()
val url =
"$mainUrl/works"
if (page > 1) return HeadMainPageResponse(
url,
ArrayList()
) // Latest ONLY HAS 1 PAGE

val response = app.get(url)

val document = response.document
val works = document.select("li.work")
if (works.size <= 0) return HeadMainPageResponse(url, ArrayList())

val returnValue: ArrayList<SearchResponse> = ArrayList()
for (h in works) {
val workLink = h?.selectFirst("div.header.module > h4.heading > a")
if (workLink == null){
debugWarning { "Ao3 work has no actual work?" }
continue
}
val name = workLink.text()
val url = workLink.attr("href")

returnValue.add(
SearchResponse(
name,
fixUrl(url),
apiName = this.name
)
)
}
return HeadMainPageResponse(url, returnValue)
}


override suspend fun search(query: String): List<SearchResponse> {
tryLogIn()
val response = app.get("$mainUrl/works/search?work_search[query]=$query")

val document = response.document
val works = document.select("li.work")
if (works.size <= 0) return ArrayList()
val returnValue: ArrayList<SearchResponse> = ArrayList()
for (h in works) {
val workLink = h?.selectFirst("div.header.module > h4.heading > a")
if (workLink == null){
debugWarning { "Ao3 work has no actual work?" }
continue
}
val name = workLink.text()
val url = workLink.attr("href")

val authorLink = h.selectFirst("div.header.module > h4.heading > a[rel=\"author\"]")
if (authorLink == null){
debugWarning { "Ao3 work has no actual author?" }
continue
}
val author = authorLink.attr("href")

returnValue.add(
SearchResponse(
name,
fixUrl(url),
fixUrlNull(author),
apiName = this.name
)
)
}
return returnValue
}

override suspend fun load(url: String): LoadResponse? {
tryLogIn()
val response = app.get("$url/?view_adult=true")

val document = response.document

val name = document.selectFirst("h2.title.heading")?.text().toString()
val author = document.selectFirst("h3.byline.heading > a[rel=\"author\"]")
val peopleVoted = document.selectFirst("dd.kudos")?.text()?.replace(",","")?.toInt()
val views = document.selectFirst("dd.hits")?.text()?.replace(",","")?.toInt()
val synopsis = document.selectFirst("div.summary.module > blockquote.userstuff")
?.children()?.joinToString("\n", transform = Element::text)

val tags = document.select("a.tag").map(org.jsoup.nodes.Element::text)

val chaptersResponse = app.get("$url/navigate?view_adult=true")
val chapterDocument = Jsoup.parse(chaptersResponse.text)

val chapters = chapterDocument.selectFirst("ol.chapter.index.group[role=\"navigation\"]")

val data = chapters?.children()?.map {
val link = it.child(0)
val date = it.child(1)
ChapterData(
name = link.text().toString(),
url = fixUrl(link.attr("href").toString()),
dateOfRelease = date.text()
)
}


return StreamResponse(
url,
name,
data ?: ArrayList(),
author = author?.text(),
peopleVoted.toString(),
views,
synopsis = synopsis,
tags = tags
)
}

override suspend fun loadHtml(url: String): String? {
tryLogIn()
val response = app.get(url)
val document = Jsoup.parse(response.text)
return document.selectFirst("div.chapter")?.html()
}

private var hasLoggedIn = false
private suspend fun tryLogIn() {

if (hasLoggedIn) return

//Don't try to log in if we don't have email and password
val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context!!.applicationContext)

val ao3Email = preferenceManager.getString(context?.getString(R.string.ao3_email_key),"")!!
val ao3Password = preferenceManager.getString(context?.getString(R.string.ao3_password_key),"")!!

if (ao3Email == "" || ao3Password == ""){
return
}

val response = app.get("$mainUrl/works/new", allowRedirects = false)
if(response.code == 200){
hasLoggedIn = true
return
}
if(response.code != 302) {
debugException { "AO3 isn't redirecting us to login page for some reason. If issue persists please report to extension creator." }
return
}

val loginPageResponse = app.get("$mainUrl/users/login")

val authenticityToken =
loginPageResponse.document.selectFirst("form.new_user#new_user")
?.selectFirst("input[type=hidden][name=authenticity_token]")
?.attr("value")!!


val loginResponse = app.post("$mainUrl/users/login", requestBody = FormBody.Builder()
.add("user[login]", ao3Email)
.add("user[password]", ao3Password)
.add("user[remember_me]", "1")
.add("commit", "Log in")
.add("authenticity_token", authenticityToken)
.build()
)

/*data= mapOf(
Pair("user[login]", ao3Email),
Pair("user[password]", ao3Password),
Pair("user[remember_me]", "1"),
Pair("commit", "Log in"),
Pair("authenticity_token", authenticityToken),
)*/

if(loginResponse.okhttpResponse.priorResponse == null){
if(loginResponse.text.contains("The password or user name you entered doesn't match our records.")){
if (!preferenceManager.edit().putString(context?.getString(R.string.ao3_password_key), "").commit()){
debugException { "Something went wrong clearing your password!" }
}
debugWarning { "Username or Password incorrect! Password's been cleared" }
}else{
debugException { "Something went wrong logging you in." }
}

}else{
debugAssert( {response.url.startsWith("$mainUrl/users") && response.url != "$mainUrl/users/login"},
{"Expected to be sent to $mainUrl/users/yourusername was instead sent to ${response.url}"})
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.lagradost.quicknovel.ui.settings


import android.os.Bundle
import androidx.preference.*
import com.lagradost.quicknovel.R

class ProviderSettingsFragment : PreferenceFragmentCompat() {

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.provider_settings, rootKey)
}
}
1 change: 1 addition & 0 deletions app/src/main/java/com/lagradost/quicknovel/util/Apis.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Apis {
IndoWebNovelProvider(),
SakuraNovelProvider(),
WattpadProvider(),
ArchiveOfOurOwnProvider(),
)

fun getApiFromName(name: String): APIRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.quicknovel.R

object SingleSelectionHelper {
fun Context.showDialog(
private fun Context.showDialog(
dialog: Dialog,
items: List<String>,
selectedIndex: List<Int>,
Expand Down Expand Up @@ -110,6 +110,7 @@ object SingleSelectionHelper {
)
}


fun Context.showDialog(
items: List<String>,
selectedIndex: Int,
Expand Down Expand Up @@ -160,4 +161,5 @@ object SingleSelectionHelper {
dismissCallback
)
}

}
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/ic_archive_of_our_own.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="415dp"
android:viewportWidth="600"
android:viewportHeight="415">

<path
android:fillColor="#900"
android:pathData="M370.16 10.811c-54.019 0-97.816 42.648-97.816 95.266 0 52.608 43.797 95.244 97.816 95.244s97.816-42.636 97.816-95.244c0-52.618-43.797-95.266-97.816-95.266zm-355.28 4.582c-2.9356 0.1201-4.0242 0.92857-4.8105 1.7188 0 0-1.2608 5.0412 7.9844 6.7148 9.2351 1.6837 62.165 17.231 134.85 74.355 72.671 57.135 112.16 107.96 140.3 137.79 0 0-85.697 30.246-186.09 96.615 0 0 35.286-42.426 49.148-147.87 2.0265-15.415 5.7679-34.651 7.5625-52.93 0 0 2.0969-14.699-3.3574-14.699-5.4644 0-14.287 73.517-29.41 119.3-8.237 24.933-18.48 55.875-47.889 96.203-18.188 24.953-36.366 43.665-32.344 52.094 8.8217 18.48 27.303 0.41407 27.303 0.41407s117.63-77.712 241.13-115.94c0 0 30.074 32.897 35.428 39.229 16.121 19.055 27.686 40.117 41.438 39.754 11.171-0.30246 15.124-6.3012 21.012-38.654 5.8778-32.343 18.902-64.273 18.902-64.273s74.033-3.5589 77.713 20.164c2.46 15.849-8.549 24.016-17.32 26.93-4.2748 1.4316-17.432 5.2615-22.896 6.8242-10.223 2.9137-12.834 18.44 4.0938 25.9 27.625 12.169 55.945 18.229 53.344 39.492-2.5205 20.577-36.588 30.852-60.492 25.619-44.109-9.6586-46.226-30.206-53.768-27.303-5.4644 2.097 10.082 18.903 18.48 24.367 8.4084 5.4544 70.161 36.549 102.5-2.1055 36.557-43.675-33.604-70.988-33.604-70.988s43.262-19.328 35.287-55.875c-7.985-36.547-55.039-42.011-89.902-44.945 0 0 15.123-38.23 49.986-78.135 18.127-20.749 46.75-44.3 63.859-57.135 16.797-12.602 29.743-14.851 26.043-22.262-4.2042-8.3983-72.258 40.752-85.273 52.508-13.026 11.766-50.834 48.726-82.34 105.02 0 0-50.824 2.5208-89.479 11.756 0 0-55.028-59.644-119.3-109.64-64.273-49.987-124.34-80.234-160.89-91.576-22.287-6.9188-32.309-8.6338-37.201-8.4336zm355.28 20.654c39.713 0 71.904 31.345 71.904 70.029 0 38.664-32.191 70.02-71.904 70.02s-71.904-31.355-71.904-70.02c0-38.685 32.191-70.029 71.904-70.029zm34.158 212.75s-10.688 21.141-15.709 42.031c-8.8419-11.191-29.621-33.824-29.621-33.824s15.77-4.0532 45.33-8.207z" />
</vector>
1 change: 1 addition & 0 deletions app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<color name="novelPassionColor">#FFF</color>
<color name="wuxiaWorldOnlineColor">#24242B</color>
<color name="archiveOfOurOwnColor">@color/primaryGrayBackground</color>
<color name="royalRoadColor">@color/primaryGrayBackground</color>
<color name="wuxiaWorldSiteColor">#1670bf</color>
<color name="readLightNovelColor">#FFF</color>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

<string name="provider_lang_key" translatable="false">provider_lang_key</string>
<string name="restore_key" translatable="false">restore_key</string>
<string name="ao3_email_key" translatable="false">ao3_email</string>
<string name="ao3_password_key" translatable="false">ao3_password</string>

<string name="restore_settings">Restore data from backup</string>

<string name="search_hint">Search Novels</string>
Expand Down Expand Up @@ -126,6 +129,7 @@
<string name="home_expanded_hide">Hide</string>
<string name="reload_chapter">Reload Chapter</string>
<string name="provider_lang_settings">Provider Languages</string>
<string name="provider_settings">Provider Settings</string>
<string name="sort_apply">Apply</string>
<string name="sort_cancel">Cancel</string>
<string name="keep_screen_active">Keep screen active</string>
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/res/xml/provider_settings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

<PreferenceCategory android:title="Archive Of Our Own">
<EditTextPreference
android:defaultValue=""
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="Email"
android:key="@string/ao3_email_key"
android:dialogLayout="@xml/text_edit_dialog"/>
<EditTextPreference
android:defaultValue=""
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="Password"
android:key="@string/ao3_password_key"
android:dialogLayout="@xml/text_edit_dialog" />
</PreferenceCategory>
</PreferenceScreen>
Loading