Removed old features
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,4 +16,5 @@
|
|||||||
/gh-pages
|
/gh-pages
|
||||||
|
|
||||||
#Private files
|
#Private files
|
||||||
**/google-services.json
|
**/google-services.json
|
||||||
|
**/credentials.json
|
||||||
@@ -29,7 +29,6 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import xyz.quaver.pupil.util.cancelImport
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class BroadcastReciever : BroadcastReceiver() {
|
class BroadcastReciever : BroadcastReceiver() {
|
||||||
@@ -93,9 +92,6 @@ class BroadcastReciever : BroadcastReceiver() {
|
|||||||
|
|
||||||
notificationManager.notify(R.id.notification_id_update, notification)
|
notificationManager.notify(R.id.notification_id_update, notification)
|
||||||
}
|
}
|
||||||
ACTION_CANCEL_IMPORT -> {
|
|
||||||
cancelImport = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,43 +156,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.request_import_old_galleries -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
data?.data?.also { uri ->
|
|
||||||
val takeFlags: Int =
|
|
||||||
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
|
||||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
||||||
|
|
||||||
val file = uri.toFile(this)
|
|
||||||
|
|
||||||
if (file?.canRead() != true)
|
|
||||||
Snackbar.make(
|
|
||||||
settings,
|
|
||||||
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
else
|
|
||||||
importOldGalleries(this, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.request_import_old_galleries_old -> {
|
|
||||||
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
|
|
||||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
|
||||||
|
|
||||||
if (!File(directory).canRead())
|
|
||||||
Snackbar.make(
|
|
||||||
settings,
|
|
||||||
resources.getText(R.string.import_old_galleries_folder_not_readable),
|
|
||||||
Snackbar.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
else {
|
|
||||||
importOldGalleries(this, File(directory))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,31 +198,6 @@ class SettingsFragment :
|
|||||||
|
|
||||||
activity?.startActivityForResult(intent, R.id.request_restore.normalizeID())
|
activity?.startActivityForResult(intent, R.id.request_restore.normalizeID())
|
||||||
}
|
}
|
||||||
"old_import_galleries" -> {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
|
|
||||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
|
||||||
ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), R.id.request_write_permission_and_saf.normalizeID())
|
|
||||||
else {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
||||||
putExtra("android.content.extra.SHOW_ADVANCED", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.startActivityForResult(intent, R.id.request_import_old_galleries.normalizeID())
|
|
||||||
}
|
|
||||||
} else { // Can't use SAF on old Androids!
|
|
||||||
val config = DirectoryChooserConfig.builder()
|
|
||||||
.newDirectoryName("Pupil")
|
|
||||||
.allowNewDirectoryNameModification(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val intent = Intent(requireContext(), DirectoryChooserActivity::class.java).apply {
|
|
||||||
putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
activity?.startActivityForResult(intent, R.id.request_import_old_galleries_old.normalizeID())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"user_id" -> {
|
"user_id" -> {
|
||||||
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||||
ClipData.newPlainText("user_id", sharedPreference.getString("user_id", ""))
|
ClipData.newPlainText("user_id", sharedPreference.getString("user_id", ""))
|
||||||
@@ -380,9 +355,6 @@ class SettingsFragment :
|
|||||||
"restore" -> {
|
"restore" -> {
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"old_import_galleries" -> {
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
|
||||||
}
|
|
||||||
"user_id" -> {
|
"user_id" -> {
|
||||||
summary = sharedPreference.getString("user_id", "")
|
summary = sharedPreference.getString("user_id", "")
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
|
|||||||
@@ -184,149 +184,4 @@ fun checkUpdate(context: Context, force: Boolean = false) {
|
|||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var cancelImport = false
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun importOldGalleries(context: Context, folder: File) = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
val client = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(0, TimeUnit.SECONDS)
|
|
||||||
.proxy(proxy)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val cancelIntent = Intent(context, BroadcastReciever::class.java).apply {
|
|
||||||
action = BroadcastReciever.ACTION_CANCEL_IMPORT
|
|
||||||
putExtra(BroadcastReciever.EXTRA_IMPORT_NOTIFICATION_ID, 0)
|
|
||||||
}
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, 0)
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
val notificationBuilder = NotificationCompat.Builder(context, "import").apply {
|
|
||||||
setContentTitle(context.getText(R.string.import_old_galleries_notification))
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
setSmallIcon(R.drawable.ic_notification)
|
|
||||||
addAction(0, context.getText(android.R.string.cancel), pendingIntent)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(0, notificationBuilder.build())
|
|
||||||
|
|
||||||
if (!folder.isDirectory)
|
|
||||||
return@launch
|
|
||||||
|
|
||||||
val galleryRegex = Regex("""[0-9]+$""")
|
|
||||||
val imageRegex = Regex("""^[0-9]+\..+$""")
|
|
||||||
var size = 0
|
|
||||||
fun setProgress(progress: Int) {
|
|
||||||
notificationBuilder.apply {
|
|
||||||
setContentText(
|
|
||||||
context.getString(
|
|
||||||
R.string.import_old_galleries_notification_text,
|
|
||||||
progress,
|
|
||||||
size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setProgress(size, progress, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.notify(0, notificationBuilder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.listFiles { _, name ->
|
|
||||||
galleryRegex.matches(name)
|
|
||||||
}?.also {
|
|
||||||
size = it.size
|
|
||||||
setProgress(0)
|
|
||||||
}?.forEachIndexed { index, gallery ->
|
|
||||||
if (cancelImport)
|
|
||||||
return@forEachIndexed
|
|
||||||
|
|
||||||
setProgress(index)
|
|
||||||
|
|
||||||
val galleryID = gallery.name.toIntOrNull() ?: return@forEachIndexed
|
|
||||||
|
|
||||||
File(getDownloadDirectory(context), galleryID.toString()).mkdirs()
|
|
||||||
|
|
||||||
val reader = async {
|
|
||||||
kotlin.runCatching {
|
|
||||||
Json.decodeFromString<Reader>(File(gallery, "reader.json").readText())
|
|
||||||
}.getOrElse {
|
|
||||||
getReader(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val galleryBlock = async {
|
|
||||||
kotlin.runCatching {
|
|
||||||
Json.decodeFromString<GalleryBlock>(File(gallery, "galleryBlock.json").readText())
|
|
||||||
}.getOrElse {
|
|
||||||
getGalleryBlock(galleryID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
val thumbnail = async thumbnail@{
|
|
||||||
val galleryBlock = galleryBlock.await()
|
|
||||||
|
|
||||||
Base64.encodeToString(try {
|
|
||||||
File(gallery, "thumbnail.jpg").readBytes()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val url = galleryBlock?.thumbnails?.firstOrNull()
|
|
||||||
|
|
||||||
if (url == null)
|
|
||||||
null
|
|
||||||
else {
|
|
||||||
val request = Request.Builder().url(url).build()
|
|
||||||
|
|
||||||
var done = false
|
|
||||||
var result: ByteArray? = null
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
|
||||||
override fun onFailure(call: Call?, e: IOException?) {
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call?, response: Response?) {
|
|
||||||
result = response?.body()?.use {
|
|
||||||
it.bytes()
|
|
||||||
}
|
|
||||||
done = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!done)
|
|
||||||
yield()
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
} ?: return@thumbnail null, Base64.DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache(context).setCachedMetadata(galleryID,
|
|
||||||
Metadata(
|
|
||||||
thumbnail.await(),
|
|
||||||
galleryBlock.await(),
|
|
||||||
reader.await()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
File(gallery, "images").listFiles { _, name ->
|
|
||||||
imageRegex.matches(name)
|
|
||||||
}?.forEach {
|
|
||||||
if (cancelImport)
|
|
||||||
return@forEach
|
|
||||||
|
|
||||||
@Suppress("NAME_SHADOWING")
|
|
||||||
val index = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
|
|
||||||
|
|
||||||
Cache(context).putImage(galleryID, index, it.extension, it.inputStream())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationBuilder.apply {
|
|
||||||
setContentText(context.getText(R.string.import_old_galleries_notification_done))
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
setOngoing(false)
|
|
||||||
mActions.clear()
|
|
||||||
}
|
|
||||||
notificationManager.notify(0, notificationBuilder.build())
|
|
||||||
|
|
||||||
cancelImport = false
|
|
||||||
}
|
}
|
||||||
@@ -108,10 +108,6 @@
|
|||||||
app:key="restore"
|
app:key="restore"
|
||||||
app:title="@string/settings_restore_title"/>
|
app:title="@string/settings_restore_title"/>
|
||||||
|
|
||||||
<Preference
|
|
||||||
app:key="old_import_galleries"
|
|
||||||
app:title="@string/settings_import_old_galleries"/>
|
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
app:key="user_id"
|
app:key="user_id"
|
||||||
app:title="@string/settings_user_id"/>
|
app:title="@string/settings_user_id"/>
|
||||||
|
|||||||
1
libpupil/.gitignore
vendored
1
libpupil/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
202
libpupil/LICENSE
202
libpupil/LICENSE
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
apply plugin: 'java-library'
|
|
||||||
apply plugin: 'kotlin'
|
|
||||||
apply plugin: 'kotlinx-serialization'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0"
|
|
||||||
implementation 'org.jsoup:jsoup:1.13.1'
|
|
||||||
testImplementation 'junit:junit:4.13'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
compileKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileTestKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver
|
|
||||||
|
|
||||||
|
|
||||||
enum class Code {
|
|
||||||
HITOMI,
|
|
||||||
HIYOBI
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver
|
|
||||||
|
|
||||||
import kotlinx.serialization.UnstableDefault
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.net.Proxy
|
|
||||||
|
|
||||||
var proxy : Proxy = Proxy.NO_PROXY
|
|
||||||
|
|
||||||
@OptIn(UnstableDefault::class)
|
|
||||||
var json = Json {
|
|
||||||
isLenient = true
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
serializeSpecialFloatingPointValues = true
|
|
||||||
useArrayPolymorphism = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun availableInHiyobi(galleryID: Int) : Boolean {
|
|
||||||
return try {
|
|
||||||
xyz.quaver.hiyobi.getReader(galleryID)
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import xyz.quaver.json
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
const val protocol = "https:"
|
|
||||||
|
|
||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
|
||||||
fun getGalleryInfo(galleryID: Int) =
|
|
||||||
json.parse(
|
|
||||||
GalleryInfo.serializer(),
|
|
||||||
URL("$protocol//$domain/galleries/$galleryID.js").openConnection(proxy).getInputStream().use {
|
|
||||||
it.reader().readText()
|
|
||||||
}.replace("var galleryinfo = ", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
//common.js
|
|
||||||
var adapose = false
|
|
||||||
const val domain = "ltn.hitomi.la"
|
|
||||||
const val galleryblockdir = "galleryblock"
|
|
||||||
const val nozomiextension = ".nozomi"
|
|
||||||
|
|
||||||
fun subdomainFromGalleryID(g: Int, numberOfFrontends: Int) : String {
|
|
||||||
if (adapose)
|
|
||||||
return "0"
|
|
||||||
|
|
||||||
val o = g % numberOfFrontends
|
|
||||||
|
|
||||||
return (97+o).toChar().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun subdomainFromURL(url: String, base: String? = null) : String {
|
|
||||||
var retval = "a"
|
|
||||||
|
|
||||||
if (!base.isNullOrBlank())
|
|
||||||
retval = base
|
|
||||||
|
|
||||||
var numberOfFrontends = 3
|
|
||||||
val b = 16
|
|
||||||
|
|
||||||
val r = Regex("""/[0-9a-f]/([0-9a-f]{2})/""")
|
|
||||||
val m = r.find(url) ?: return retval
|
|
||||||
|
|
||||||
var g = m.groupValues[1].toIntOrNull(b)
|
|
||||||
|
|
||||||
if (g != null) {
|
|
||||||
if (g < 0x30)
|
|
||||||
numberOfFrontends = 2
|
|
||||||
if (g < 0x09)
|
|
||||||
g = 1
|
|
||||||
|
|
||||||
retval = subdomainFromGalleryID(g, numberOfFrontends) + retval
|
|
||||||
}
|
|
||||||
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
fun urlFromURL(url: String, base: String? = null) : String {
|
|
||||||
return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun fullPathFromHash(hash: String?) : String? {
|
|
||||||
return when {
|
|
||||||
(hash?.length ?: 0) < 3 -> hash
|
|
||||||
else -> hash!!.replace(Regex("^.*(..)(.)$"), "$2/$1/$hash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
|
|
||||||
fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
|
|
||||||
val ext = ext ?: dir ?: image.name.split('.').last()
|
|
||||||
val dir = dir ?: "images"
|
|
||||||
return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
|
|
||||||
urlFromURL(urlFromHash(galleryID, image, dir, ext), base)
|
|
||||||
|
|
||||||
fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
|
|
||||||
val webp = if (image.hash != null && image.haswebp != 0 && !noWebp)
|
|
||||||
"webp"
|
|
||||||
else
|
|
||||||
null
|
|
||||||
|
|
||||||
return urlFromUrlFromHash(galleryID, image, webp)
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import java.net.URLDecoder
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Gallery(
|
|
||||||
val related: List<Int>,
|
|
||||||
val langList: List<Pair<String, String>>,
|
|
||||||
val cover: String,
|
|
||||||
val title: String,
|
|
||||||
val artists: List<String>,
|
|
||||||
val groups: List<String>,
|
|
||||||
val type: String,
|
|
||||||
val language: String,
|
|
||||||
val series: List<String>,
|
|
||||||
val characters: List<String>,
|
|
||||||
val tags: List<String>,
|
|
||||||
val thumbnails: List<String>
|
|
||||||
)
|
|
||||||
fun getGallery(galleryID: Int) : Gallery {
|
|
||||||
val url = Jsoup.connect("https://hitomi.la/galleries/$galleryID.html").proxy(proxy).get()
|
|
||||||
.select("a").attr("href")
|
|
||||||
|
|
||||||
val doc = Jsoup.connect(url).get()
|
|
||||||
|
|
||||||
val related = Regex("\\d+")
|
|
||||||
.findAll(doc.select("script").first().html())
|
|
||||||
.map {
|
|
||||||
it.value.toInt()
|
|
||||||
}.toList()
|
|
||||||
|
|
||||||
val langList = doc.select("#lang-list a").map {
|
|
||||||
Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val cover = protocol + doc.selectFirst(".cover img").attr("src")
|
|
||||||
val title = doc.selectFirst(".gallery h1 a").text()
|
|
||||||
val artists = doc.select(".gallery h2 a").map { it.text() }
|
|
||||||
val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() }
|
|
||||||
val type = doc.selectFirst(".gallery-info a[href~=^/type/]").text()
|
|
||||||
|
|
||||||
val language = {
|
|
||||||
val href = doc.select(".gallery-info a[href~=^/index-.+-1.html]").attr("href")
|
|
||||||
href.slice(7 until href.indexOf("-1"))
|
|
||||||
}.invoke()
|
|
||||||
|
|
||||||
val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() }
|
|
||||||
val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() }
|
|
||||||
|
|
||||||
val tags = doc.select(".gallery-info a[href~=^/tag/]").map {
|
|
||||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
|
||||||
href.slice(5 until href.indexOf('-'))
|
|
||||||
}
|
|
||||||
|
|
||||||
val thumbnails = getGalleryInfo(galleryID).files.map { galleryInfo ->
|
|
||||||
urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import java.net.URL
|
|
||||||
import java.net.URLDecoder
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.util.*
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
|
|
||||||
//galleryblock.js
|
|
||||||
fun fetchNozomi(area: String? = null, tag: String = "index", language: String = "all", start: Int = -1, count: Int = -1) : Pair<List<Int>, Int> {
|
|
||||||
val url =
|
|
||||||
when(area) {
|
|
||||||
null -> "$protocol//$domain/$tag-$language$nozomiextension"
|
|
||||||
else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
|
|
||||||
}
|
|
||||||
|
|
||||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
|
||||||
requestMethod = "GET"
|
|
||||||
|
|
||||||
if (start != -1 && count != -1) {
|
|
||||||
val startByte = start*4
|
|
||||||
val endByte = (start+count)*4-1
|
|
||||||
|
|
||||||
setRequestProperty("Range", "bytes=$startByte-$endByte")
|
|
||||||
}
|
|
||||||
|
|
||||||
connect()
|
|
||||||
|
|
||||||
val totalItems = getHeaderField("Content-Range")
|
|
||||||
.replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
|
|
||||||
|
|
||||||
val nozomi = ArrayList<Int>()
|
|
||||||
|
|
||||||
val arrayBuffer = ByteBuffer
|
|
||||||
.wrap(inputStream.readBytes())
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
while (arrayBuffer.hasRemaining())
|
|
||||||
nozomi.add(arrayBuffer.int)
|
|
||||||
|
|
||||||
return Pair(nozomi, totalItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GalleryBlock(
|
|
||||||
val code: Code,
|
|
||||||
val id: Int,
|
|
||||||
val galleryUrl: String,
|
|
||||||
val thumbnails: List<String>,
|
|
||||||
val title: String,
|
|
||||||
val artists: List<String>,
|
|
||||||
val series: List<String>,
|
|
||||||
val type: String,
|
|
||||||
val language: String,
|
|
||||||
val relatedTags: List<String>
|
|
||||||
)
|
|
||||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
|
||||||
val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
|
|
||||||
|
|
||||||
val doc = Jsoup.connect(url).proxy(proxy).get()
|
|
||||||
|
|
||||||
val galleryUrl = doc.selectFirst(".lillie").attr("href")
|
|
||||||
|
|
||||||
val thumbnails = doc.select(".dj-img-cont img").map { protocol + it.attr("src") }
|
|
||||||
|
|
||||||
val title = doc.selectFirst("h1.lillie > a").text()
|
|
||||||
val artists = doc.select("div.artist-list a").map{ it.text() }
|
|
||||||
val series = doc.select("a[href~=^/series/]").map { it.text() }
|
|
||||||
val type = doc.selectFirst("a[href~=^/type/]").text()
|
|
||||||
|
|
||||||
val language = {
|
|
||||||
val href = doc.select("a[href~=^/index-.+-1.html]").attr("href")
|
|
||||||
href.slice(7 until href.indexOf("-1"))
|
|
||||||
}.invoke()
|
|
||||||
|
|
||||||
val relatedTags = doc.select(".relatedtags a").map {
|
|
||||||
val href = URLDecoder.decode(it.attr("href"), "UTF-8")
|
|
||||||
href.slice(5 until href.indexOf("-all"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return GalleryBlock(Code.HITOMI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import xyz.quaver.Code
|
|
||||||
|
|
||||||
fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GalleryInfo(
|
|
||||||
val language_localname: String? = null,
|
|
||||||
val language: String? = null,
|
|
||||||
val date: String? = null,
|
|
||||||
val files: List<GalleryFiles>,
|
|
||||||
val id: Int? = null,
|
|
||||||
val type: String? = null,
|
|
||||||
val title: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GalleryFiles(
|
|
||||||
val width: Int,
|
|
||||||
val hash: String? = null,
|
|
||||||
val haswebp: Int = 0,
|
|
||||||
val name: String,
|
|
||||||
val height: Int,
|
|
||||||
val hasavif: Int = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Reader(val code: Code, val galleryInfo: GalleryInfo)
|
|
||||||
|
|
||||||
//Set header `Referer` to reader url to avoid 403 error
|
|
||||||
fun getReader(galleryID: Int) : Reader {
|
|
||||||
return Reader(Code.HITOMI, getGalleryInfo(galleryID))
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun doSearch(query: String, sortByPopularity: Boolean = false) : List<Int> {
|
|
||||||
val terms = query
|
|
||||||
.trim()
|
|
||||||
.replace(Regex("""^\?"""), "")
|
|
||||||
.toLowerCase(Locale.US)
|
|
||||||
.split(Regex("\\s+"))
|
|
||||||
.map {
|
|
||||||
it.replace('_', ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
val positiveTerms = LinkedList<String>()
|
|
||||||
val negativeTerms = LinkedList<String>()
|
|
||||||
|
|
||||||
for (term in terms) {
|
|
||||||
if (term.matches(Regex("^-.+")))
|
|
||||||
negativeTerms.push(term.replace(Regex("^-"), ""))
|
|
||||||
else
|
|
||||||
positiveTerms.push(term)
|
|
||||||
}
|
|
||||||
|
|
||||||
val positiveResults = positiveTerms.map {
|
|
||||||
CoroutineScope(Dispatchers.IO).async {
|
|
||||||
getGalleryIDsForQuery(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val negativeResults = negativeTerms.map {
|
|
||||||
CoroutineScope(Dispatchers.IO).async {
|
|
||||||
getGalleryIDsForQuery(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var results = when {
|
|
||||||
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", "all")
|
|
||||||
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", "all")
|
|
||||||
else -> listOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
@Synchronized fun filterPositive(newResults: List<Int>) {
|
|
||||||
results = when {
|
|
||||||
results.isEmpty() -> newResults
|
|
||||||
else -> newResults.sorted().let { sorted ->
|
|
||||||
results.filter { sorted.binarySearch(it) >= 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun filterNegative(newResults: List<Int>) {
|
|
||||||
results = newResults.sorted().let { sorted ->
|
|
||||||
results.filter { sorted.binarySearch(it) < 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//positive results
|
|
||||||
positiveResults.forEach {
|
|
||||||
filterPositive(it.await())
|
|
||||||
}
|
|
||||||
|
|
||||||
//negative results
|
|
||||||
negativeResults.forEach {
|
|
||||||
filterNegative(it.await())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import java.net.URL
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
|
|
||||||
//searchlib.js
|
|
||||||
const val separator = "-"
|
|
||||||
const val extension = ".html"
|
|
||||||
const val index_dir = "tagindex"
|
|
||||||
const val galleries_index_dir = "galleriesindex"
|
|
||||||
const val max_node_size = 464
|
|
||||||
const val B = 16
|
|
||||||
const val compressed_nozomi_prefix = "n"
|
|
||||||
|
|
||||||
var tag_index_version = getIndexVersion("tagindex")
|
|
||||||
var galleries_index_version = getIndexVersion("galleriesindex")
|
|
||||||
|
|
||||||
fun sha256(data: ByteArray) : ByteArray {
|
|
||||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
|
||||||
fun hashTerm(term: String) : UByteArray {
|
|
||||||
return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sanitize(input: String) : String {
|
|
||||||
return input.replace(Regex("[/#]"), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIndexVersion(name: String) : String {
|
|
||||||
return try {
|
|
||||||
URL("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}").openConnection(proxy).getInputStream().use {
|
|
||||||
it.reader().readText()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//search.js
|
|
||||||
fun getGalleryIDsForQuery(query: String) : List<Int> {
|
|
||||||
query.replace("_", " ").let {
|
|
||||||
if (it.indexOf(':') > -1) {
|
|
||||||
val sides = it.split(":")
|
|
||||||
val ns = sides[0]
|
|
||||||
var tag = sides[1]
|
|
||||||
|
|
||||||
var area : String? = ns
|
|
||||||
var language = "all"
|
|
||||||
when (ns) {
|
|
||||||
"female", "male" -> {
|
|
||||||
area = "tag"
|
|
||||||
tag = it
|
|
||||||
}
|
|
||||||
"language" -> {
|
|
||||||
area = null
|
|
||||||
language = tag
|
|
||||||
tag = "index"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getGalleryIDsFromNozomi(area, tag, language)
|
|
||||||
}
|
|
||||||
|
|
||||||
val key = hashTerm(it)
|
|
||||||
val field = "galleries"
|
|
||||||
|
|
||||||
val node = getNodeAtAddress(field, 0) ?: return emptyList()
|
|
||||||
|
|
||||||
val data = bSearch(field, key, node)
|
|
||||||
|
|
||||||
if (data != null)
|
|
||||||
return getGalleryIDsFromData(data)
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSuggestionsForQuery(query: String) : List<Suggestion> {
|
|
||||||
query.replace('_', ' ').let {
|
|
||||||
var field = "global"
|
|
||||||
var term = it
|
|
||||||
|
|
||||||
if (term.indexOf(':') > -1) {
|
|
||||||
val sides = it.split(':')
|
|
||||||
field = sides[0]
|
|
||||||
term = sides[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
val key = hashTerm(term)
|
|
||||||
val node = getNodeAtAddress(field, 0) ?: return emptyList()
|
|
||||||
val data = bSearch(field, key, node)
|
|
||||||
|
|
||||||
if (data != null)
|
|
||||||
return getSuggestionsFromData(field, data)
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
|
|
||||||
fun getSuggestionsFromData(field: String, data: Pair<Long, Int>) : List<Suggestion> {
|
|
||||||
if (tag_index_version.isEmpty())
|
|
||||||
tag_index_version = getIndexVersion("tagindex")
|
|
||||||
|
|
||||||
val url = "$protocol//$domain/$index_dir/$field.$tag_index_version.data"
|
|
||||||
val (offset, length) = data
|
|
||||||
if (length > 10000 || length <= 0)
|
|
||||||
throw Exception("length $length is too long")
|
|
||||||
|
|
||||||
val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return emptyList()
|
|
||||||
|
|
||||||
val suggestions = ArrayList<Suggestion>()
|
|
||||||
|
|
||||||
val buffer = ByteBuffer
|
|
||||||
.wrap(inbuf)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
val numberOfSuggestions = buffer.int
|
|
||||||
|
|
||||||
if (numberOfSuggestions > 100 || numberOfSuggestions <= 0)
|
|
||||||
throw Exception("number of suggestions $numberOfSuggestions is too long")
|
|
||||||
|
|
||||||
for (i in 0.until(numberOfSuggestions)) {
|
|
||||||
var top = buffer.int
|
|
||||||
|
|
||||||
val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
|
|
||||||
buffer.position(buffer.position()+top)
|
|
||||||
|
|
||||||
top = buffer.int
|
|
||||||
|
|
||||||
val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
|
|
||||||
buffer.position(buffer.position()+top)
|
|
||||||
|
|
||||||
val count = buffer.int
|
|
||||||
|
|
||||||
val tagname = sanitize(tag)
|
|
||||||
val u =
|
|
||||||
when(ns) {
|
|
||||||
"female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
|
|
||||||
"language" -> "/index-$tagname${separator}1$extension"
|
|
||||||
else -> "/$ns/$tagname${separator}all${separator}1$extension"
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestions.add(Suggestion(tag, count, u, ns))
|
|
||||||
}
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : List<Int> {
|
|
||||||
val nozomiAddress =
|
|
||||||
when(area) {
|
|
||||||
null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
|
|
||||||
else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
|
|
||||||
}
|
|
||||||
|
|
||||||
val bytes = try {
|
|
||||||
URL(nozomiAddress).openConnection(proxy).getInputStream().use {
|
|
||||||
it.readBytes()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nozomi = ArrayList<Int>()
|
|
||||||
|
|
||||||
val arrayBuffer = ByteBuffer
|
|
||||||
.wrap(bytes)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
while (arrayBuffer.hasRemaining())
|
|
||||||
nozomi.add(arrayBuffer.int)
|
|
||||||
|
|
||||||
return nozomi
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGalleryIDsFromData(data: Pair<Long, Int>) : List<Int> {
|
|
||||||
if (galleries_index_version.isEmpty())
|
|
||||||
galleries_index_version = getIndexVersion("galleriesindex")
|
|
||||||
|
|
||||||
val url = "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.data"
|
|
||||||
val (offset, length) = data
|
|
||||||
if (length > 100000000 || length <= 0)
|
|
||||||
throw Exception("length $length is too long")
|
|
||||||
|
|
||||||
val inbuf = getURLAtRange(url, offset.until(offset+length)) ?: return emptyList()
|
|
||||||
|
|
||||||
val galleryIDs = ArrayList<Int>()
|
|
||||||
|
|
||||||
val buffer = ByteBuffer
|
|
||||||
.wrap(inbuf)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
val numberOfGalleryIDs = buffer.int
|
|
||||||
|
|
||||||
val expectedLength = numberOfGalleryIDs*4+4
|
|
||||||
|
|
||||||
if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
|
|
||||||
throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
|
|
||||||
else if (inbuf.size != expectedLength)
|
|
||||||
throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
|
|
||||||
|
|
||||||
for (i in 0.until(numberOfGalleryIDs))
|
|
||||||
galleryIDs.add(buffer.int)
|
|
||||||
|
|
||||||
return galleryIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNodeAtAddress(field: String, address: Long) : Node? {
|
|
||||||
if (tag_index_version.isEmpty())
|
|
||||||
tag_index_version = getIndexVersion("tagindex")
|
|
||||||
if (galleries_index_version.isEmpty())
|
|
||||||
galleries_index_version = getIndexVersion("galleriesindex")
|
|
||||||
|
|
||||||
val url =
|
|
||||||
when(field) {
|
|
||||||
"galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.$galleries_index_version.index"
|
|
||||||
else -> "$protocol//$domain/$index_dir/$field.$tag_index_version.index"
|
|
||||||
}
|
|
||||||
|
|
||||||
val nodedata = getURLAtRange(url, address.until(address+max_node_size)) ?: return null
|
|
||||||
|
|
||||||
return decodeNode(nodedata)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getURLAtRange(url: String, range: LongRange) : ByteArray? {
|
|
||||||
try {
|
|
||||||
with (URL(url).openConnection(proxy) as HttpsURLConnection) {
|
|
||||||
requestMethod = "GET"
|
|
||||||
|
|
||||||
setRequestProperty("Range", "bytes=${range.first}-${range.last}")
|
|
||||||
|
|
||||||
return inputStream.readBytes()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
|
||||||
data class Node(val keys: List<UByteArray>, val datas: List<Pair<Long, Int>>, val subNodeAddresses: List<Long>)
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
|
||||||
fun decodeNode(data: ByteArray) : Node {
|
|
||||||
val buffer = ByteBuffer
|
|
||||||
.wrap(data)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
|
|
||||||
val uData = data.toUByteArray()
|
|
||||||
|
|
||||||
val numberOfKeys = buffer.int
|
|
||||||
val keys = ArrayList<UByteArray>()
|
|
||||||
|
|
||||||
for (i in 0.until(numberOfKeys)) {
|
|
||||||
val keySize = buffer.int
|
|
||||||
|
|
||||||
if (keySize == 0 || keySize > 32)
|
|
||||||
throw Exception("fatal: !keySize || keySize > 32")
|
|
||||||
|
|
||||||
keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize)))
|
|
||||||
buffer.position(buffer.position()+keySize)
|
|
||||||
}
|
|
||||||
|
|
||||||
val numberOfDatas = buffer.int
|
|
||||||
val datas = ArrayList<Pair<Long, Int>>()
|
|
||||||
|
|
||||||
for (i in 0.until(numberOfDatas)) {
|
|
||||||
val offset = buffer.long
|
|
||||||
val length = buffer.int
|
|
||||||
|
|
||||||
datas.add(Pair(offset, length))
|
|
||||||
}
|
|
||||||
|
|
||||||
val numberOfSubNodeAddresses = B+1
|
|
||||||
val subNodeAddresses = ArrayList<Long>()
|
|
||||||
|
|
||||||
for (i in 0.until(numberOfSubNodeAddresses)) {
|
|
||||||
val subNodeAddress = buffer.long
|
|
||||||
subNodeAddresses.add(subNodeAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Node(keys, datas, subNodeAddresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalUnsignedTypes::class)
|
|
||||||
fun bSearch(field: String, key: UByteArray, node: Node) : Pair<Long, Int>? {
|
|
||||||
fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int {
|
|
||||||
val top = Math.min(dv1.size, dv2.size)
|
|
||||||
|
|
||||||
for (i in 0.until(top)) {
|
|
||||||
if (dv1[i] < dv2[i])
|
|
||||||
return -1
|
|
||||||
else if (dv1[i] > dv2[i])
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun locateKey(key: UByteArray, node: Node) : Pair<Boolean, Int> {
|
|
||||||
for (i in 0 until node.keys.size) {
|
|
||||||
val cmpResult = compareArrayBuffers(key, node.keys[i])
|
|
||||||
|
|
||||||
if (cmpResult <= 0)
|
|
||||||
return Pair(cmpResult==0, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(false, node.keys.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLeaf(node: Node) : Boolean {
|
|
||||||
for (subnode in node.subNodeAddresses)
|
|
||||||
if (subnode != 0L)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.keys.isEmpty())
|
|
||||||
return null
|
|
||||||
|
|
||||||
val (there, where) = locateKey(key, node)
|
|
||||||
if (there)
|
|
||||||
return node.datas[where]
|
|
||||||
else if (isLeaf(node))
|
|
||||||
return null
|
|
||||||
|
|
||||||
val nextNode = getNodeAtAddress(field, node.subNodeAddresses[where]) ?: return null
|
|
||||||
return bSearch(field, key, nextNode)
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hiyobi
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
|
||||||
import xyz.quaver.hitomi.protocol
|
|
||||||
import xyz.quaver.json
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import java.net.URL
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
|
|
||||||
fun getGalleryBlock(galleryID: Int) : GalleryBlock? {
|
|
||||||
val url = "$protocol//api.$hiyobi/gallery/$galleryID"
|
|
||||||
|
|
||||||
val galleryBlock = with (URL(url).openConnection(proxy) as HttpsURLConnection) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
|
||||||
setRequestProperty("Cookie", cookie)
|
|
||||||
connectTimeout = 1000
|
|
||||||
connect()
|
|
||||||
|
|
||||||
inputStream.bufferedReader().use { it.readText() }
|
|
||||||
}.let {
|
|
||||||
json.parseJson(it).jsonObject
|
|
||||||
}
|
|
||||||
|
|
||||||
val galleryUrl = "reader/$galleryID"
|
|
||||||
|
|
||||||
val thumbnails = listOf("$protocol//cdn.$hiyobi/tn/$galleryID.jpg")
|
|
||||||
|
|
||||||
val title = galleryBlock["title"]?.contentOrNull ?: ""
|
|
||||||
val artists = galleryBlock["artists"]?.jsonArray?.mapNotNull {
|
|
||||||
it.jsonObject["value"]?.contentOrNull
|
|
||||||
} ?: listOf()
|
|
||||||
val series = galleryBlock["parodys"]?.jsonArray?.mapNotNull {
|
|
||||||
it.jsonObject["value"]?.contentOrNull
|
|
||||||
} ?: listOf()
|
|
||||||
val type = when (galleryBlock["type"]?.intOrNull) {
|
|
||||||
1 -> "doujinshi"
|
|
||||||
2 -> "manga"
|
|
||||||
3 -> "artistcg"
|
|
||||||
4 -> "gamecg"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val language = "korean"
|
|
||||||
|
|
||||||
val relatedTags = galleryBlock["tags"]?.jsonArray?.mapNotNull {
|
|
||||||
it.jsonObject["value"]?.contentOrNull
|
|
||||||
} ?: listOf()
|
|
||||||
|
|
||||||
return GalleryBlock(Code.HIYOBI, galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.hiyobi
|
|
||||||
|
|
||||||
import kotlinx.serialization.UnstableDefault
|
|
||||||
import kotlinx.serialization.builtins.list
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import xyz.quaver.Code
|
|
||||||
import xyz.quaver.hitomi.GalleryFiles
|
|
||||||
import xyz.quaver.hitomi.GalleryInfo
|
|
||||||
import xyz.quaver.hitomi.Reader
|
|
||||||
import xyz.quaver.hitomi.protocol
|
|
||||||
import xyz.quaver.json
|
|
||||||
import xyz.quaver.proxy
|
|
||||||
import java.net.URL
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
|
|
||||||
const val hiyobi = "hiyobi.me"
|
|
||||||
const val primary_img_domain = "cdn.hiyobi.me"
|
|
||||||
const val user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36"
|
|
||||||
|
|
||||||
var cookie: String = ""
|
|
||||||
get() {
|
|
||||||
if (field.isEmpty())
|
|
||||||
field = renewCookie()
|
|
||||||
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Images(
|
|
||||||
val path: String,
|
|
||||||
val no: Int,
|
|
||||||
val name: String
|
|
||||||
)
|
|
||||||
|
|
||||||
fun renewCookie() : String {
|
|
||||||
val url = "https://$hiyobi/"
|
|
||||||
|
|
||||||
try {
|
|
||||||
with(URL(url).openConnection(proxy) as HttpsURLConnection) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
|
||||||
connectTimeout = 2000
|
|
||||||
connect()
|
|
||||||
return headerFields["Set-Cookie"]!![0]
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableDefault::class)
|
|
||||||
fun getReader(galleryID: Int) : Reader {
|
|
||||||
val data = "https://cdn.$hiyobi/data/json/$galleryID.json"
|
|
||||||
val list = "https://cdn.$hiyobi/data/json/${galleryID}_list.json"
|
|
||||||
|
|
||||||
val title = with(URL(data).openConnection(proxy) as HttpsURLConnection) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
|
||||||
setRequestProperty("Cookie", cookie)
|
|
||||||
connectTimeout = 1000
|
|
||||||
connect()
|
|
||||||
|
|
||||||
inputStream.bufferedReader().use { it.readText() }
|
|
||||||
}.let {
|
|
||||||
json.parseJson(it).jsonObject["n"]?.contentOrNull
|
|
||||||
}
|
|
||||||
|
|
||||||
val galleryFiles = json.parse(
|
|
||||||
GalleryFiles.serializer().list,
|
|
||||||
with(URL(list).openConnection(proxy) as HttpsURLConnection) {
|
|
||||||
setRequestProperty("User-Agent", user_agent)
|
|
||||||
setRequestProperty("Cookie", cookie)
|
|
||||||
connectTimeout = 1000
|
|
||||||
connect()
|
|
||||||
|
|
||||||
inputStream.bufferedReader().use { it.readText() }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Reader(Code.HIYOBI, GalleryInfo(title = title, files = galleryFiles))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createImgList(galleryID: Int, reader: Reader, lowQuality: Boolean = false) =
|
|
||||||
if (lowQuality)
|
|
||||||
reader.galleryInfo.files.map {
|
|
||||||
val name = it.name.replace(Regex("""\.[^/.]+$"""), "")
|
|
||||||
Images("$protocol//$primary_img_domain/data_r/$galleryID/$name.jpg", galleryID, it.name)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
reader.galleryInfo.files.map { Images("$protocol//$primary_img_domain/data/$galleryID/${it.name}", galleryID, it.name) }
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 tom5079
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:Suppress("UNUSED_VARIABLE")
|
|
||||||
|
|
||||||
package xyz.quaver.hitomi
|
|
||||||
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
import xyz.quaver.availableInHiyobi
|
|
||||||
|
|
||||||
class UnitTest {
|
|
||||||
@Test
|
|
||||||
fun test() {
|
|
||||||
assertEquals(
|
|
||||||
"6/2d/c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6",
|
|
||||||
fullPathFromHash("c26014fc6153ef717932d85f4d26c75195560fb2ce1da60b431ef376501642d6")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_nozomi() {
|
|
||||||
val nozomi = getGalleryIDsFromNozomi(null, "popular", "all")
|
|
||||||
|
|
||||||
print(nozomi.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_search() {
|
|
||||||
val ids = getGalleryIDsForQuery("language:korean").reversed()
|
|
||||||
|
|
||||||
print(ids.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_suggestions() {
|
|
||||||
val suggestions = getSuggestionsForQuery("language:g")
|
|
||||||
|
|
||||||
print(suggestions)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_doSearch() {
|
|
||||||
val r = doSearch("female:loli female:bondage language:korean -male:yaoi -male:guro -female:guro", true)
|
|
||||||
|
|
||||||
print(r.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_getBlock() {
|
|
||||||
val galleryBlock = getGalleryBlock(1428250)
|
|
||||||
|
|
||||||
print(galleryBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_getGallery() {
|
|
||||||
val gallery = getGallery(1552751)
|
|
||||||
|
|
||||||
print(gallery)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_getReader() {
|
|
||||||
val reader = getReader(1574736)
|
|
||||||
|
|
||||||
print(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_getImages() {
|
|
||||||
val reader = getReader(1702001)
|
|
||||||
|
|
||||||
print(urlFromUrlFromHash(1702001, reader.galleryInfo.files[15], "webp"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_hiyobi() {
|
|
||||||
val reader = xyz.quaver.hiyobi.getReader(1664762)
|
|
||||||
|
|
||||||
print(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_urlFromUrlFromHash() {
|
|
||||||
val url = urlFromUrlFromHash(1531795, GalleryFiles(
|
|
||||||
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
|
|
||||||
), "webp")
|
|
||||||
|
|
||||||
print(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_availableInHiyobi() {
|
|
||||||
val result = availableInHiyobi(1272781)
|
|
||||||
|
|
||||||
print(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun test_hiyobi_galleryBlock() {
|
|
||||||
val galleryBlock = xyz.quaver.hiyobi.getGalleryBlock(10000027)
|
|
||||||
|
|
||||||
print(galleryBlock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user