mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
155 Commits
v0.1.0.447
...
v0.1.0.768
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e47c7e6a47 | ||
|
|
d937e0324f | ||
|
|
ff623d4c39 | ||
|
|
0b05f5ef24 | ||
|
|
e6c3292485 | ||
|
|
ba1c1baeb5 | ||
|
|
5e7f4f3fc1 | ||
|
|
3dd11213fa | ||
|
|
50cae0719f | ||
|
|
2c6680e4fa | ||
|
|
96afb7f327 | ||
|
|
0d1025d60a | ||
|
|
0508dd2b66 | ||
|
|
026a503d5f | ||
|
|
b3f8e648cd | ||
|
|
13b458090d | ||
|
|
f97c3ff9bd | ||
|
|
841ff7b6ee | ||
|
|
377db47daf | ||
|
|
2addcab765 | ||
|
|
ddc676c608 | ||
|
|
924db7a394 | ||
|
|
580113d6ce | ||
|
|
d532a69edc | ||
|
|
01f7e11d5a | ||
|
|
de97ec95db | ||
|
|
6e66467ab3 | ||
|
|
4c5131708d | ||
|
|
bb2e1a6037 | ||
|
|
5949bd97fd | ||
|
|
eaff071b16 | ||
|
|
a922586aba | ||
|
|
80beea9bdb | ||
|
|
8c326fc5c2 | ||
|
|
e26081acff | ||
|
|
b3b0467d22 | ||
|
|
e1c98d2b38 | ||
|
|
e304461dfb | ||
|
|
a127e5a30f | ||
|
|
e252cd4d3e | ||
|
|
9a1bd3db4c | ||
|
|
acce098e02 | ||
|
|
afa87b7113 | ||
|
|
4cbd2cd8bc | ||
|
|
e81d0f3e97 | ||
|
|
34a6a0e0c9 | ||
|
|
4254a05ea3 | ||
|
|
d32a94c14d | ||
|
|
6a9155bcf5 | ||
|
|
de442cc659 | ||
|
|
7f514c8f1e | ||
|
|
4c51f09acb | ||
|
|
a60388fcf9 | ||
|
|
11b656dabf | ||
|
|
535f29bef4 | ||
|
|
0ddc530dd4 | ||
|
|
b5321d33c9 | ||
|
|
4116c10caa | ||
|
|
0fe2cf5c2d | ||
|
|
c94573e868 | ||
|
|
1945af060d | ||
|
|
cf399ffdcc | ||
|
|
5846188202 | ||
|
|
fc65a89fbc | ||
|
|
b62ae41de8 | ||
|
|
8107f309b4 | ||
|
|
c2c12297bd | ||
|
|
81cbdab5eb | ||
|
|
9ee5a3e94b | ||
|
|
a570fd2a8f | ||
|
|
5cffb10e08 | ||
|
|
b11bf284dc | ||
|
|
5c4c042b2e | ||
|
|
d1cb744efd | ||
|
|
79b910a80c | ||
|
|
01e08e0c31 | ||
|
|
dd3c9c268e | ||
|
|
725f738ee1 | ||
|
|
22c738f43e | ||
|
|
e45f88473c | ||
|
|
889591d0b1 | ||
|
|
fe8247df8a | ||
|
|
7a5721bcee | ||
|
|
eea5c3e9a4 | ||
|
|
f55493c9a9 | ||
|
|
79618adaf9 | ||
|
|
af13d6ed80 | ||
|
|
38c09277d9 | ||
|
|
1fb693d066 | ||
|
|
7414a2f690 | ||
|
|
000590bcf7 | ||
|
|
d5d6625a63 | ||
|
|
00f33cb48f | ||
|
|
fd265c5734 | ||
|
|
316543b9aa | ||
|
|
117ebcff2d | ||
|
|
aab394b2c8 | ||
|
|
6ae520c061 | ||
|
|
dfb254d2dc | ||
|
|
07c03b0a12 | ||
|
|
eb0cf2d5f6 | ||
|
|
69c04ebe7a | ||
|
|
135db6d2ff | ||
|
|
c3deace9e6 | ||
|
|
9042594b14 | ||
|
|
44df0f5c3d | ||
|
|
7b9446eb35 | ||
|
|
c6b6daaf80 | ||
|
|
3b42b6a7e0 | ||
|
|
b6238f469c | ||
|
|
ae00c3aa6b | ||
|
|
8bf9d1b016 | ||
|
|
19ed7aa804 | ||
|
|
4d129ada95 | ||
|
|
4ec8ea0e4d | ||
|
|
bd79d3c828 | ||
|
|
7fb6c539d4 | ||
|
|
7f6fa2efbe | ||
|
|
b1727d9d91 | ||
|
|
3435d9db6e | ||
|
|
8e597c8179 | ||
|
|
80ec66514e | ||
|
|
88e677d973 | ||
|
|
a00f32c508 | ||
|
|
538db52d16 | ||
|
|
1ce7b0e56e | ||
|
|
87d91a0f15 | ||
|
|
61c1e934a5 | ||
|
|
97b09335df | ||
|
|
5e34fd2a9f | ||
|
|
7e620bd156 | ||
|
|
a97b801b24 | ||
|
|
9e64acd407 | ||
|
|
f72269f91b | ||
|
|
94d7f768a1 | ||
|
|
78cdc78cf9 | ||
|
|
2fc1257f42 | ||
|
|
210311cb38 | ||
|
|
9d7ec89314 | ||
|
|
9c279701a6 | ||
|
|
743e2e9b21 | ||
|
|
4a8daea940 | ||
|
|
bd90b74c12 | ||
|
|
4dff0c075a | ||
|
|
b8f57507dd | ||
|
|
61bfa9e7ed | ||
|
|
bbea256c85 | ||
|
|
5a1186639e | ||
|
|
a8f2700fe6 | ||
|
|
eeec505182 | ||
|
|
66dc53b92f | ||
|
|
334f3514df | ||
|
|
6ce35f6a24 | ||
|
|
f6a5f887ce | ||
|
|
bc90415394 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: Prowlarr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: prowlarr
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit or Discord first. Exceptions do not mean you found a bug!
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug! -->
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen.-->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem.-->
|
||||
|
||||
**Platform Information (please complete the following information):**
|
||||
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
|
||||
- Docker: <!-- [Yes/No] -->
|
||||
- .NET Version (System -> Status): <!--[e.g. .NET 5.0.1] -->
|
||||
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
|
||||
- Prowlarr Version: <!--[e.g. 0.1.2.1854-->
|
||||
- Prowlarr Branch: <!--[e.g. develop, nightly]-->
|
||||
|
||||
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
|
||||
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
|
||||
<!-- Trace logs are named Prowlarr.trace.txt or Prowlarr.trace.#.txt and will contain "trace" in them-->
|
||||
74
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Bug Report
|
||||
title: "[BUG]: "
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Prowlarr**: Prowlarr 0.1.0.650
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
value: |
|
||||
- OS:
|
||||
- Prowlarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What branch are you running?
|
||||
options:
|
||||
- Master
|
||||
- Develop
|
||||
- Nightly
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/prowlarr/troubleshooting#logging-and-log-files)
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: true
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Indexer Requests
|
||||
url: https://requests.prowlarr.com/
|
||||
about: Request new indexers to be added. Vote on existing requests.
|
||||
- name: Support via Discord
|
||||
url: https://prowlarr.com/discord
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for Prowlarr
|
||||
title: ''
|
||||
labels: 'Type: Feature Request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Feature Request
|
||||
title: "[FEAT]: "
|
||||
description: 'Suggest an idea for Prowlarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: true
|
||||
22
.github/ISSUE_TEMPLATE/indexer_request.md
vendored
22
.github/ISSUE_TEMPLATE/indexer_request.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Indexer Request
|
||||
about: Request an indexer for Prowlarr. Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests will be closed without warning. Please search GitHub prior to requesting.
|
||||
title: '(Indexer) '
|
||||
labels: 'Type: Indexer Request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests or requests covered in existing GitHub Issues will be closed without warning. Please search GitHub prior to requesting.-->
|
||||
|
||||
**Type:** <Usenet|Torrents>
|
||||
|
||||
**Tracker:** <Indexer/Tracker Name>
|
||||
|
||||
**URL:** <Indexer/Tracker URL>
|
||||
|
||||
**In Jackett?:** <Yes|No>
|
||||
<!-- Check the pinned Jackett parity issue prior to submitting a request. Duplicated requests or requests covered in existing GitHub Issues will be closed without warning. Please search GitHub prior to requesting.-->
|
||||
|
||||
**Additional Context:**
|
||||
<!-- Add any other context or screenshots about the request here. -->
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,14 +1,15 @@
|
||||
#### Database Migration
|
||||
YES | NO
|
||||
YES - XXXX | NO
|
||||
|
||||
#### Description
|
||||
A few sentences describing the overall goals of the pull request's commits.
|
||||
|
||||
#### Screenshot (if UI related)
|
||||
|
||||
#### Todos
|
||||
- [ ] Tests
|
||||
- [ ] Translation Keys
|
||||
- [ ] Wiki Updates
|
||||
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
|
||||
- [ ] [Wiki Updates](https://wiki.servarr.com)
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
|
||||
|
||||
41
.github/workflows/azuresync.yml
vendored
Normal file
41
.github/workflows/azuresync.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Sync issue to Azure DevOps work item
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
|
||||
|
||||
concurrency: azuresync-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
alert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Prowlarr"
|
||||
ado_wit: "Bug"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Prowlarr"
|
||||
ado_wit: "User Story"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
We're always looking for people to help make Prowlarr even better, there are a number of ways to contribute.
|
||||
|
||||
This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/prowlarr/contributing).
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.servarr.com/prowlarr) the better.
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/prowlarr) the better.
|
||||
|
||||
## Development ##
|
||||
|
||||
@@ -23,11 +25,11 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wikijs.ser
|
||||
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||
5. Build the project in Visual Studio, Setting startup project to `Prowlarr.Console` and framework to `net5.0`
|
||||
6. Debug the project in Visual Studio
|
||||
7. Open http://localhost:7878
|
||||
7. Open http://localhost:9696
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Prowlarr/Prowlarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||
- Rebase from Radarr's develop branch, don't merge
|
||||
- Rebase from Prowlarr's develop branch, don't merge
|
||||
- Make meaningful commits, or squash them
|
||||
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||
- Reach out to us on the discord if you have any questions
|
||||
|
||||
17
README.md
17
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
||||
[](https://wikijs.servarr.com/prowlarr/installation#docker)
|
||||
[](https://wiki.servarr.com/prowlarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
@@ -10,12 +10,14 @@
|
||||
Prowlarr is a indexer manager/proxy built on the popular arr .net/reactjs base stack to integrate with your various PVR apps. Prowlarr supports both Torrent Trackers and Usenet Indexers. It integrates seamlessly with Sonarr, Radarr, Lidarr, and Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all).
|
||||
|
||||
## Major Features Include:
|
||||
- Usenet support for any Newznab compatible indexer, including Headphones VIP
|
||||
- Torrent support 400+ trackers & more coming soon
|
||||
- Usenet support for 24 indexers natively, including Headphones VIP, and support for any Newznab compatible indexer via "Generic Newznab"
|
||||
- Torrent support for over 500 trackers with more added all the time
|
||||
- Torrent support for any Torznab compatible tracker via "Generic Torznab"
|
||||
- Indexer Sync to Sonarr/Radarr/Readarr/Lidarr, so no manual configuration of the other applications are required
|
||||
- Indexer History and Statistics
|
||||
- Manual Searching of Trackers & Indexers at a category level
|
||||
- Support for pushing releases directly to your download clients from Prowlarr
|
||||
- Indexer health and status notifications
|
||||
|
||||
## Support
|
||||
Note: Prowlarr is currently early in life, thus bugs should be expected
|
||||
@@ -23,11 +25,14 @@ Note: Prowlarr is currently early in life, thus bugs should be expected
|
||||
[](https://prowlarr.com/discord)
|
||||
[](https://www.reddit.com/r/Prowlarr)
|
||||
[](https://github.com/Prowlarr/Prowlarr/issues)
|
||||
[](https://wikijs.servarr.com/prowlarr)
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
|
||||
## Feature Requests
|
||||
## Indexers/Trackers
|
||||
|
||||
[Feature Requests](https://github.com/Prowlarr/Prowlarr/issues/new?assignees=&template=feature_request.md&Type%3A%20Feature%20Request&title=)
|
||||
[Supported Indexers](https://wiki.servarr.com/en/prowlarr/supported-indexers)
|
||||
|
||||
[Indexer Requests](https://requests.prowlarr.com)
|
||||
- Request or vote on an existing request for a new tracker/indexer
|
||||
|
||||
## Contributors & Developers
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
|
||||
@@ -13,7 +13,7 @@ variables:
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '5.0.203'
|
||||
dotnetVersion: '5.0.302'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
|
||||
trigger:
|
||||
@@ -879,7 +879,7 @@ stages:
|
||||
artifactName: 'WindowsAutomationScreenshots'
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
- checkout: none
|
||||
- powershell: |
|
||||
- pwsh: |
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
|
||||
env:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
|
||||
@@ -129,7 +129,7 @@ class FileBrowserModalContent extends Component {
|
||||
className={styles.mappedDrivesWarning}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
<Link to="https://wikijs.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server">
|
||||
<Link to="https://wiki.servarr.com/prowlarr/faq#why-cant-prowlarr-see-my-files-on-a-remote-server">
|
||||
{translate('MappedDrivesRunningAsService')}
|
||||
</Link>
|
||||
</Alert>
|
||||
|
||||
@@ -13,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerModalContent.css';
|
||||
|
||||
@@ -31,6 +32,7 @@ function EditIndexerModalContent(props) {
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteIndexerPress,
|
||||
onAdvancedSettingsPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -165,6 +167,12 @@ function EditIndexerModalContent(props) {
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
@@ -204,6 +212,7 @@ EditIndexerModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteIndexerPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
|
||||
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
|
||||
import EditIndexerModalContent from './EditIndexerModalContent';
|
||||
|
||||
@@ -23,7 +24,8 @@ const mapDispatchToProps = {
|
||||
setIndexerValue,
|
||||
setIndexerFieldValue,
|
||||
saveIndexer,
|
||||
testIndexer
|
||||
testIndexer,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditIndexerModalContentConnector extends Component {
|
||||
@@ -56,6 +58,11 @@ class EditIndexerModalContentConnector extends Component {
|
||||
this.props.testIndexer({ id: this.props.id });
|
||||
}
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
console.log('settings');
|
||||
this.props.toggleAdvancedSettings();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -65,6 +72,7 @@ class EditIndexerModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -80,6 +88,7 @@ EditIndexerModalContentConnector.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
setIndexerValue: PropTypes.func.isRequired,
|
||||
setIndexerFieldValue: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
saveIndexer: PropTypes.func.isRequired,
|
||||
testIndexer: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -71,7 +71,7 @@ class IndexerIndexRow extends Component {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
baseUrl,
|
||||
indexerUrls,
|
||||
enable,
|
||||
redirect,
|
||||
tags,
|
||||
@@ -248,7 +248,7 @@ class IndexerIndexRow extends Component {
|
||||
className={styles.externalLink}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
title={'Website'}
|
||||
to={baseUrl.replace('api.', '')}
|
||||
to={indexerUrls[0].replace('api.', '')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
@@ -289,7 +289,7 @@ class IndexerIndexRow extends Component {
|
||||
|
||||
IndexerIndexRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
|
||||
@@ -18,7 +18,7 @@ function IndexerInfoModalContent(props) {
|
||||
description,
|
||||
encoding,
|
||||
language,
|
||||
baseUrl,
|
||||
indexerUrls,
|
||||
protocol,
|
||||
onModalClose
|
||||
} = props;
|
||||
@@ -54,10 +54,10 @@ function IndexerInfoModalContent(props) {
|
||||
|
||||
<DescriptionListItemTitle>Indexer Site</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={baseUrl}>{baseUrl}</Link>
|
||||
<Link to={indexerUrls[0]}>{indexerUrls[0]}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url</DescriptionListItemTitle>
|
||||
<DescriptionListItemTitle>{`${protocol === 'usenet' ? 'Newznab' : 'Torznab'} Url`}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
|
||||
</DescriptionListItemDescription>
|
||||
@@ -74,7 +74,7 @@ IndexerInfoModalContent.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
encoding: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
indexerUrls: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.advancedSettings,
|
||||
createIndexerSelector(),
|
||||
(advancedSettings, indexer) => {
|
||||
console.log(indexer);
|
||||
return {
|
||||
advancedSettings,
|
||||
...indexer
|
||||
|
||||
@@ -19,6 +19,10 @@ function getAverageResponseTimeData(indexerStats) {
|
||||
};
|
||||
});
|
||||
|
||||
data.sort((a, b) => {
|
||||
return b.value - a.value;
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Protocol
|
||||
{translate('Protocol')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -35,7 +35,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Age
|
||||
{translate('Age')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -53,7 +53,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Indexer
|
||||
{translate('Indexer')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -62,7 +62,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Size
|
||||
{translate('Size')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -71,7 +71,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Files
|
||||
{translate('Files')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -80,7 +80,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Grabs
|
||||
{translate('Grabs')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -89,7 +89,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Peers
|
||||
{translate('Peers')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
@@ -98,7 +98,7 @@ function SearchIndexSortMenu(props) {
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Category
|
||||
{translate('Category')}
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
|
||||
@@ -19,7 +19,7 @@ function NoSearchResults(props) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
No search results found, try performing a new search below.
|
||||
{translate('NoSearchResultsFound')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import TextInput from 'Components/Form/TextInput';
|
||||
import keyboardShortcuts from 'Components/keyboardShortcuts';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SearchFooterLabel from './SearchFooterLabel';
|
||||
import styles from './SearchFooter.css';
|
||||
|
||||
@@ -167,7 +168,7 @@ class SearchFooter extends Component {
|
||||
isDisabled={isFetching || !hasIndexers}
|
||||
onPress={this.onSearchPress}
|
||||
>
|
||||
Search
|
||||
{translate('Search')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Tooltip from '../../Components/Tooltip/Tooltip';
|
||||
|
||||
function CategoryLabel({ categories }) {
|
||||
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
|
||||
|
||||
if (categories?.length === 0) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={<Label kind={kinds.DANGER}>Unknown</Label>}
|
||||
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
@@ -20,6 +32,10 @@ function CategoryLabel({ categories }) {
|
||||
);
|
||||
}
|
||||
|
||||
CategoryLabel.defaultProps = {
|
||||
categories: []
|
||||
};
|
||||
|
||||
CategoryLabel.propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ import styles from './AdvancedSettingsButton.css';
|
||||
function AdvancedSettingsButton(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
onAdvancedSettingsPress
|
||||
onAdvancedSettingsPress,
|
||||
showLabel
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -43,18 +44,27 @@ function AdvancedSettingsButton(props) {
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showLabel &&
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
AdvancedSettingsButton.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
showLabel: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AdvancedSettingsButton.defaultProps = {
|
||||
showLabel: true
|
||||
};
|
||||
|
||||
export default AdvancedSettingsButton;
|
||||
|
||||
@@ -55,7 +55,7 @@ function UpdateSettings(props) {
|
||||
type={inputTypes.TEXT}
|
||||
name="branch"
|
||||
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
|
||||
helpLink="https://wikijs.servarr.com/prowlarr/settings#updates"
|
||||
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
|
||||
{...branch}
|
||||
onChange={onInputChange}
|
||||
readOnly={usingExternalUpdateMechanism}
|
||||
@@ -92,7 +92,7 @@ function UpdateSettings(props) {
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText={translate('UpdateMechanismHelpText')}
|
||||
helpLink="https://wikijs.servarr.com/prowlarr/settings#updates"
|
||||
helpLink="https://wiki.servarr.com/prowlarr/settings#updates"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,7 @@ function NotificationEventItems(props) {
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text={translate('NotificationTriggersHelpText')}
|
||||
link="https://wikijs.servarr.com/prowlarr/settings#connections"
|
||||
link="https://wiki.servarr.com/prowlarr/settings#connections"
|
||||
/>
|
||||
<div className={styles.events}>
|
||||
<div>
|
||||
|
||||
@@ -202,7 +202,8 @@ export const defaultState = {
|
||||
|
||||
export const persistState = [
|
||||
'releases.customFilters',
|
||||
'releases.selectedFilterKey'
|
||||
'releases.selectedFilterKey',
|
||||
'releases.columns'
|
||||
];
|
||||
|
||||
//
|
||||
|
||||
@@ -13,7 +13,7 @@ function createHealthCheckSelector() {
|
||||
source: 'UI',
|
||||
type: 'warning',
|
||||
message: translate('CouldNotConnectSignalR'),
|
||||
wikiUrl: 'https://wikijs.servarr.com/prowlarr/system#could-not-connect-to-signalr'
|
||||
wikiUrl: 'https://wiki.servarr.com/prowlarr/system#could-not-connect-to-signalr'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ function getInternalLink(source) {
|
||||
function getTestLink(source, props) {
|
||||
switch (source) {
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
|
||||
@@ -22,7 +22,7 @@ class MoreInfo extends Component {
|
||||
|
||||
<DescriptionListItemTitle>{translate('Wiki')}</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://wikijs.servarr.com/prowlarr">wikijs.servarr.com/prowlarr</Link>
|
||||
<Link to="https://wiki.servarr.com/prowlarr">wiki.servarr.com/prowlarr</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>{translate('Reddit')}</DescriptionListItemTitle>
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="https://wikijs.servarr.com/prowlarr/faq#help-i-have-locked-myself-out"
|
||||
href="https://wiki.servarr.com/prowlarr/faq#help-i-have-locked-myself-out"
|
||||
class="forgot-password"
|
||||
>Forgot your password?</a
|
||||
>
|
||||
|
||||
16
package.json
16
package.json
@@ -30,9 +30,9 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||
"@fortawesome/react-fontawesome": "0.1.14",
|
||||
"@microsoft/signalr": "5.0.6",
|
||||
"@sentry/browser": "6.3.1",
|
||||
"@sentry/integrations": "6.3.1",
|
||||
"@microsoft/signalr": "5.0.8",
|
||||
"@sentry/browser": "6.10.0",
|
||||
"@sentry/integrations": "6.10.0",
|
||||
"chart.js": "3.2.0",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.8",
|
||||
@@ -98,12 +98,12 @@
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.11.0",
|
||||
"css-loader": "5.2.4",
|
||||
"eslint": "7.25.0",
|
||||
"eslint": "7.31.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-react": "7.23.2",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
"eslint-plugin-react": "7.24.0",
|
||||
"eslint-plugin-simple-import-sort": "7.0.0",
|
||||
"esprint": "2.0.0",
|
||||
"esprint": "3.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "5.0.0",
|
||||
"html-webpack-plugin": "5.3.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
"webpack": "5.35.1",
|
||||
"webpack-cli": "4.6.0",
|
||||
"webpack-livereload-plugin": "3.0.1",
|
||||
"stylelint": "13.13.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-order": "4.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.97" />
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"https://horrorcharnel.org/takeloginhorror.php: username=mySecret&password=mySecret&use_sslvalue==&perm_ssl=1&submitme=X&use_ssl=1&returnto=%2F&captchaSelection=1230456")]
|
||||
[TestCase(@"https://torrentdb.net/login: _token=2b51db35e1912ffc138825a12b9933d2&username=mySecret&password=mySecret&remember=on")]
|
||||
[TestCase(@" var authkey = ""2b51db35e1910123321025a12b9933d2"";")]
|
||||
[TestCase(@"https://hd-space.org/index.php?page=login: uid=mySecret&pwd=mySecret")]
|
||||
[TestCase(@"https://beyond-hd.me/api/torrents/2b51db35e1912ffc138825a12b9933d2")]
|
||||
|
||||
// NzbGet
|
||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||
@@ -90,5 +93,14 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||
|
||||
cleansedMessage.Should().Be(message);
|
||||
}
|
||||
|
||||
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
|
||||
[TestCase(@"&useToken=2b51db35e1910123321025a12b9933d2")]
|
||||
public void should_not_clean_usetoken(string message)
|
||||
{
|
||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||
|
||||
cleansedMessage.Should().Be(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ namespace NzbDrone.Common.Extensions
|
||||
return dateTime >= afterDateTime && dateTime <= beforeDateTime;
|
||||
}
|
||||
|
||||
public static DateTime EndOfDay(this DateTime date)
|
||||
{
|
||||
return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
|
||||
}
|
||||
|
||||
public static DateTime StartOfDay(this DateTime date)
|
||||
{
|
||||
return new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public static DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,5 +194,21 @@ namespace NzbDrone.Common.Extensions
|
||||
var inputBytes = encoding.GetBytes(searchString);
|
||||
return encoding.GetString(WebUtility.UrlDecodeToBytes(inputBytes, 0, inputBytes.Length));
|
||||
}
|
||||
|
||||
public static string CleanFileName(this string name)
|
||||
{
|
||||
string result = name;
|
||||
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
|
||||
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
|
||||
|
||||
result = result.Replace(": ", " - ");
|
||||
|
||||
for (int i = 0; i < badCharacters.Length; i++)
|
||||
{
|
||||
result = result.Replace(badCharacters[i], goodCharacters[i]);
|
||||
}
|
||||
|
||||
return result.TrimStart(' ', '.').TrimEnd(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace NzbDrone.Common.Http
|
||||
public HttpUri Url { get; set; }
|
||||
public HttpMethod Method { get; set; }
|
||||
public HttpHeader Headers { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
public byte[] ContentData { get; set; }
|
||||
public string ContentSummary { get; set; }
|
||||
public bool SuppressHttpError { get; set; }
|
||||
@@ -75,8 +76,15 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
public void SetContent(string data)
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
|
||||
ContentData = encoding.GetBytes(data);
|
||||
if (Encoding != null)
|
||||
{
|
||||
ContentData = Encoding.GetBytes(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
var encoding = HttpHeader.GetEncodingFromContentType(Headers.ContentType);
|
||||
ContentData = encoding.GetBytes(data);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddBasicAuthentication(string username, string password)
|
||||
|
||||
@@ -47,7 +47,14 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
if (_content == null)
|
||||
{
|
||||
_content = Headers.GetEncodingFromContentType().GetString(ResponseData);
|
||||
if (Request.Encoding != null)
|
||||
{
|
||||
_content = Request.Encoding.GetString(ResponseData);
|
||||
}
|
||||
else
|
||||
{
|
||||
_content = Headers.GetEncodingFromContentType().GetString(ResponseData);
|
||||
}
|
||||
}
|
||||
|
||||
return _content;
|
||||
|
||||
@@ -11,13 +11,15 @@ namespace NzbDrone.Common.Instrumentation
|
||||
private static readonly Regex[] CleansingRules = new[]
|
||||
{
|
||||
// Url
|
||||
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&| )[^=]*?(_?token|username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&|: |;)(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd|pwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=\?|&| )[^=]*?(_?(?<!use)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"iptorrents\.com/[/a-z0-9?&;]*?(?:[?&;](u|tp)=(?<secret>[^&=;]+?))+(?= |;|&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||
new Regex(@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=authkey = "")(?<secret>[^&=]+?)(?="")", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
new Regex(@"(?<=beyond-hd\.[a-z]+/api/torrents/)(?<secret>[^&=][a-z0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
// Path
|
||||
new Regex(@"""C:\\Users\\(?<secret>[^\""]+?)(\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
|
||||
<PackageReference Include="DryIoc.dll" Version="4.7.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.9" />
|
||||
<PackageReference Include="Sentry" Version="3.3.3" />
|
||||
<PackageReference Include="Sentry" Version="3.8.3" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.Test.HealthCheck
|
||||
[TestFixture]
|
||||
public class HealthCheckFixture : CoreTest
|
||||
{
|
||||
private const string WikiRoot = "https://wikijs.servarr.com/";
|
||||
private const string WikiRoot = "https://wiki.servarr.com/";
|
||||
|
||||
[TestCase("I blew up because of some weird user mistake", null, WikiRoot + "prowlarr/system#i-blew-up-because-of-some-weird-user-mistake")]
|
||||
[TestCase("I blew up because of some weird user mistake", "#my-health-check", WikiRoot + "prowlarr/system#my-health-check")]
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
Subject.Settings = new FileListSettings()
|
||||
{
|
||||
Passkey = "abcd",
|
||||
Username = "somename"
|
||||
Username = "somename",
|
||||
BaseUrl = "https://filelist.io"
|
||||
};
|
||||
|
||||
Subject.Capabilities = new IndexerCapabilities
|
||||
@@ -54,8 +55,6 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
SearchTerm = "Star Wars",
|
||||
Categories = new int[] { 2000 }
|
||||
};
|
||||
|
||||
Subject.BaseUrl = "https://filelist.io";
|
||||
}
|
||||
|
||||
private void MovieWithoutIMDB()
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
public class TestIndexer : UsenetIndexerBase<TestIndexerSettings>
|
||||
{
|
||||
public override string Name => "Test Indexer";
|
||||
public override string BaseUrl => "http://testindexer.com";
|
||||
public override string[] IndexerUrls => new string[] { "http://testindexer.com" };
|
||||
public override string Description => "";
|
||||
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Usenet;
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Test.IndexerTests
|
||||
{
|
||||
public class TestIndexerSettings : IProviderConfig
|
||||
public class TestIndexerSettings : IIndexerSettings
|
||||
{
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
@@ -14,5 +12,6 @@ namespace NzbDrone.Core.Test.IndexerTests
|
||||
}
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<PackageReference Include="Dapper" Version="2.0.78" />
|
||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.1.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace NzbDrone.Core.Applications
|
||||
protected readonly IAppIndexerMapService _appIndexerMapService;
|
||||
protected readonly Logger _logger;
|
||||
|
||||
protected static readonly Regex AppIndexerRegex = new Regex(@"(?<indexer>\d*)/api",
|
||||
protected static readonly Regex AppIndexerRegex = new Regex(@"\/(?<indexer>\d.)\/",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public abstract string Name { get; }
|
||||
|
||||
@@ -114,7 +114,12 @@ namespace NzbDrone.Core.Applications
|
||||
var prowlarrMappings = indexerMappings.ToDictionary(i => i.RemoteIndexerId, i => i.IndexerId);
|
||||
|
||||
//Get Dictionary of Remote Indexers point to Prowlarr and what they are mapped to
|
||||
var remoteMappings = app.GetIndexerMappings();
|
||||
var remoteMappings = ExecuteAction(a => a.GetIndexerMappings(), app);
|
||||
|
||||
if (remoteMappings == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//Add mappings if not already in db, these were setup manually in the app or orphaned by a table wipe
|
||||
foreach (var mapping in remoteMappings)
|
||||
@@ -214,5 +219,64 @@ namespace NzbDrone.Core.Applications
|
||||
_logger.Error(ex, "An error occurred while talking to remote application.");
|
||||
}
|
||||
}
|
||||
|
||||
private TResult ExecuteAction<TResult>(Func<IApplication, TResult> applicationAction, IApplication application)
|
||||
{
|
||||
TResult result;
|
||||
|
||||
try
|
||||
{
|
||||
result = applicationAction(application);
|
||||
_applicationStatusService.RecordSuccess(application.Definition.Id);
|
||||
return result;
|
||||
}
|
||||
catch (WebException webException)
|
||||
{
|
||||
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
|
||||
webException.Status == WebExceptionStatus.ConnectFailure)
|
||||
{
|
||||
_applicationStatusService.RecordConnectionFailure(application.Definition.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id);
|
||||
}
|
||||
|
||||
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
|
||||
webException.Message.Contains("timed out"))
|
||||
{
|
||||
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn("{0} {1}", this, webException.Message);
|
||||
}
|
||||
}
|
||||
catch (TooManyRequestsException ex)
|
||||
{
|
||||
if (ex.RetryAfter != TimeSpan.Zero)
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id, ex.RetryAfter);
|
||||
}
|
||||
else
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id, TimeSpan.FromHours(1));
|
||||
}
|
||||
|
||||
_logger.Warn("API Request Limit reached for {0}", this);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id);
|
||||
_logger.Warn("{0} {1}", this, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_applicationStatusService.RecordFailure(application.Definition.Id);
|
||||
_logger.Error(ex, "An error occurred while talking to remote application.");
|
||||
}
|
||||
|
||||
return default(TResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
failures.AddIfNotNull(_lidarrV1Proxy.Test(Settings));
|
||||
var testIndexer = new IndexerDefinition
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
|
||||
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Audio);
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_lidarrV1Proxy.TestConnection(BuildLidarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Lidarr"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s):// and port if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings);
|
||||
void RemoveIndexer(int indexerId, LidarrSettings settings);
|
||||
LidarrIndexer UpdateIndexer(LidarrIndexer indexer, LidarrSettings settings);
|
||||
ValidationFailure Test(LidarrSettings settings);
|
||||
ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings);
|
||||
}
|
||||
|
||||
public class LidarrV1Proxy : ILidarrV1Proxy
|
||||
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
return Execute<LidarrIndexer>(request);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(LidarrSettings settings)
|
||||
public ValidationFailure TestConnection(LidarrIndexer indexer, LidarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
GetStatus(settings);
|
||||
Execute<LidarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Lidarr
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Prowlarr URL is invalid");
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Lidarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("ApiKey", "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
failures.AddIfNotNull(_radarrV3Proxy.Test(Settings));
|
||||
var testIndexer = new IndexerDefinition
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
|
||||
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies);
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_radarrV3Proxy.TestConnection(BuildRadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Radarr"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s):// and port if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")]
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
List<RadarrIndexer> GetIndexerSchema(RadarrSettings settings);
|
||||
void RemoveIndexer(int indexerId, RadarrSettings settings);
|
||||
RadarrIndexer UpdateIndexer(RadarrIndexer indexer, RadarrSettings settings);
|
||||
ValidationFailure Test(RadarrSettings settings);
|
||||
ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings);
|
||||
}
|
||||
|
||||
public class RadarrV3Proxy : IRadarrV3Proxy
|
||||
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
return Execute<RadarrIndexer>(request);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(RadarrSettings settings)
|
||||
public ValidationFailure TestConnection(RadarrIndexer indexer, RadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.POST);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
GetStatus(settings);
|
||||
Execute<RadarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Radarr
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Prowlarr URL is invalid");
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Radarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("ApiKey", "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
failures.AddIfNotNull(_readarrV1Proxy.Test(Settings));
|
||||
var testIndexer = new IndexerDefinition
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
|
||||
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Books);
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_readarrV1Proxy.TestConnection(BuildReadarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Readarr"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s):// and port if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")]
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings);
|
||||
void RemoveIndexer(int indexerId, ReadarrSettings settings);
|
||||
ReadarrIndexer UpdateIndexer(ReadarrIndexer indexer, ReadarrSettings settings);
|
||||
ValidationFailure Test(ReadarrSettings settings);
|
||||
ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings);
|
||||
}
|
||||
|
||||
public class ReadarrV1Proxy : IReadarrV1Proxy
|
||||
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
return Execute<ReadarrIndexer>(request);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(ReadarrSettings settings)
|
||||
public ValidationFailure TestConnection(ReadarrIndexer indexer, ReadarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v1/indexer/test", HttpMethod.POST);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
GetStatus(settings);
|
||||
Execute<ReadarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Readarr
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Prowlarr URL is invalid");
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Readarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("ApiKey", "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
@@ -31,7 +32,25 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
{
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
failures.AddIfNotNull(_sonarrV3Proxy.Test(Settings));
|
||||
var testIndexer = new IndexerDefinition
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Test",
|
||||
Protocol = DownloadProtocol.Usenet,
|
||||
Capabilities = new IndexerCapabilities()
|
||||
};
|
||||
|
||||
testIndexer.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.TV);
|
||||
|
||||
try
|
||||
{
|
||||
failures.AddIfNotNull(_sonarrV3Proxy.TestConnection(BuildSonarrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Sonarr"));
|
||||
}
|
||||
|
||||
return new ValidationResult(failures);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
|
||||
public IEnumerable<int> SyncCategories { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s):// and port if needed")]
|
||||
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s)://, port, and urlbase if needed")]
|
||||
public string ProwlarrUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")]
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings);
|
||||
void RemoveIndexer(int indexerId, SonarrSettings settings);
|
||||
SonarrIndexer UpdateIndexer(SonarrIndexer indexer, SonarrSettings settings);
|
||||
ValidationFailure Test(SonarrSettings settings);
|
||||
ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings);
|
||||
}
|
||||
|
||||
public class SonarrV3Proxy : ISonarrV3Proxy
|
||||
@@ -91,11 +91,15 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
return Execute<SonarrIndexer>(request);
|
||||
}
|
||||
|
||||
public ValidationFailure Test(SonarrSettings settings)
|
||||
public ValidationFailure TestConnection(SonarrIndexer indexer, SonarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.POST);
|
||||
|
||||
request.SetContent(indexer.ToJson());
|
||||
|
||||
try
|
||||
{
|
||||
GetStatus(settings);
|
||||
Execute<SonarrIndexer>(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
@@ -105,8 +109,14 @@ namespace NzbDrone.Core.Applications.Sonarr
|
||||
return new ValidationFailure("ApiKey", "API Key is invalid");
|
||||
}
|
||||
|
||||
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
_logger.Error(ex, "Prowlarr URL is invalid");
|
||||
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Sonarr cannot connect to Prowlarr");
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Unable to send test message");
|
||||
return new ValidationFailure("ApiKey", "Unable to send test message");
|
||||
return new ValidationFailure("BaseUrl", "Unable to complete application test");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace NzbDrone.Core.Configuration
|
||||
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||
|
||||
// TODO: Change back to "master" for the first stable release.
|
||||
public string Branch => GetValue("Branch", "nightly").ToLowerInvariant();
|
||||
public string Branch => GetValue("Branch", "develop").ToLowerInvariant();
|
||||
|
||||
public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant();
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Configuration
|
||||
var releaseInfoPath = Path.Combine(bin, "release_info");
|
||||
|
||||
PackageUpdateMechanism = UpdateMechanism.BuiltIn;
|
||||
DefaultBranch = "nightly";
|
||||
DefaultBranch = "develop";
|
||||
|
||||
if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath))
|
||||
{
|
||||
|
||||
36
src/NzbDrone.Core/Datastore/Converters/CookieConverter.cs
Normal file
36
src/NzbDrone.Core/Datastore/Converters/CookieConverter.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class CookieConverter : SqlMapper.TypeHandler<IDictionary<string, string>>
|
||||
{
|
||||
protected readonly JsonSerializerOptions SerializerSettings;
|
||||
|
||||
public CookieConverter()
|
||||
{
|
||||
var serializerSettings = new JsonSerializerOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
IgnoreNullValues = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
SerializerSettings = serializerSettings;
|
||||
}
|
||||
|
||||
public override void SetValue(IDbDataParameter parameter, IDictionary<string, string> value)
|
||||
{
|
||||
parameter.Value = JsonSerializer.Serialize(value, SerializerSettings);
|
||||
}
|
||||
|
||||
public override IDictionary<string, string> Parse(object value)
|
||||
{
|
||||
return JsonSerializer.Deserialize<Dictionary<string, string>>((string)value, SerializerSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,10 @@ namespace NzbDrone.Core.Datastore
|
||||
|
||||
if (OsInfo.IsOsx)
|
||||
{
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wikijs.servarr.com/prowlarr/faq#i-use-prowlarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName);
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-use-prowlarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName);
|
||||
}
|
||||
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wikijs.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
|
||||
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://wiki.servarr.com/prowlarr/faq#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
|
||||
63
src/NzbDrone.Core/Datastore/Migration/008_redacted_api.cs
Normal file
63
src/NzbDrone.Core/Datastore/Migration/008_redacted_api.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(8)]
|
||||
public class redacted_api : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Execute.WithConnection(MigrateToRedactedApi);
|
||||
}
|
||||
|
||||
private void MigrateToRedactedApi(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tran;
|
||||
cmd.CommandText = "SELECT Id, Settings FROM Indexers WHERE Implementation = 'Redacted'";
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var id = reader.GetInt32(0);
|
||||
var settings = reader.GetString(1);
|
||||
if (!string.IsNullOrWhiteSpace(settings))
|
||||
{
|
||||
var jsonObject = Json.Deserialize<JObject>(settings);
|
||||
|
||||
// Remove username
|
||||
if (jsonObject.ContainsKey("username"))
|
||||
{
|
||||
jsonObject.Remove("username");
|
||||
}
|
||||
|
||||
// Remove password
|
||||
if (jsonObject.ContainsKey("password"))
|
||||
{
|
||||
jsonObject.Remove("password");
|
||||
}
|
||||
|
||||
// write new json back to db, switch to new ConfigContract, and disable the indexer
|
||||
settings = jsonObject.ToJson();
|
||||
using (var updateCmd = conn.CreateCommand())
|
||||
{
|
||||
updateCmd.Transaction = tran;
|
||||
updateCmd.CommandText = "UPDATE Indexers SET Settings = ?, ConfigContract = ?, Enable = 0 WHERE Id = ?";
|
||||
updateCmd.AddParameter(settings);
|
||||
updateCmd.AddParameter("RedactedSettings");
|
||||
updateCmd.AddParameter(id);
|
||||
updateCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Datastore
|
||||
.Ignore(i => i.Description)
|
||||
.Ignore(i => i.Language)
|
||||
.Ignore(i => i.Encoding)
|
||||
.Ignore(i => i.BaseUrl)
|
||||
.Ignore(i => i.IndexerUrls)
|
||||
.Ignore(i => i.Protocol)
|
||||
.Ignore(i => i.Privacy)
|
||||
.Ignore(i => i.SupportsRss)
|
||||
@@ -100,7 +100,7 @@ namespace NzbDrone.Core.Datastore
|
||||
SqlMapper.RemoveTypeMap(typeof(DateTime));
|
||||
SqlMapper.AddTypeHandler(new DapperUtcConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<Dictionary<string, string>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<IDictionary<string, string>>());
|
||||
SqlMapper.AddTypeHandler(new CookieConverter());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<int>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<KeyValuePair<string, int>>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<KeyValuePair<string, int>>());
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
{
|
||||
public class TorrentBlackhole : TorrentClientBase<TorrentBlackholeSettings>
|
||||
{
|
||||
public override bool PreferTorrentFile => true;
|
||||
|
||||
public TorrentBlackhole(ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentLink(ReleaseInfo release, string hash, string torrentLink)
|
||||
{
|
||||
throw new NotImplementedException("Blackhole does not support redirected indexers.");
|
||||
}
|
||||
|
||||
protected override string AddFromMagnetLink(ReleaseInfo release, string hash, string magnetLink)
|
||||
{
|
||||
if (!Settings.SaveMagnetFiles)
|
||||
{
|
||||
throw new NotSupportedException("Blackhole does not support magnet links.");
|
||||
}
|
||||
|
||||
var title = release.Title;
|
||||
|
||||
title = title.CleanFileName();
|
||||
|
||||
var filepath = Path.Combine(Settings.TorrentFolder, $"{title}.{Settings.MagnetFileExtension.Trim('.')}");
|
||||
|
||||
var fileContent = Encoding.UTF8.GetBytes(magnetLink);
|
||||
using (var stream = _diskProvider.OpenWriteStream(filepath))
|
||||
{
|
||||
stream.Write(fileContent, 0, fileContent.Length);
|
||||
}
|
||||
|
||||
_logger.Debug("Saving magnet link succeeded, saved to: {0}", filepath);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(ReleaseInfo release, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
var title = release.Title;
|
||||
|
||||
title = title.CleanFileName();
|
||||
|
||||
var filepath = Path.Combine(Settings.TorrentFolder, string.Format("{0}.torrent", title));
|
||||
|
||||
using (var stream = _diskProvider.OpenWriteStream(filepath))
|
||||
{
|
||||
stream.Write(fileContent, 0, fileContent.Length);
|
||||
}
|
||||
|
||||
_logger.Debug("Torrent Download succeeded, saved to: {0}", filepath);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string Name => "Torrent Blackhole";
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestFolder(Settings.TorrentFolder, "TorrentFolder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.ComponentModel;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
{
|
||||
public class TorrentBlackholeSettingsValidator : AbstractValidator<TorrentBlackholeSettings>
|
||||
{
|
||||
public TorrentBlackholeSettingsValidator()
|
||||
{
|
||||
//Todo: Validate that the path actually exists
|
||||
RuleFor(c => c.TorrentFolder).IsValidPath();
|
||||
RuleFor(c => c.MagnetFileExtension).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class TorrentBlackholeSettings : IProviderConfig
|
||||
{
|
||||
public TorrentBlackholeSettings()
|
||||
{
|
||||
MagnetFileExtension = ".magnet";
|
||||
}
|
||||
|
||||
private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator();
|
||||
|
||||
[FieldDefinition(0, Label = "Torrent Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .torrent file")]
|
||||
public string TorrentFolder { get; set; }
|
||||
|
||||
[DefaultValue(false)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[FieldDefinition(1, Label = "Save Magnet Files", Type = FieldType.Checkbox, HelpText = "Save a .magnet file with the magnet link if no .torrent file is available (only useful if the download client supports .magnet files)")]
|
||||
public bool SaveMagnetFiles { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Save Magnet Files", Type = FieldType.Textbox, HelpText = "Extension to use for magnet links, defaults to '.magnet'")]
|
||||
public string MagnetFileExtension { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
{
|
||||
public class UsenetBlackhole : UsenetClientBase<UsenetBlackholeSettings>
|
||||
{
|
||||
public UsenetBlackhole(IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
Logger logger)
|
||||
: base(httpClient, configService, diskProvider, logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string AddFromLink(ReleaseInfo release)
|
||||
{
|
||||
throw new NotSupportedException("Blackhole does not support redirected indexers.");
|
||||
}
|
||||
|
||||
protected override string AddFromNzbFile(ReleaseInfo release, string filename, byte[] fileContent)
|
||||
{
|
||||
var title = release.Title;
|
||||
|
||||
title = title.CleanFileName();
|
||||
|
||||
var filepath = Path.Combine(Settings.NzbFolder, title + ".nzb");
|
||||
|
||||
using (var stream = _diskProvider.OpenWriteStream(filepath))
|
||||
{
|
||||
stream.Write(fileContent, 0, fileContent.Length);
|
||||
}
|
||||
|
||||
_logger.Debug("NZB Download succeeded, saved to: {0}", filepath);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string Name => "Usenet Blackhole";
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestFolder(Settings.NzbFolder, "NzbFolder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
using NzbDrone.Core.Validation.Paths;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.Blackhole
|
||||
{
|
||||
public class UsenetBlackholeSettingsValidator : AbstractValidator<UsenetBlackholeSettings>
|
||||
{
|
||||
public UsenetBlackholeSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.NzbFolder).IsValidPath();
|
||||
}
|
||||
}
|
||||
|
||||
public class UsenetBlackholeSettings : IProviderConfig
|
||||
{
|
||||
private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator();
|
||||
|
||||
[FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path, HelpText = "Folder in which Prowlarr will store the .nzb file")]
|
||||
public string NzbFolder { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
|
||||
public int Port { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to Sabnzbd")]
|
||||
[FieldDefinition(2, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use secure connection when connecting to NZBGet")]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the nzbget url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
|
||||
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the NZBGet url, e.g. http://[host]:[port]/[urlBase]/jsonrpc")]
|
||||
public string UrlBase { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
|
||||
|
||||
@@ -6,7 +6,6 @@ using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MonoTorrent;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Exceptions
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace NzbDrone.Core.HealthCheck
|
||||
|
||||
private static HttpUri MakeWikiUrl(string fragment)
|
||||
{
|
||||
return new HttpUri("https://wikijs.servarr.com/prowlarr/system#") + new HttpUri(fragment);
|
||||
return new HttpUri("https://wiki.servarr.com/prowlarr/system#") + new HttpUri(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
@@ -16,6 +17,7 @@ namespace NzbDrone.Core.History
|
||||
History MostRecentForIndexer(int indexerId);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
void Cleanup(int days);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
}
|
||||
|
||||
public class HistoryRepository : BasicRepository<History>, IHistoryRepository
|
||||
@@ -87,5 +89,21 @@ namespace NzbDrone.Core.History
|
||||
|
||||
return Query(builder).OrderBy(h => h.Date).ToList();
|
||||
}
|
||||
|
||||
public int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes)
|
||||
{
|
||||
var builder = new SqlBuilder()
|
||||
.SelectCount()
|
||||
.Where<History>(x => x.IndexerId == indexerId)
|
||||
.Where<History>(x => x.Date >= date)
|
||||
.Where<History>(x => eventTypes.Contains(x.EventType));
|
||||
|
||||
var sql = builder.AddPageCountTemplate(typeof(History));
|
||||
|
||||
using (var conn = _database.OpenConnection())
|
||||
{
|
||||
return conn.ExecuteScalar<int>(sql.RawSql, sql.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace NzbDrone.Core.History
|
||||
List<History> GetByIndexerId(int indexerId, HistoryEventType? eventType);
|
||||
void UpdateMany(List<History> toUpdate);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes);
|
||||
}
|
||||
|
||||
public class HistoryService : IHistoryService,
|
||||
@@ -205,5 +206,10 @@ namespace NzbDrone.Core.History
|
||||
{
|
||||
_historyRepository.Purge(vacuum: true);
|
||||
}
|
||||
|
||||
public int CountSince(int indexerId, DateTime date, List<HistoryEventType> eventTypes)
|
||||
{
|
||||
return _historyRepository.CountSince(indexerId, date, eventTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public List<ReleaseInfo> Releases;
|
||||
public List<ReleaseInfo> Releases { get; set; }
|
||||
|
||||
private static string RemoveInvalidXMLChars(string text)
|
||||
{
|
||||
@@ -100,7 +100,8 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
GetNabElement("minimumratio", t.MinimumRatio, protocol),
|
||||
GetNabElement("minimumseedtime", t.MinimumSeedTime, protocol),
|
||||
GetNabElement("downloadvolumefactor", t.DownloadVolumeFactor, protocol),
|
||||
GetNabElement("uploadvolumefactor", t.UploadVolumeFactor, protocol)))));
|
||||
GetNabElement("uploadvolumefactor", t.UploadVolumeFactor, protocol),
|
||||
GetNabElement("coverurl", r.PosterUrl, protocol)))));
|
||||
|
||||
return xdoc.Declaration + Environment.NewLine + xdoc;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Indexers.Events;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
@@ -20,19 +19,19 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
public class NzbSearchService : ISearchForNzb
|
||||
{
|
||||
private readonly IIndexerLimitService _indexerLimitService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly IDownloadMappingService _downloadMappingService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NzbSearchService(IEventAggregator eventAggregator,
|
||||
IIndexerFactory indexerFactory,
|
||||
IDownloadMappingService downloadMappingService,
|
||||
IIndexerLimitService indexerLimitService,
|
||||
Logger logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_indexerFactory = indexerFactory;
|
||||
_downloadMappingService = downloadMappingService;
|
||||
_indexerLimitService = indexerLimitService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -163,16 +162,36 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
|
||||
private async Task<IList<ReleaseInfo>> DispatchIndexer(Func<IIndexer, Task<IndexerPageableQueryResult>> searchAction, IIndexer indexer, SearchCriteriaBase criteriaBase)
|
||||
{
|
||||
if (_indexerLimitService.AtQueryLimit((IndexerDefinition)indexer.Definition))
|
||||
{
|
||||
return new List<ReleaseInfo>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var indexerReports = await searchAction(indexer);
|
||||
|
||||
var releases = indexerReports.Releases;
|
||||
|
||||
//Filter results to only those in searched categories
|
||||
if (criteriaBase.Categories.Length > 0)
|
||||
{
|
||||
var expandedQueryCats = ((IndexerDefinition)indexer.Definition).Capabilities.Categories.ExpandTorznabQueryCategories(criteriaBase.Categories);
|
||||
|
||||
releases = releases.Where(result => result.Categories?.Any() != true || expandedQueryCats.Intersect(result.Categories.Select(c => c.Id)).Any()).ToList();
|
||||
|
||||
if (releases.Count != indexerReports.Releases.Count)
|
||||
{
|
||||
_logger.Trace("{0} {1} Releases which didn't contain search categories [{2}] were filtered", indexerReports.Releases.Count - releases.Count, indexer.Name, string.Join(", ", expandedQueryCats));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var query in indexerReports.Queries)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new IndexerQueryEvent(indexer.Definition.Id, criteriaBase, query.ElapsedTime, query.StatusCode == 200, query.Releases.Count()));
|
||||
}
|
||||
|
||||
return indexerReports.Releases;
|
||||
return releases;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
@@ -24,7 +25,17 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
public class IndexerDefinitionUpdateService : IIndexerDefinitionUpdateService, IExecute<IndexerDefinitionUpdateCommand>
|
||||
{
|
||||
private const int DEFINITION_VERSION = 1;
|
||||
private readonly List<string> _defintionBlacklist = new List<string>() { "aither", "animeworld", "blutopia", "beyond-hd", "beyond-hd-oneurl", "hdbits" };
|
||||
private readonly List<string> _defintionBlacklist = new List<string>()
|
||||
{
|
||||
"aither",
|
||||
"animeworld",
|
||||
"blutopia",
|
||||
"beyond-hd",
|
||||
"beyond-hd-oneurl",
|
||||
"danishbytes",
|
||||
"hdbits",
|
||||
"shareisland"
|
||||
};
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IAppFolderInfo _appFolderInfo;
|
||||
@@ -59,6 +70,40 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
var request = new HttpRequest($"https://indexers.prowlarr.com/master/{DEFINITION_VERSION}");
|
||||
var response = _httpClient.Get<List<CardigannMetaDefinition>>(request);
|
||||
indexerList = response.Resource.Where(i => !_defintionBlacklist.Contains(i.File)).ToList();
|
||||
|
||||
var definitionFolder = Path.Combine(_appFolderInfo.AppDataFolder, "Definitions", "Custom");
|
||||
|
||||
var directoryInfo = new DirectoryInfo(definitionFolder);
|
||||
|
||||
if (directoryInfo.Exists)
|
||||
{
|
||||
var files = directoryInfo.GetFiles($"*.yml");
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
_logger.Debug("Loading Custom Cardigann definition " + file.FullName);
|
||||
|
||||
try
|
||||
{
|
||||
var definitionString = File.ReadAllText(file.FullName);
|
||||
var definition = _deserializer.Deserialize<CardigannMetaDefinition>(definitionString);
|
||||
|
||||
definition.File = Path.GetFileNameWithoutExtension(file.Name);
|
||||
|
||||
if (indexerList.Any(i => i.File == definition.File || i.Name == definition.Name))
|
||||
{
|
||||
_logger.Warn("Custom Cardigann definition {0} does not have unique file name or Indexer name", file.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
indexerList.Add(definition);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Error while parsing custom Cardigann definition {file.FullName}\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -108,7 +153,7 @@ namespace NzbDrone.Core.IndexerVersions
|
||||
|
||||
if (directoryInfo.Exists)
|
||||
{
|
||||
var files = directoryInfo.GetFiles($"{fileKey}.yml");
|
||||
var files = directoryInfo.GetFiles($"{fileKey}.yml", SearchOption.AllDirectories);
|
||||
|
||||
if (files.Any())
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class Aither : Unit3dBase
|
||||
{
|
||||
public override string Name => "Aither";
|
||||
public override string BaseUrl => "https://aither.cc/";
|
||||
public override string[] IndexerUrls => new string[] { "https://aither.cc/" };
|
||||
public override string Description => "Aither is a Private Torrent Tracker for HD MOVIES / TV";
|
||||
public override string Language => "en-us";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class AlphaRatio : Gazelle.Gazelle
|
||||
{
|
||||
public override string Name => "AlphaRatio";
|
||||
public override string BaseUrl => "https://alpharatio.cc/";
|
||||
public override string[] IndexerUrls => new string[] { "https://alpharatio.cc/" };
|
||||
public override string Description => "AlphaRatio(AR) is a Private Torrent Tracker for 0DAY / GENERAL";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
@@ -25,8 +25,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Settings = Settings,
|
||||
HttpClient = _httpClient,
|
||||
Logger = _logger,
|
||||
Capabilities = Capabilities,
|
||||
BaseUrl = BaseUrl
|
||||
Capabilities = Capabilities
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +39,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q, MovieSearchParam.ImdbId
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
550
src/NzbDrone.Core/Indexers/Definitions/Anidub.cs
Normal file
550
src/NzbDrone.Core/Indexers/Definitions/Anidub.cs
Normal file
@@ -0,0 +1,550 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Anidub : TorrentIndexerBase<AnidubSettings>
|
||||
{
|
||||
public override string Name => "Anidub";
|
||||
public override string[] IndexerUrls => new string[] { "https://tr.anidub.com/" };
|
||||
public override string Description => "Anidub is russian anime voiceover group and eponymous anime tracker.";
|
||||
public override string Language => "ru-ru";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.SemiPublic;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Anidub(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnidubRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnidubParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
UpdateCookies(null, null);
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl + "index.php")
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
};
|
||||
|
||||
var mainPage = await _httpClient.ExecuteAsync(new HttpRequest(Settings.BaseUrl));
|
||||
|
||||
requestBuilder.Method = Common.Http.HttpMethod.POST;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
requestBuilder.SetCookies(mainPage.GetCookies());
|
||||
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("login_name", Settings.Username)
|
||||
.AddFormParameter("login_password", Settings.Password)
|
||||
.AddFormParameter("login", "submit")
|
||||
.SetHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.Build();
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (response.Content != null && !CheckIfLoginNeeded(response))
|
||||
{
|
||||
UpdateCookies(response.GetCookies(), DateTime.Now + TimeSpan.FromDays(30));
|
||||
_logger.Debug("Anidub authentication succeeded");
|
||||
}
|
||||
else
|
||||
{
|
||||
const string ErrorSelector = "#content .berror .berror_c";
|
||||
var parser = new HtmlParser();
|
||||
var document = await parser.ParseDocumentAsync(response.Content);
|
||||
var errorMessage = document.QuerySelector(ErrorSelector).TextContent.Trim();
|
||||
throw new IndexerAuthException("Anidub authentication failed. Error: " + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (httpResponse.Content.Contains("index.php?action=logout"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
BookSearchParams = new List<BookSearchParam>
|
||||
{
|
||||
BookSearchParam.Q
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "Аниме TV");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.Movies, "Аниме Фильмы");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVAnime, "Аниме OVA");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVAnime, "Аниме OVA |- Аниме ONA");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TV, "Дорамы / Японские Сериалы и Фильмы");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.TV, "Дорамы / Корейские Сериалы и Фильмы");
|
||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.TV, "Дорамы / Китайские Сериалы и Фильмы");
|
||||
caps.Categories.AddCategoryMapping(9, NewznabStandardCategory.TV, "Дорамы");
|
||||
caps.Categories.AddCategoryMapping(10, NewznabStandardCategory.TVAnime, "Аниме TV / Аниме Ongoing");
|
||||
caps.Categories.AddCategoryMapping(11, NewznabStandardCategory.TVAnime, "Аниме TV / Многосерийный сёнэн");
|
||||
caps.Categories.AddCategoryMapping(12, NewznabStandardCategory.Other, "Аниме Ongoing Анонсы");
|
||||
caps.Categories.AddCategoryMapping(13, NewznabStandardCategory.XXX, "18+");
|
||||
caps.Categories.AddCategoryMapping(14, NewznabStandardCategory.TVAnime, "Аниме TV / Законченные сериалы");
|
||||
caps.Categories.AddCategoryMapping(15, NewznabStandardCategory.BooksComics, "Манга");
|
||||
caps.Categories.AddCategoryMapping(16, NewznabStandardCategory.Audio, "OST");
|
||||
caps.Categories.AddCategoryMapping(17, NewznabStandardCategory.Audio, "Подкасты");
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnidubRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public AnidubSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public AnidubRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var requestUrl = string.Empty;
|
||||
var isSearch = !string.IsNullOrWhiteSpace(term);
|
||||
|
||||
if (isSearch)
|
||||
{
|
||||
requestUrl = string.Format("{0}/index.php?do=search", Settings.BaseUrl.TrimEnd('/'));
|
||||
}
|
||||
else
|
||||
{
|
||||
requestUrl = Settings.BaseUrl;
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
|
||||
|
||||
if (isSearch)
|
||||
{
|
||||
request.HttpRequest.Method = NzbDrone.Common.Http.HttpMethod.POST;
|
||||
var postData = new NameValueCollection
|
||||
{
|
||||
{ "do", "search" },
|
||||
{ "subaction", "search" },
|
||||
{ "search_start", "1" },
|
||||
{ "full_search", "1" },
|
||||
{ "result_from", "1" },
|
||||
|
||||
// Remove season and episode info from search term cause it breaks search
|
||||
{ "story", Regex.Replace(term, @"(?:[SsEe]?\d{1,4}){1,2}$", "").TrimEnd() },
|
||||
{ "titleonly", "3" },
|
||||
{ "searchuser", "" },
|
||||
{ "replyless", "0" },
|
||||
{ "replylimit", "0" },
|
||||
{ "searchdate", "0" },
|
||||
{ "beforeafter", "after" },
|
||||
{ "sortby", "" },
|
||||
{ "resorder", "desc" },
|
||||
{ "showposts", "1" },
|
||||
{ "catlist[]", "0" }
|
||||
};
|
||||
var headers = new NameValueCollection
|
||||
{
|
||||
{ "Content-Type", "application/x-www-form-urlencoded" }
|
||||
};
|
||||
|
||||
request.HttpRequest.SetContent(postData.GetQueryString());
|
||||
request.HttpRequest.Headers.Add(headers);
|
||||
}
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnidubParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly AnidubSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
public IHttpClient HttpClient { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
|
||||
private static Dictionary<string, string> CategoriesMap => new Dictionary<string, string>
|
||||
{
|
||||
{ "/anime_tv/full", "14" },
|
||||
{ "/anime_tv/anime_ongoing", "10" },
|
||||
{ "/anime_tv/shonen", "11" },
|
||||
{ "/anime_tv", "2" },
|
||||
{ "/xxx", "13" },
|
||||
{ "/manga", "15" },
|
||||
{ "/ost", "16" },
|
||||
{ "/podcast", "17" },
|
||||
{ "/anime_movie", "3" },
|
||||
{ "/anime_ova/anime_ona", "5" },
|
||||
{ "/anime_ova", "4" },
|
||||
{ "/dorama/japan_dorama", "6" },
|
||||
{ "/dorama/korea_dorama", "7" },
|
||||
{ "/dorama/china_dorama", "8" },
|
||||
{ "/dorama", "9" },
|
||||
{ "/anons_ongoing", "12" }
|
||||
};
|
||||
|
||||
public AnidubParser(AnidubSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
private static string GetTitle(AngleSharp.Html.Dom.IHtmlDocument content, AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
var domTitle = content.QuerySelector("#news-title");
|
||||
var baseTitle = domTitle.TextContent.Trim();
|
||||
var quality = GetQuality(tabNode.ParentElement);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(quality))
|
||||
{
|
||||
return $"{baseTitle} [{quality}]";
|
||||
}
|
||||
|
||||
return baseTitle;
|
||||
}
|
||||
|
||||
private static string GetQuality(AngleSharp.Dom.IElement releaseNode)
|
||||
{
|
||||
// For some releases there's no block with quality
|
||||
if (string.IsNullOrWhiteSpace(releaseNode.Id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var quality = releaseNode.Id.Trim();
|
||||
switch (quality.ToLowerInvariant())
|
||||
{
|
||||
case "tv720":
|
||||
return "HDTV 720p";
|
||||
case "tv1080":
|
||||
return "HDTV 1080p";
|
||||
case "bd720":
|
||||
return "BDRip 720p";
|
||||
case "bd1080":
|
||||
return "BDRip 1080p";
|
||||
case "hwp":
|
||||
return "SDTV";
|
||||
default:
|
||||
return quality.ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetReleaseLeechers(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string LeechersSelector = ".list.down > .li_swing_m";
|
||||
|
||||
var leechersStr = tabNode.QuerySelector(LeechersSelector).TextContent;
|
||||
int.TryParse(leechersStr, out var leechers);
|
||||
return leechers;
|
||||
}
|
||||
|
||||
private static int GetReleaseSeeders(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string SeedersSelector = ".list.down > .li_distribute_m";
|
||||
|
||||
var seedersStr = tabNode.QuerySelector(SeedersSelector).TextContent;
|
||||
int.TryParse(seedersStr, out var seeders);
|
||||
return seeders;
|
||||
}
|
||||
|
||||
private static int GetReleaseGrabs(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string GrabsSelector = ".list.down > .li_download_m";
|
||||
|
||||
var grabsStr = tabNode.QuerySelector(GrabsSelector).TextContent;
|
||||
int.TryParse(grabsStr, out var grabs);
|
||||
return grabs;
|
||||
}
|
||||
|
||||
private static string GetDateFromDocument(AngleSharp.Html.Dom.IHtmlDocument content)
|
||||
{
|
||||
const string DateSelector = ".story_inf > li:nth-child(2)";
|
||||
|
||||
var domDate = content.QuerySelector(DateSelector).LastChild;
|
||||
|
||||
if (domDate?.NodeName != "#text")
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return domDate.NodeValue.Trim();
|
||||
}
|
||||
|
||||
private DateTime GetDateFromShowPage(AngleSharp.Html.Dom.IHtmlDocument content)
|
||||
{
|
||||
const string dateFormat = "d-MM-yyyy";
|
||||
const string dateTimeFormat = dateFormat + ", HH:mm";
|
||||
|
||||
// Would be better to use AssumeLocal and provide "ru-RU" culture,
|
||||
// but doesn't work cross-platform
|
||||
const DateTimeStyles style = DateTimeStyles.AssumeUniversal;
|
||||
|
||||
var culture = CultureInfo.InvariantCulture;
|
||||
|
||||
var dateText = GetDateFromDocument(content);
|
||||
|
||||
//Correct way but will not always work on cross-platform
|
||||
//var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
|
||||
//var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, localTimeZone);
|
||||
|
||||
// Russian Standard Time is +03:00, no DST
|
||||
const int russianStandardTimeDiff = 3;
|
||||
var nowLocal = DateTime.UtcNow.AddHours(russianStandardTimeDiff);
|
||||
|
||||
dateText = dateText
|
||||
.Replace("Вчера", nowLocal.AddDays(-1).ToString(dateFormat))
|
||||
.Replace("Сегодня", nowLocal.ToString(dateFormat));
|
||||
|
||||
if (DateTime.TryParseExact(dateText, dateTimeFormat, culture, style, out var date))
|
||||
{
|
||||
var utcDate = date.ToUniversalTime();
|
||||
return utcDate.AddHours(-russianStandardTimeDiff);
|
||||
}
|
||||
|
||||
Logger.Warn($"[AniDub] Date time couldn't be parsed on. Date text: {dateText}");
|
||||
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private static long GetReleaseSize(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
const string SizeSelector = ".list.down > .red";
|
||||
|
||||
var sizeStr = tabNode.QuerySelector(SizeSelector).TextContent;
|
||||
return ReleaseInfo.GetBytes(sizeStr);
|
||||
}
|
||||
|
||||
private string GetReleaseLink(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
return $"{_settings.BaseUrl}engine/download.php?id={GetTorrentId(tabNode)}";
|
||||
}
|
||||
|
||||
private static string GetTorrentId(AngleSharp.Dom.IElement tabNode)
|
||||
{
|
||||
var nodeId = tabNode.Id;
|
||||
|
||||
// Format is "torrent_{id}_info"
|
||||
return nodeId
|
||||
.Replace("torrent_", string.Empty)
|
||||
.Replace("_info", string.Empty);
|
||||
}
|
||||
|
||||
private ICollection<IndexerCategory> ParseCategories(string uriPath)
|
||||
{
|
||||
var categoriesMap = CategoriesMap;
|
||||
|
||||
return categoriesMap
|
||||
.Where(categoryMap => uriPath.StartsWith(categoryMap.Key))
|
||||
.Select(categoryMap => _categories.MapTrackerCatToNewznab(categoryMap.Value))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private IList<TorrentInfo> ParseRelease(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
foreach (var t in dom.QuerySelectorAll("#tabs .torrent_c > div"))
|
||||
{
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = GetTitle(dom, t),
|
||||
InfoUrl = indexerResponse.Request.Url.ToString(),
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
|
||||
Guid = indexerResponse.Request.Url.ToString() + t.Id,
|
||||
Seeders = GetReleaseSeeders(t),
|
||||
Peers = GetReleaseSeeders(t) + GetReleaseLeechers(t),
|
||||
Grabs = GetReleaseGrabs(t),
|
||||
Categories = ParseCategories(indexerResponse.Request.Url.Path),
|
||||
PublishDate = GetDateFromShowPage(dom),
|
||||
DownloadUrl = GetReleaseLink(t),
|
||||
Size = GetReleaseSize(t),
|
||||
Resolution = GetQuality(t)
|
||||
};
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
|
||||
return torrentInfos;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var domQuery = string.Empty;
|
||||
|
||||
if (indexerResponse.Request.Url.Query.Contains("do=search"))
|
||||
{
|
||||
domQuery = ".searchitem > h3 > a";
|
||||
}
|
||||
else
|
||||
{
|
||||
domQuery = "#dle-content > .story > .story_h > .lcol > h2 > a";
|
||||
}
|
||||
|
||||
var links = dom.QuerySelectorAll(domQuery);
|
||||
foreach (var link in links)
|
||||
{
|
||||
var url = link.GetAttribute("href");
|
||||
|
||||
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
|
||||
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
{
|
||||
if ((int)releaseResponse.HttpResponse.StatusCode == 429)
|
||||
{
|
||||
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
torrentInfos.AddRange(ParseRelease(releaseResponse));
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnidubSettingsValidator : AbstractValidator<AnidubSettings>
|
||||
{
|
||||
public AnidubSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class AnidubSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AnidubSettingsValidator Validator = new AnidubSettingsValidator();
|
||||
|
||||
public AnidubSettings()
|
||||
{
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/NzbDrone.Core/Indexers/Definitions/Anilibria.cs
Normal file
294
src/NzbDrone.Core/Indexers/Definitions/Anilibria.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Anilibria : TorrentIndexerBase<AnilibriaSettings>
|
||||
{
|
||||
public override string Name => "Anilibria";
|
||||
public override string[] IndexerUrls => new string[] { "https://anilibria.tv/" };
|
||||
public override string Description => "Anilibria is russian anime voiceover group and eponymous anime tracker.";
|
||||
public override string Language => "ru-ru";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Anilibria(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnilibriaRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnilibriaParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "ТВ");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TVAnime, "ONA");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.TVAnime, "OVA");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "Фильм");
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnilibriaRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public AnilibriaSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public AnilibriaRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var apiUrl = Regex.Replace(Settings.BaseUrl, @"(https?:\/\/)(.*)", "$1api.$2v2");
|
||||
var queryCollection = new NameValueCollection
|
||||
{
|
||||
{ "limit", "100" },
|
||||
{ "filter", "names,code,torrents.list,season.year,type.string" }
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
apiUrl += "/getUpdates?" + queryCollection.GetQueryString();
|
||||
}
|
||||
else
|
||||
{
|
||||
apiUrl += "/searchTitles?" + queryCollection.GetQueryString() + "&search=" + Uri.EscapeDataString(term);
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(apiUrl, HttpAccept.Json);
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
// Anilibria doesn't support music, but this function required by interface
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
// Anilibria doesn't support books, but this function required by interface
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly AnilibriaSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public AnilibriaParser(AnilibriaSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
private string composeTitle(AnilibriaTitle tl, AnilibriaTorrent tr)
|
||||
{
|
||||
var title = tl.Names.Ru;
|
||||
title += " / " + tl.Names.En;
|
||||
if (tl.Names.Alternative is string)
|
||||
{
|
||||
title += " / " + tl.Names.Alternative;
|
||||
}
|
||||
|
||||
title += " " + tl.Season.Year;
|
||||
title += " [" + tr.Quality.String + "]";
|
||||
if (!string.IsNullOrWhiteSpace(tr.Series.String))
|
||||
{
|
||||
title += " - E" + tr.Series.String;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
var queryResponseItems = JsonConvert.DeserializeObject<List<AnilibriaTitle>>(indexerResponse.Content);
|
||||
|
||||
foreach (var tl in queryResponseItems)
|
||||
{
|
||||
foreach (var tr in tl.Torrents.List)
|
||||
{
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Title = composeTitle(tl, tr),
|
||||
InfoUrl = string.Format("{0}/release/{1}.html", _settings.BaseUrl.TrimEnd('/'), tl.Code),
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
Seeders = tr.Seeders,
|
||||
Peers = tr.Leechers + tr.Seeders,
|
||||
Grabs = tr.Downloads,
|
||||
Categories = _categories.MapTrackerCatDescToNewznab(tl.Type.String),
|
||||
|
||||
// API provides timestamp in UTC+3 timezone, so we need to substract 3 hours
|
||||
PublishDate = DateTimeUtil.UnixTimestampToDateTime(tr.UploadedTimestamp).AddHours(-3),
|
||||
Guid = _settings.BaseUrl + tr.Url,
|
||||
DownloadUrl = _settings.BaseUrl + tr.Url,
|
||||
Size = tr.TotalSize,
|
||||
Resolution = tr.Quality.Resolution,
|
||||
Codec = tr.Quality.Encoder
|
||||
};
|
||||
|
||||
torrentInfos.Add(torrentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaSettingsValidator : AbstractValidator<AnilibriaSettings>
|
||||
{
|
||||
public AnilibriaSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class AnilibriaSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AnilibriaSettingsValidator Validator = new AnilibriaSettingsValidator();
|
||||
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
|
||||
public class AnilibriaTitle
|
||||
{
|
||||
public AnilibriaNames Names { get; set; }
|
||||
public string Code { get; set; }
|
||||
public AnilibriaTorrents Torrents { get; set; }
|
||||
public AnilibriaSeason Season { get; set; }
|
||||
public AnilibriaTitleType Type { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaTitleType
|
||||
{
|
||||
public string String { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaNames
|
||||
{
|
||||
public string Ru { get; set; }
|
||||
public string En { get; set; }
|
||||
public object Alternative { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaSeason
|
||||
{
|
||||
public long Year { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaTorrents
|
||||
{
|
||||
public AnilibriaTorrent[] List { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaTorrent
|
||||
{
|
||||
public AnilibriaSeries Series { get; set; }
|
||||
public AnilibriaQuality Quality { get; set; }
|
||||
public int Leechers { get; set; }
|
||||
public int Seeders { get; set; }
|
||||
public int Downloads { get; set; }
|
||||
|
||||
[JsonProperty("total_size")]
|
||||
public long TotalSize { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("uploaded_timestamp")]
|
||||
public long UploadedTimestamp { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaQuality
|
||||
{
|
||||
public string String { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Resolution { get; set; }
|
||||
public string Encoder { get; set; }
|
||||
}
|
||||
|
||||
public class AnilibriaSeries
|
||||
{
|
||||
public long First { get; set; }
|
||||
public long Last { get; set; }
|
||||
public string String { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
@@ -18,7 +19,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
@@ -26,8 +26,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class AnimeBytes : TorrentIndexerBase<AnimeBytesSettings>
|
||||
{
|
||||
public override string Name => "AnimeBytes";
|
||||
public override string BaseUrl => "https://animebytes.tv/";
|
||||
public override string Description => "Powered by Tentacles";
|
||||
public override string[] IndexerUrls => new string[] { "https://animebytes.tv/" };
|
||||
public override string Description => "AnimeBytes (AB) is the largest private torrent tracker that specialises in anime and anime-related content.";
|
||||
public override string Language => "en-us";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
@@ -41,12 +41,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnimeBytesRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
|
||||
return new AnimeBytesRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnimeBytesParser(Settings, Capabilities.Categories, BaseUrl);
|
||||
return new AnimeBytesParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
@@ -101,7 +101,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public AnimeBytesSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
public AnimeBytesRequestGenerator()
|
||||
{
|
||||
@@ -109,14 +108,14 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string searchType, string term, int[] categories)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/scrape.php", BaseUrl.TrimEnd('/'));
|
||||
var searchUrl = string.Format("{0}/scrape.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
|
||||
var queryCollection = new NameValueCollection
|
||||
{
|
||||
{ "username", Settings.Username },
|
||||
{ "torrent_pass", Settings.Passkey },
|
||||
{ "type", searchType },
|
||||
{ "searchstr", term }
|
||||
{ "searchstr", StripEpisodeNumber(term) }
|
||||
};
|
||||
|
||||
var queryCats = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
@@ -183,19 +182,26 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
|
||||
private string StripEpisodeNumber(string term)
|
||||
{
|
||||
// Tracer does not support searching with episode number so strip it if we have one
|
||||
term = Regex.Replace(term, @"\W(\dx)?\d?\d$", string.Empty);
|
||||
term = Regex.Replace(term, @"\W(S\d\d?E)?\d?\d$", string.Empty);
|
||||
term = Regex.Replace(term, @"\W\d+$", string.Empty);
|
||||
return term;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimeBytesParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly AnimeBytesSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public AnimeBytesParser(AnimeBytesSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
|
||||
public AnimeBytesParser(AnimeBytesSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
@@ -212,29 +218,17 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
//TODO: Create API Resource Type
|
||||
var json = JsonConvert.DeserializeObject<dynamic>(indexerResponse.Content);
|
||||
var response = JsonConvert.DeserializeObject<AnimeBytesResponse>(indexerResponse.Content);
|
||||
|
||||
if (json["error"] != null)
|
||||
if (response.Matches > 0)
|
||||
{
|
||||
throw new Exception(json["error"].ToString());
|
||||
}
|
||||
|
||||
var matches = (long)json["Matches"];
|
||||
|
||||
if (matches > 0)
|
||||
{
|
||||
var groups = (JArray)json.Groups;
|
||||
|
||||
foreach (var group in groups)
|
||||
foreach (var group in response.Groups)
|
||||
{
|
||||
var synonyms = new List<string>();
|
||||
var posterStr = (string)group["Image"];
|
||||
var poster = string.IsNullOrWhiteSpace(posterStr) ? null : new Uri(posterStr);
|
||||
var year = (int)group["Year"];
|
||||
var groupName = (string)group["GroupName"];
|
||||
var seriesName = (string)group["SeriesName"];
|
||||
var mainTitle = WebUtility.HtmlDecode((string)group["FullName"]);
|
||||
var year = group.Year;
|
||||
var groupName = group.GroupName;
|
||||
var seriesName = group.SeriesName;
|
||||
var mainTitle = WebUtility.HtmlDecode(group.FullName);
|
||||
if (seriesName != null)
|
||||
{
|
||||
mainTitle = seriesName;
|
||||
@@ -242,105 +236,111 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
synonyms.Add(mainTitle);
|
||||
|
||||
// TODO: Do we need all these options?
|
||||
//if (group["Synonymns"].HasValues)
|
||||
//{
|
||||
// if (group["Synonymns"] is JArray)
|
||||
// {
|
||||
// var allSyonyms = group["Synonymns"].ToObject<List<string>>();
|
||||
|
||||
// if (AddJapaneseTitle && allSyonyms.Count >= 1)
|
||||
// synonyms.Add(allSyonyms[0]);
|
||||
// if (AddRomajiTitle && allSyonyms.Count >= 2)
|
||||
// synonyms.Add(allSyonyms[1]);
|
||||
// if (AddAlternativeTitles && allSyonyms.Count >= 3)
|
||||
// synonyms.AddRange(allSyonyms[2].Split(',').Select(t => t.Trim()));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var allSynonyms = group["Synonymns"].ToObject<Dictionary<int, string>>();
|
||||
|
||||
// if (AddJapaneseTitle && allSynonyms.ContainsKey(0))
|
||||
// synonyms.Add(allSynonyms[0]);
|
||||
// if (AddRomajiTitle && allSynonyms.ContainsKey(1))
|
||||
// synonyms.Add(allSynonyms[1]);
|
||||
// if (AddAlternativeTitles && allSynonyms.ContainsKey(2))
|
||||
// {
|
||||
// synonyms.AddRange(allSynonyms[2].Split(',').Select(t => t.Trim()));
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
List<IndexerCategory> category = null;
|
||||
var categoryName = (string)group["CategoryName"];
|
||||
|
||||
var description = (string)group["Description"];
|
||||
|
||||
foreach (var torrent in group["Torrents"])
|
||||
if (group.Synonymns.StringArray != null)
|
||||
{
|
||||
var releaseInfo = "S01";
|
||||
string episode = null;
|
||||
synonyms.AddRange(group.Synonymns.StringArray);
|
||||
}
|
||||
else
|
||||
{
|
||||
synonyms.AddRange(group.Synonymns.StringMap.Values);
|
||||
}
|
||||
|
||||
List<IndexerCategory> category = null;
|
||||
var categoryName = group.CategoryName;
|
||||
|
||||
var description = group.Description;
|
||||
|
||||
foreach (var torrent in group.Torrents)
|
||||
{
|
||||
var releaseInfo = _settings.EnableSonarrCompatibility ? "S01" : "";
|
||||
int? episode = null;
|
||||
int? season = null;
|
||||
var editionTitle = (string)torrent["EditionData"]["EditionTitle"];
|
||||
var editionTitle = torrent.EditionData.EditionTitle;
|
||||
if (!string.IsNullOrWhiteSpace(editionTitle))
|
||||
{
|
||||
releaseInfo = WebUtility.HtmlDecode(editionTitle);
|
||||
|
||||
if (_settings.EnableSonarrCompatibility)
|
||||
{
|
||||
var simpleSeasonRegEx = new Regex(@"Season (\d+)", RegexOptions.Compiled);
|
||||
var simpleSeasonRegExMatch = simpleSeasonRegEx.Match(releaseInfo);
|
||||
if (simpleSeasonRegExMatch.Success)
|
||||
{
|
||||
season = ParseUtil.CoerceInt(simpleSeasonRegExMatch.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
var episodeRegEx = new Regex(@"Episode (\d+)", RegexOptions.Compiled);
|
||||
var episodeRegExMatch = episodeRegEx.Match(releaseInfo);
|
||||
if (episodeRegExMatch.Success)
|
||||
{
|
||||
episode = ParseUtil.CoerceInt(episodeRegExMatch.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
var seasonRegEx = new Regex(@"Season (\d+)", RegexOptions.Compiled);
|
||||
var seasonRegExMatch = seasonRegEx.Match(releaseInfo);
|
||||
if (seasonRegExMatch.Success)
|
||||
if (_settings.EnableSonarrCompatibility)
|
||||
{
|
||||
season = ParseUtil.CoerceInt(seasonRegExMatch.Groups[1].Value);
|
||||
var advancedSeasonRegEx = new Regex(@"(\d+)(st|nd|rd|th) Season", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
var advancedSeasonRegExMatch = advancedSeasonRegEx.Match(mainTitle);
|
||||
if (advancedSeasonRegExMatch.Success)
|
||||
{
|
||||
season = ParseUtil.CoerceInt(advancedSeasonRegExMatch.Groups[1].Value);
|
||||
}
|
||||
|
||||
var seasonCharactersRegEx = new Regex(@"(I{2,})$", RegexOptions.Compiled);
|
||||
var seasonCharactersRegExMatch = seasonCharactersRegEx.Match(mainTitle);
|
||||
if (seasonCharactersRegExMatch.Success)
|
||||
{
|
||||
season = seasonCharactersRegExMatch.Groups[1].Value.Length;
|
||||
}
|
||||
|
||||
var seasonNumberRegEx = new Regex(@"([2-9])$", RegexOptions.Compiled);
|
||||
var seasonNumberRegExMatch = seasonNumberRegEx.Match(mainTitle);
|
||||
if (seasonNumberRegExMatch.Success)
|
||||
{
|
||||
season = ParseUtil.CoerceInt(seasonNumberRegExMatch.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
var episodeRegEx = new Regex(@"Episode (\d+)", RegexOptions.Compiled);
|
||||
var episodeRegExMatch = episodeRegEx.Match(releaseInfo);
|
||||
if (episodeRegExMatch.Success)
|
||||
if (episode != null)
|
||||
{
|
||||
episode = episodeRegExMatch.Groups[1].Value;
|
||||
releaseInfo = episode is > 0 and < 10
|
||||
? "0" + episode
|
||||
: episode.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (season != null && _settings.EnableSonarrCompatibility)
|
||||
{
|
||||
releaseInfo = $"S{season}";
|
||||
}
|
||||
}
|
||||
|
||||
releaseInfo = releaseInfo.Replace("Episode ", "");
|
||||
releaseInfo = releaseInfo.Replace("Season ", "S");
|
||||
releaseInfo = releaseInfo.Trim();
|
||||
|
||||
//if (PadEpisode && int.TryParse(releaseInfo, out _) && releaseInfo.Length == 1)
|
||||
//{
|
||||
// releaseInfo = "0" + releaseInfo;
|
||||
//}
|
||||
|
||||
//if (FilterSeasonEpisode)
|
||||
//{
|
||||
// if (query.Season != 0 && season != null && season != query.Season) // skip if season doesn't match
|
||||
// continue;
|
||||
// if (query.Episode != null && episode != null && episode != query.Episode) // skip if episode doesn't match
|
||||
// continue;
|
||||
//}
|
||||
var torrentId = (long)torrent["ID"];
|
||||
var property = ((string)torrent["Property"]).Replace(" | Freeleech", "");
|
||||
var link = (string)torrent["Link"];
|
||||
var linkUri = new Uri(link);
|
||||
var uploadTimeString = (string)torrent["UploadTime"];
|
||||
var uploadTime = DateTime.ParseExact(uploadTimeString, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
var publishDate = DateTime.SpecifyKind(uploadTime, DateTimeKind.Utc).ToLocalTime();
|
||||
var details = new Uri(_baseUrl + "torrent/" + torrentId + "/group");
|
||||
var size = (long)torrent["Size"];
|
||||
var snatched = (int)torrent["Snatched"];
|
||||
var seeders = (int)torrent["Seeders"];
|
||||
var leechers = (int)torrent["Leechers"];
|
||||
var fileCount = (int)torrent["FileCount"];
|
||||
var torrentId = torrent.Id;
|
||||
var property = torrent.Property.Replace(" | Freeleech", string.Empty);
|
||||
var link = torrent.Link;
|
||||
var uploadTime = torrent.UploadTime;
|
||||
var publishDate = DateTime.SpecifyKind(uploadTime.DateTime, DateTimeKind.Utc).ToLocalTime();
|
||||
var details = new Uri(_settings.BaseUrl + "torrent/" + torrentId + "/group");
|
||||
var size = torrent.Size;
|
||||
var snatched = torrent.Snatched;
|
||||
var seeders = torrent.Seeders;
|
||||
var leechers = torrent.Leechers;
|
||||
var fileCount = torrent.FileCount;
|
||||
var peers = seeders + leechers;
|
||||
|
||||
var rawDownMultiplier = (int?)torrent["RawDownMultiplier"] ?? 0;
|
||||
var rawUpMultiplier = (int?)torrent["RawUpMultiplier"] ?? 0;
|
||||
var rawDownMultiplier = torrent.RawDownMultiplier;
|
||||
var rawUpMultiplier = torrent.RawUpMultiplier;
|
||||
|
||||
// Ignore these categories as they'll cause hell with the matcher
|
||||
// TV Special, ONA, DVD Special, BD Special
|
||||
if (groupName == "TV Series" || groupName == "OVA")
|
||||
{
|
||||
category = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
|
||||
}
|
||||
|
||||
// Ignore these categories as they'll cause hell with the matcher
|
||||
// TV Special, OVA, ONA, DVD Special, BD Special
|
||||
if (groupName == "Movie" || groupName == "Live Action Movie")
|
||||
{
|
||||
category = new List<IndexerCategory> { NewznabStandardCategory.Movies };
|
||||
@@ -426,7 +426,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
//{
|
||||
// continue;
|
||||
//}
|
||||
var infoString = releaseTags.Aggregate("", (prev, cur) => prev + "[" + cur + "]");
|
||||
var infoString = releaseTags.Aggregate(string.Empty, (prev, cur) => prev + "[" + cur + "]");
|
||||
var minimumSeedTime = 259200;
|
||||
|
||||
// Additional 5 hours per GB
|
||||
@@ -447,7 +447,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Title = releaseTitle,
|
||||
InfoUrl = details.AbsoluteUri,
|
||||
Guid = guid.AbsoluteUri,
|
||||
DownloadUrl = linkUri.AbsoluteUri,
|
||||
DownloadUrl = link.AbsoluteUri,
|
||||
PublishDate = publishDate,
|
||||
Categories = category,
|
||||
Description = description,
|
||||
@@ -457,7 +457,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Grabs = snatched,
|
||||
Files = fileCount,
|
||||
DownloadVolumeFactor = rawDownMultiplier,
|
||||
UploadVolumeFactor = rawUpMultiplier
|
||||
UploadVolumeFactor = rawUpMultiplier,
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
@@ -484,7 +484,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimeBytesSettings : IProviderConfig
|
||||
public class AnimeBytesSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AnimeBytesSettingsValidator Validator = new AnimeBytesSettingsValidator();
|
||||
|
||||
@@ -492,17 +492,321 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
Passkey = "";
|
||||
Username = "";
|
||||
EnableSonarrCompatibility = true;
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Passkey", HelpText = "Site Passkey")]
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Passkey", HelpText = "Site Passkey", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Passkey { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Username", HelpText = "Site username")]
|
||||
[FieldDefinition(3, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Enable Sonarr Compatibility", Type = FieldType.Checkbox, HelpText = "Makes Prowlarr try to add Season information into Release names, without this Sonarr can't match any Seasons, but it has a lot of false positives as well")]
|
||||
public bool EnableSonarrCompatibility { get; set; }
|
||||
|
||||
[FieldDefinition(5)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimeBytesResponse
|
||||
{
|
||||
[JsonProperty("Matches")]
|
||||
public long Matches { get; set; }
|
||||
|
||||
[JsonProperty("Limit")]
|
||||
public long Limit { get; set; }
|
||||
|
||||
[JsonProperty("Results")]
|
||||
[JsonConverter(typeof(ParseStringConverter))]
|
||||
public long Results { get; set; }
|
||||
|
||||
[JsonProperty("Groups")]
|
||||
public Group[] Groups { get; set; }
|
||||
}
|
||||
|
||||
public class Group
|
||||
{
|
||||
[JsonProperty("ID")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonProperty("CategoryName")]
|
||||
public string CategoryName { get; set; }
|
||||
|
||||
[JsonProperty("FullName")]
|
||||
public string FullName { get; set; }
|
||||
|
||||
[JsonProperty("GroupName")]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
[JsonProperty("SeriesID")]
|
||||
[JsonConverter(typeof(ParseStringConverter))]
|
||||
public long SeriesId { get; set; }
|
||||
|
||||
[JsonProperty("SeriesName")]
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
[JsonProperty("Artists")]
|
||||
public object Artists { get; set; }
|
||||
|
||||
[JsonProperty("Year")]
|
||||
[JsonConverter(typeof(ParseStringConverter))]
|
||||
public long Year { get; set; }
|
||||
|
||||
[JsonProperty("Image")]
|
||||
public Uri Image { get; set; }
|
||||
|
||||
[JsonProperty("Synonymns")]
|
||||
[JsonConverter(typeof(SynonymnsConverter))]
|
||||
public Synonymns Synonymns { get; set; }
|
||||
|
||||
[JsonProperty("Snatched")]
|
||||
public long Snatched { get; set; }
|
||||
|
||||
[JsonProperty("Comments")]
|
||||
public long Comments { get; set; }
|
||||
|
||||
[JsonProperty("Links")]
|
||||
[JsonConverter(typeof(LinksUnionConverter))]
|
||||
public LinksUnion Links { get; set; }
|
||||
|
||||
[JsonProperty("Votes")]
|
||||
public long Votes { get; set; }
|
||||
|
||||
[JsonProperty("AvgVote")]
|
||||
public double AvgVote { get; set; }
|
||||
|
||||
[JsonProperty("Associations")]
|
||||
public object Associations { get; set; }
|
||||
|
||||
[JsonProperty("Description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonProperty("DescriptionHTML")]
|
||||
public string DescriptionHtml { get; set; }
|
||||
|
||||
[JsonProperty("EpCount")]
|
||||
public long EpCount { get; set; }
|
||||
|
||||
[JsonProperty("StudioList")]
|
||||
public string StudioList { get; set; }
|
||||
|
||||
[JsonProperty("PastWeek")]
|
||||
public long PastWeek { get; set; }
|
||||
|
||||
[JsonProperty("Incomplete")]
|
||||
public bool Incomplete { get; set; }
|
||||
|
||||
[JsonProperty("Ongoing")]
|
||||
public bool Ongoing { get; set; }
|
||||
|
||||
[JsonProperty("Tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
[JsonProperty("Torrents")]
|
||||
public List<Torrent> Torrents { get; set; }
|
||||
}
|
||||
|
||||
public class LinksClass
|
||||
{
|
||||
[JsonProperty("ANN", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Uri Ann { get; set; }
|
||||
|
||||
[JsonProperty("Manga-Updates", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Uri MangaUpdates { get; set; }
|
||||
|
||||
[JsonProperty("Wikipedia", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Uri Wikipedia { get; set; }
|
||||
|
||||
[JsonProperty("MAL", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Uri Mal { get; set; }
|
||||
|
||||
[JsonProperty("AniDB", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Uri AniDb { get; set; }
|
||||
}
|
||||
|
||||
public class Torrent
|
||||
{
|
||||
[JsonProperty("ID")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[JsonProperty("EditionData")]
|
||||
public EditionData EditionData { get; set; }
|
||||
|
||||
[JsonProperty("RawDownMultiplier")]
|
||||
public double? RawDownMultiplier { get; set; }
|
||||
|
||||
[JsonProperty("RawUpMultiplier")]
|
||||
public double? RawUpMultiplier { get; set; }
|
||||
|
||||
[JsonProperty("Link")]
|
||||
public Uri Link { get; set; }
|
||||
|
||||
[JsonProperty("Property")]
|
||||
public string Property { get; set; }
|
||||
|
||||
[JsonProperty("Snatched")]
|
||||
public int Snatched { get; set; }
|
||||
|
||||
[JsonProperty("Seeders")]
|
||||
public int Seeders { get; set; }
|
||||
|
||||
[JsonProperty("Leechers")]
|
||||
public int Leechers { get; set; }
|
||||
|
||||
[JsonProperty("Size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonProperty("FileCount")]
|
||||
public int FileCount { get; set; }
|
||||
|
||||
[JsonProperty("UploadTime")]
|
||||
public DateTimeOffset UploadTime { get; set; }
|
||||
}
|
||||
|
||||
public class EditionData
|
||||
{
|
||||
[JsonProperty("EditionTitle")]
|
||||
public string EditionTitle { get; set; }
|
||||
}
|
||||
|
||||
public struct LinksUnion
|
||||
{
|
||||
public List<object> AnythingArray;
|
||||
public LinksClass LinksClass;
|
||||
|
||||
public static implicit operator LinksUnion(List<object> anythingArray) => new LinksUnion { AnythingArray = anythingArray };
|
||||
|
||||
public static implicit operator LinksUnion(LinksClass linksClass) => new LinksUnion { LinksClass = linksClass };
|
||||
}
|
||||
|
||||
public struct Synonymns
|
||||
{
|
||||
public List<string> StringArray;
|
||||
public Dictionary<string, string> StringMap;
|
||||
|
||||
public static implicit operator Synonymns(List<string> stringArray) => new Synonymns { StringArray = stringArray };
|
||||
|
||||
public static implicit operator Synonymns(Dictionary<string, string> stringMap) => new Synonymns { StringMap = stringMap };
|
||||
}
|
||||
|
||||
internal class LinksUnionConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type t) => t == typeof(LinksUnion) || t == typeof(LinksUnion?);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.StartObject:
|
||||
var objectValue = serializer.Deserialize<LinksClass>(reader);
|
||||
return new LinksUnion { LinksClass = objectValue };
|
||||
case JsonToken.StartArray:
|
||||
var arrayValue = serializer.Deserialize<List<object>>(reader);
|
||||
return new LinksUnion { AnythingArray = arrayValue };
|
||||
}
|
||||
|
||||
throw new Exception("Cannot unmarshal type LinksUnion");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
|
||||
{
|
||||
var value = (LinksUnion)untypedValue;
|
||||
if (value.AnythingArray != null)
|
||||
{
|
||||
serializer.Serialize(writer, value.AnythingArray);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.LinksClass == null)
|
||||
{
|
||||
throw new Exception("Cannot marshal type LinksUnion");
|
||||
}
|
||||
|
||||
serializer.Serialize(writer, value.LinksClass);
|
||||
}
|
||||
|
||||
public static readonly LinksUnionConverter Singleton = new LinksUnionConverter();
|
||||
}
|
||||
|
||||
internal class ParseStringConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = serializer.Deserialize<string>(reader);
|
||||
if (long.TryParse(value, out var l))
|
||||
{
|
||||
return l;
|
||||
}
|
||||
|
||||
throw new Exception("Cannot unmarshal type long");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
|
||||
{
|
||||
if (untypedValue == null)
|
||||
{
|
||||
serializer.Serialize(writer, null);
|
||||
return;
|
||||
}
|
||||
|
||||
var value = (long)untypedValue;
|
||||
serializer.Serialize(writer, value.ToString());
|
||||
}
|
||||
|
||||
public static readonly ParseStringConverter Singleton = new ParseStringConverter();
|
||||
}
|
||||
|
||||
internal class SynonymnsConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type t) => t == typeof(Synonymns) || t == typeof(Synonymns?);
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.StartObject:
|
||||
var objectValue = serializer.Deserialize<Dictionary<string, string>>(reader);
|
||||
return new Synonymns { StringMap = objectValue };
|
||||
case JsonToken.StartArray:
|
||||
var arrayValue = serializer.Deserialize<List<string>>(reader);
|
||||
return new Synonymns { StringArray = arrayValue };
|
||||
}
|
||||
|
||||
throw new Exception("Cannot unmarshal type Synonymns");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
|
||||
{
|
||||
var value = (Synonymns)untypedValue;
|
||||
if (value.StringArray != null)
|
||||
{
|
||||
serializer.Serialize(writer, value.StringArray);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.StringMap == null)
|
||||
{
|
||||
throw new Exception("Cannot marshal type Synonymns");
|
||||
}
|
||||
|
||||
serializer.Serialize(writer, value.StringMap);
|
||||
}
|
||||
|
||||
public static readonly SynonymnsConverter Singleton = new SynonymnsConverter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
@@ -25,8 +24,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "AnimeTorrents";
|
||||
|
||||
public override string BaseUrl => "https://animetorrents.me/";
|
||||
private string LoginUrl => BaseUrl + "login.php";
|
||||
public override string[] IndexerUrls => new string[] { "https://animetorrents.me/" };
|
||||
public override string Description => "Definitive source for anime and manga";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
@@ -38,12 +38,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
|
||||
return new AnimeTorrentsRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnimeTorrentsParser(Settings, Capabilities.Categories, BaseUrl);
|
||||
return new AnimeTorrentsParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
@@ -135,7 +135,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public AnimeTorrentsSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
public AnimeTorrentsRequestGenerator()
|
||||
{
|
||||
@@ -148,8 +147,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
// replace any space, special char, etc. with % (wildcard)
|
||||
var replaceRegex = new Regex("[^a-zA-Z0-9]+");
|
||||
searchString = replaceRegex.Replace(searchString, "%");
|
||||
var searchUrl = BaseUrl + "ajax/torrents_data.php";
|
||||
var searchUrlReferer = BaseUrl + "torrents.php?cat=0&searchin=filename&search=";
|
||||
var searchUrl = Settings.BaseUrl + "ajax/torrents_data.php";
|
||||
var searchUrlReferer = Settings.BaseUrl + "torrents.php?cat=0&searchin=filename&search=";
|
||||
|
||||
var trackerCats = Capabilities.Categories.MapTorznabCapsToTrackers(categories) ?? new List<string>();
|
||||
|
||||
@@ -229,13 +228,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly AnimeTorrentsSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public AnimeTorrentsParser(AnimeTorrentsSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
|
||||
public AnimeTorrentsParser(AnimeTorrentsSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
@@ -340,7 +337,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimeTorrentsSettings : IProviderConfig
|
||||
public class AnimeTorrentsSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AnimeTorrentsSettingsValidator Validator = new AnimeTorrentsSettingsValidator();
|
||||
|
||||
@@ -350,12 +347,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Username", HelpText = "Site username")]
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Site password", Privacy = PrivacyLevel.Password)]
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class AnimeWorld : Unit3dBase
|
||||
{
|
||||
public override string Name => "AnimeWorld";
|
||||
public override string BaseUrl => "https://animeworld.cx/";
|
||||
public override string[] IndexerUrls => new string[] { "https://animeworld.cx/" };
|
||||
public override string Description => "AnimeWorld (AW) is a GERMAN Private site for ANIME / MANGA / HENTAI";
|
||||
public override string Language => "de-de";
|
||||
|
||||
|
||||
350
src/NzbDrone.Core/Indexers/Definitions/Animedia.cs
Normal file
350
src/NzbDrone.Core/Indexers/Definitions/Animedia.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Animedia : TorrentIndexerBase<AnimediaSettings>
|
||||
{
|
||||
public override string Name => "Animedia";
|
||||
public override string[] IndexerUrls => new string[] { "https://tt.animedia.tv/" };
|
||||
public override string Description => "Animedia is russian anime voiceover group and eponymous anime tracker.";
|
||||
public override string Language => "ru-ru";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Public;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Animedia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnimediaRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnimediaParser(Settings, Capabilities.Categories) { HttpClient = _httpClient, Logger = _logger };
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
caps.Categories.AddCategoryMapping(1, NewznabStandardCategory.TVAnime, "TV Anime");
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA/ONA/Special");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.TV, "Dorama");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.Movies, "Movies");
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimediaRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public AnimediaSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public AnimediaRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
{
|
||||
var requestUrl = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(term))
|
||||
{
|
||||
requestUrl = Settings.BaseUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
var queryCollection = new NameValueCollection
|
||||
{
|
||||
// Remove season and episode info from search term cause it breaks search
|
||||
{ "keywords", Regex.Replace(term, @"(?:[SsEe]?\d{1,4}){1,2}$", "").TrimEnd() },
|
||||
{ "limit", "20" },
|
||||
{ "orderby_sort", "entry_date|desc" }
|
||||
};
|
||||
|
||||
requestUrl = string.Format("{0}/ajax/search_result/P0?{1}", Settings.BaseUrl.TrimEnd('/'), queryCollection.GetQueryString());
|
||||
}
|
||||
|
||||
var request = new IndexerRequest(requestUrl, HttpAccept.Html);
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
// Animedia doesn't support music, but this function required by interface
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
// Animedia doesn't support books, but this function required by interface
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
return new IndexerPageableRequestChain();
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnimediaParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly AnimediaSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private static readonly Regex EpisodesInfoQueryRegex = new Regex(@"сери[ия] (\d+)(?:-(\d+))? из.*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex ResolutionInfoQueryRegex = new Regex(@"качество (\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex SizeInfoQueryRegex = new Regex(@"размер:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex ReleaseDateInfoQueryRegex = new Regex(@"добавлен:(.*)\n", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex CategorieMovieRegex = new Regex(@"Фильм", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex CategorieOVARegex = new Regex(@"ОВА|OVA|ОНА|ONA|Special", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex CategorieDoramaRegex = new Regex(@"Дорама", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
public IHttpClient HttpClient { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
|
||||
public AnimediaParser(AnimediaSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
private string composeTitle(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var name_ru = dom.QuerySelector("div.media__post__header > h1").TextContent.Trim();
|
||||
var name_en = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(1) > div > span").TextContent.Trim();
|
||||
var name_orig = dom.QuerySelector("div.media__panel > div:nth-of-type(1) > div.col-l:nth-of-type(2) > div > span").TextContent.Trim();
|
||||
|
||||
var title = name_ru + " / " + name_en;
|
||||
if (name_en != name_orig)
|
||||
{
|
||||
title += " / " + name_orig;
|
||||
}
|
||||
|
||||
var tabName = t.TextContent;
|
||||
tabName = tabName.Replace("Сезон", "Season");
|
||||
if (tabName.Contains("Серии"))
|
||||
{
|
||||
tabName = "";
|
||||
}
|
||||
|
||||
var heading = tr.QuerySelector("h3.tracker_info_bold").TextContent;
|
||||
|
||||
// Parse episodes info from heading if episods info present
|
||||
var match = EpisodesInfoQueryRegex.Match(heading);
|
||||
heading = tabName;
|
||||
if (match.Success)
|
||||
{
|
||||
if (string.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
heading += " E" + match.Groups[1].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
heading += string.Format(" E{0}-{1}", match.Groups[1].Value, match.Groups[2].Value);
|
||||
}
|
||||
}
|
||||
|
||||
return title + " - " + heading + " [" + getResolution(tr) + "p]";
|
||||
}
|
||||
|
||||
private string getResolution(AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var resolution = tr.QuerySelector("div.tracker_info_left").TextContent;
|
||||
return ResolutionInfoQueryRegex.Match(resolution).Groups[1].Value;
|
||||
}
|
||||
|
||||
private long getReleaseSize(AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
|
||||
return ReleaseInfo.GetBytes(SizeInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
|
||||
}
|
||||
|
||||
private DateTime getReleaseDate(AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var sizeStr = tr.QuerySelector("div.tracker_info_left").TextContent;
|
||||
return DateTime.Parse(ReleaseDateInfoQueryRegex.Match(sizeStr).Groups[1].Value.Trim());
|
||||
}
|
||||
|
||||
private ICollection<IndexerCategory> MapCategories(AngleSharp.Html.Dom.IHtmlDocument dom, AngleSharp.Dom.IElement t, AngleSharp.Dom.IElement tr)
|
||||
{
|
||||
var rName = t.TextContent;
|
||||
var rDesc = tr.QuerySelector("h3.tracker_info_bold").TextContent;
|
||||
var type = dom.QuerySelector("div.releases-date:contains('Тип:')").TextContent;
|
||||
|
||||
// Check OVA first cause OVA looks like anime with OVA in release name or description
|
||||
if (CategorieOVARegex.IsMatch(rName) || CategorieOVARegex.IsMatch(rDesc))
|
||||
{
|
||||
return _categories.MapTrackerCatDescToNewznab("OVA/ONA/Special");
|
||||
}
|
||||
|
||||
// Check movies then, cause some of releases could be movies dorama and should go to movies category
|
||||
if (CategorieMovieRegex.IsMatch(rName) || CategorieMovieRegex.IsMatch(rDesc))
|
||||
{
|
||||
return _categories.MapTrackerCatDescToNewznab("Movies");
|
||||
}
|
||||
|
||||
// Check dorama. Most of doramas are flaged as doramas in type info, but type info could have a lot of types at same time (movie, etc)
|
||||
if (CategorieDoramaRegex.IsMatch(rName) || CategorieDoramaRegex.IsMatch(type))
|
||||
{
|
||||
return _categories.MapTrackerCatDescToNewznab("Dorama");
|
||||
}
|
||||
|
||||
return _categories.MapTrackerCatDescToNewznab("TV Anime");
|
||||
}
|
||||
|
||||
private IList<TorrentInfo> ParseRelease(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<TorrentInfo>();
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
|
||||
foreach (var t in dom.QuerySelectorAll("ul.media__tabs__nav > li > a"))
|
||||
{
|
||||
var tr_id = t.Attributes["href"].Value;
|
||||
var tr = dom.QuerySelector("div" + tr_id);
|
||||
var seeders = int.Parse(tr.QuerySelector("div.circle_green_text_top").TextContent);
|
||||
var url = indexerResponse.HttpRequest.Url.ToString();
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
Title = composeTitle(dom, t, tr),
|
||||
InfoUrl = url,
|
||||
DownloadVolumeFactor = 0,
|
||||
UploadVolumeFactor = 1,
|
||||
|
||||
Guid = url + tr_id,
|
||||
Seeders = seeders,
|
||||
Peers = seeders + int.Parse(tr.QuerySelector("div.circle_red_text_top").TextContent),
|
||||
Grabs = int.Parse(tr.QuerySelector("div.circle_grey_text_top").TextContent),
|
||||
Categories = MapCategories(dom, t, tr),
|
||||
PublishDate = getReleaseDate(tr),
|
||||
DownloadUrl = tr.QuerySelector("div.download_tracker > a.btn__green").Attributes["href"].Value,
|
||||
MagnetUrl = tr.QuerySelector("div.download_tracker > a.btn__d-gray").Attributes["href"].Value,
|
||||
Size = getReleaseSize(tr),
|
||||
Resolution = getResolution(tr)
|
||||
};
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
|
||||
return torrentInfos;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(indexerResponse.Content);
|
||||
var links = dom.QuerySelectorAll("a.ads-list__item__title");
|
||||
foreach (var link in links)
|
||||
{
|
||||
var url = link.GetAttribute("href");
|
||||
|
||||
// Some URLs in search are broken
|
||||
if (url.StartsWith("//"))
|
||||
{
|
||||
url = "https:" + url;
|
||||
}
|
||||
|
||||
var releaseRequest = new IndexerRequest(url, HttpAccept.Html);
|
||||
var releaseResponse = new IndexerResponse(releaseRequest, HttpClient.Execute(releaseRequest.HttpRequest));
|
||||
|
||||
// Throw common http errors here before we try to parse
|
||||
if (releaseResponse.HttpResponse.HasHttpError)
|
||||
{
|
||||
if ((int)releaseResponse.HttpResponse.StatusCode == 429)
|
||||
{
|
||||
throw new TooManyRequestsException(releaseRequest.HttpRequest, releaseResponse.HttpResponse);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IndexerException(releaseResponse, "Http error code: " + releaseResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
torrentInfos.AddRange(ParseRelease(releaseResponse));
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnimediaSettingsValidator : AbstractValidator<AnimediaSettings>
|
||||
{
|
||||
public AnimediaSettingsValidator()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class AnimediaSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AnimediaSettingsValidator Validator = new AnimediaSettingsValidator();
|
||||
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
336
src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs
Normal file
336
src/NzbDrone.Core/Indexers/Definitions/Anthelion.cs
Normal file
@@ -0,0 +1,336 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Html.Parser;
|
||||
using FluentValidation;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public class Anthelion : TorrentIndexerBase<AnthelionSettings>
|
||||
{
|
||||
public override string Name => "Anthelion";
|
||||
public override string[] IndexerUrls => new string[] { "https://anthelion.me/" };
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override string Description => "A movies tracker";
|
||||
public override string Language => "en-us";
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public Anthelion(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new AnthelionRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new AnthelionParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(LoginUrl)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
AllowAutoRedirect = true
|
||||
};
|
||||
|
||||
requestBuilder.Method = HttpMethod.POST;
|
||||
requestBuilder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||
|
||||
var cookies = Cookies;
|
||||
|
||||
Cookies = null;
|
||||
var authLoginRequest = requestBuilder
|
||||
.AddFormParameter("username", Settings.Username)
|
||||
.AddFormParameter("password", Settings.Password)
|
||||
.AddFormParameter("keeplogged", "1")
|
||||
.AddFormParameter("login", "Log+In!")
|
||||
.SetHeader("Content-Type", "multipart/form-data")
|
||||
.Build();
|
||||
|
||||
var headers = new NameValueCollection
|
||||
{
|
||||
{ "Referer", LoginUrl }
|
||||
};
|
||||
|
||||
authLoginRequest.Headers.Add(headers);
|
||||
|
||||
var response = await ExecuteAuth(authLoginRequest);
|
||||
|
||||
if (CheckIfLoginNeeded(response))
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var dom = parser.ParseDocument(response.Content);
|
||||
var errorMessage = dom.QuerySelector("form#loginform").TextContent.Trim();
|
||||
|
||||
throw new IndexerAuthException(errorMessage);
|
||||
}
|
||||
|
||||
cookies = response.GetCookies();
|
||||
UpdateCookies(cookies, DateTime.Now + TimeSpan.FromDays(30));
|
||||
|
||||
_logger.Debug("Anthelion authentication succeeded.");
|
||||
}
|
||||
|
||||
protected override bool CheckIfLoginNeeded(HttpResponse httpResponse)
|
||||
{
|
||||
if (!httpResponse.Content.Contains("logout.php"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
{
|
||||
var caps = new IndexerCapabilities
|
||||
{
|
||||
TvSearchParams = new List<TvSearchParam>
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
}
|
||||
};
|
||||
|
||||
caps.Categories.AddCategoryMapping("1", NewznabStandardCategory.Movies, "Film/Feature");
|
||||
caps.Categories.AddCategoryMapping("2", NewznabStandardCategory.Movies, "Film/Short");
|
||||
caps.Categories.AddCategoryMapping("3", NewznabStandardCategory.TV, "TV/Miniseries");
|
||||
caps.Categories.AddCategoryMapping("4", NewznabStandardCategory.Other, "Other");
|
||||
|
||||
return caps;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnthelionRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public AnthelionSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
|
||||
public AnthelionRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories, string imdbId = null)
|
||||
{
|
||||
var searchUrl = string.Format("{0}/torrents.php", Settings.BaseUrl.TrimEnd('/'));
|
||||
|
||||
// TODO: IMDB search is available but it requires to parse the details page
|
||||
var qc = new NameValueCollection
|
||||
{
|
||||
{ "order_by", "time" },
|
||||
{ "order_way", "desc" },
|
||||
{ "action", "basic" },
|
||||
{ "searchsubmit", "1" },
|
||||
{ "searchstr", imdbId.IsNotNullOrWhiteSpace() ? imdbId : term }
|
||||
};
|
||||
|
||||
var catList = Capabilities.Categories.MapTorznabCapsToTrackers(categories);
|
||||
|
||||
foreach (var cat in catList)
|
||||
{
|
||||
qc.Add($"filter_cat[{cat}]", "1");
|
||||
}
|
||||
|
||||
searchUrl = searchUrl + "?" + qc.GetQueryString();
|
||||
|
||||
var request = new IndexerRequest(searchUrl, HttpAccept.Html);
|
||||
|
||||
yield return request;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MovieSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(MusicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(TvSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedTvSearchString), searchCriteria.Categories, searchCriteria.FullImdbId));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BookSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public IndexerPageableRequestChain GetSearchRequests(BasicSearchCriteria searchCriteria)
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnthelionParser : IParseIndexerResponse
|
||||
{
|
||||
private readonly AnthelionSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
|
||||
public AnthelionParser(AnthelionSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
{
|
||||
var torrentInfos = new List<ReleaseInfo>();
|
||||
|
||||
var parser = new HtmlParser();
|
||||
var doc = parser.ParseDocument(indexerResponse.Content);
|
||||
var rows = doc.QuerySelectorAll("table.torrent_table > tbody > tr.torrent");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var qDetailsLink = row.QuerySelector("a.torrent_name");
|
||||
var year = qDetailsLink.NextSibling.TextContent.Replace("[", "").Replace("]", "").Trim();
|
||||
var tags = row.QuerySelector("div.torrent_info").FirstChild.TextContent.Replace(" / ", " ").Trim();
|
||||
var title = $"{qDetailsLink.TextContent} {year} {tags}";
|
||||
var description = row.QuerySelector("div.tags").TextContent.Trim();
|
||||
var details = _settings.BaseUrl + qDetailsLink.GetAttribute("href");
|
||||
var torrentId = qDetailsLink.GetAttribute("href").Split('=').Last();
|
||||
var link = _settings.BaseUrl + "torrents.php?action=download&id=" + torrentId;
|
||||
var posterStr = qDetailsLink.GetAttribute("data-cover");
|
||||
var poster = !string.IsNullOrWhiteSpace(posterStr) ? posterStr : null;
|
||||
|
||||
var files = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(3)").TextContent);
|
||||
var publishDate = DateTimeUtil.FromTimeAgo(row.QuerySelector("td:nth-child(4)").TextContent);
|
||||
var size = ReleaseInfo.GetBytes(row.QuerySelector("td:nth-child(5)").FirstChild.TextContent);
|
||||
var grabs = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(6)").TextContent);
|
||||
var seeders = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(7)").TextContent);
|
||||
var leechers = ParseUtil.CoerceInt(row.QuerySelector("td:nth-child(8)").TextContent);
|
||||
|
||||
var dlVolumeFactor = row.QuerySelector("strong.tl_free") != null ? 0 : 1;
|
||||
|
||||
var cat = row.QuerySelector("td.cats_col > div").GetAttribute("class").Replace("tooltip cats_", "");
|
||||
var category = new List<IndexerCategory>
|
||||
{
|
||||
cat switch
|
||||
{
|
||||
"featurefilm" => NewznabStandardCategory.Movies,
|
||||
"shortfilm" => NewznabStandardCategory.Movies,
|
||||
"miniseries" => NewznabStandardCategory.TV,
|
||||
"other" => NewznabStandardCategory.Other,
|
||||
_ => throw new Exception($"Unknown category: {cat}")
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: TMDb is also available
|
||||
var qImdb = row.QuerySelector("a[href^=\"https://www.imdb.com\"]");
|
||||
var imdb = qImdb != null ? ParseUtil.GetImdbID(qImdb.GetAttribute("href").Split('/').Last()) : null;
|
||||
|
||||
var release = new TorrentInfo
|
||||
{
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 259200,
|
||||
Description = description,
|
||||
Title = title,
|
||||
PublishDate = publishDate,
|
||||
Categories = category,
|
||||
DownloadUrl = link,
|
||||
InfoUrl = details,
|
||||
PosterUrl = poster,
|
||||
Guid = link,
|
||||
ImdbId = imdb.GetValueOrDefault(),
|
||||
Seeders = seeders,
|
||||
Peers = leechers + seeders,
|
||||
Size = size,
|
||||
Grabs = grabs,
|
||||
Files = files,
|
||||
DownloadVolumeFactor = dlVolumeFactor,
|
||||
UploadVolumeFactor = 1
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
}
|
||||
|
||||
return torrentInfos.ToArray();
|
||||
}
|
||||
|
||||
public Action<IDictionary<string, string>, DateTime?> CookiesUpdater { get; set; }
|
||||
}
|
||||
|
||||
public class AnthelionSettingsValidator : AbstractValidator<AnthelionSettings>
|
||||
{
|
||||
public AnthelionSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class AnthelionSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AnthelionSettingsValidator Validator = new AnthelionSettingsValidator();
|
||||
|
||||
public AnthelionSettings()
|
||||
{
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class AvistaZ : AvistazBase
|
||||
{
|
||||
public override string Name => "AvistaZ";
|
||||
public override string BaseUrl => "https://avistaz.to/";
|
||||
public override string[] IndexerUrls => new string[] { "https://avistaz.to/" };
|
||||
public override string Description => "Aka AsiaTorrents";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public AvistaZ(IIndexerRepository indexerRepository, IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
@@ -25,8 +26,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Settings = Settings,
|
||||
HttpClient = _httpClient,
|
||||
Logger = _logger,
|
||||
Capabilities = Capabilities,
|
||||
BaseUrl = BaseUrl
|
||||
Capabilities = Capabilities
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public abstract class AvistazBase : TorrentIndexerBase<AvistazSettings>
|
||||
{
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override string BaseUrl => "";
|
||||
protected virtual string LoginUrl => BaseUrl + "api/v1/jackett/auth";
|
||||
public override string[] IndexerUrls => new string[] { "" };
|
||||
protected virtual string LoginUrl => Settings.BaseUrl + "api/v1/jackett/auth";
|
||||
public override bool SupportsRss => true;
|
||||
public override bool SupportsSearch => true;
|
||||
public override int PageSize => 50;
|
||||
@@ -38,8 +38,7 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
Settings = Settings,
|
||||
HttpClient = _httpClient,
|
||||
Logger = _logger,
|
||||
Capabilities = Capabilities,
|
||||
BaseUrl = BaseUrl
|
||||
Capabilities = Capabilities
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,13 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
public class AvistazRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
public AvistazSettings Settings { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
public IDictionary<string, string> AuthCookieCache { get; set; }
|
||||
public IHttpClient HttpClient { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
public Logger Logger { get; set; }
|
||||
|
||||
protected virtual string SearchUrl => BaseUrl + "api/v1/jackett/torrents";
|
||||
protected virtual string SearchUrl => Settings.BaseUrl + "api/v1/jackett/torrents";
|
||||
protected virtual bool ImdbInTags => false;
|
||||
|
||||
public Func<IDictionary<string, string>> GetCookies { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
@@ -11,10 +10,11 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
{
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
RuleFor(c => c.Password).NotEmpty();
|
||||
RuleFor(c => c.Pid).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class AvistazSettings : IProviderConfig
|
||||
public class AvistazSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly AvistazSettingsValidator Validator = new AvistazSettingsValidator();
|
||||
|
||||
@@ -25,15 +25,21 @@ namespace NzbDrone.Core.Indexers.Definitions.Avistaz
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Username", HelpText = "Username", Privacy = PrivacyLevel.UserName)]
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Password", Privacy = PrivacyLevel.Password)]
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "PID", HelpText = "PID from My Account or My Profile page")]
|
||||
[FieldDefinition(4, Label = "PID", HelpText = "PID from My Account or My Profile page")]
|
||||
public string Pid { get; set; }
|
||||
|
||||
[FieldDefinition(5)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -16,7 +16,6 @@ using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
@@ -25,8 +24,9 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "BakaBT";
|
||||
|
||||
public override string BaseUrl => "https://bakabt.me/";
|
||||
private string LoginUrl => BaseUrl + "login.php";
|
||||
public override string[] IndexerUrls => new string[] { "https://bakabt.me/" };
|
||||
public override string Description => "Anime Community";
|
||||
private string LoginUrl => Settings.BaseUrl + "login.php";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
@@ -38,12 +38,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BakaBTRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
|
||||
return new BakaBTRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new BakaBTParser(Settings, Capabilities.Categories, BaseUrl);
|
||||
return new BakaBTParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
protected override async Task DoLogin()
|
||||
@@ -110,6 +110,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
TvSearchParam.Q, TvSearchParam.Season, TvSearchParam.Ep
|
||||
},
|
||||
MovieSearchParams = new List<MovieSearchParam>
|
||||
{
|
||||
MovieSearchParam.Q
|
||||
},
|
||||
MusicSearchParams = new List<MusicSearchParam>
|
||||
{
|
||||
MusicSearchParam.Q
|
||||
@@ -124,7 +128,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
caps.Categories.AddCategoryMapping(2, NewznabStandardCategory.TVAnime, "OVA");
|
||||
caps.Categories.AddCategoryMapping(3, NewznabStandardCategory.AudioOther, "Soundtrack");
|
||||
caps.Categories.AddCategoryMapping(4, NewznabStandardCategory.BooksComics, "Manga");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.TVAnime, "Anime Movie");
|
||||
caps.Categories.AddCategoryMapping(5, NewznabStandardCategory.Movies, "Anime Movie");
|
||||
caps.Categories.AddCategoryMapping(6, NewznabStandardCategory.TVOther, "Live Action");
|
||||
caps.Categories.AddCategoryMapping(7, NewznabStandardCategory.BooksOther, "Artbook");
|
||||
caps.Categories.AddCategoryMapping(8, NewznabStandardCategory.AudioVideo, "Music Video");
|
||||
@@ -138,16 +142,15 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public BakaBTSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
public BakaBTRequestGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term, int[] categories)
|
||||
private IEnumerable<IndexerRequest> GetPagedRequests(string term)
|
||||
{
|
||||
var searchString = term;
|
||||
var searchUrl = BaseUrl + "browse.php?only=0&hentai=1&incomplete=1&lossless=1&hd=1&multiaudio=1&bonus=1&reorder=1&q=";
|
||||
var searchUrl = Settings.BaseUrl + "browse.php?only=0&hentai=1&incomplete=1&lossless=1&hd=1&multiaudio=1&bonus=1&reorder=1&q=";
|
||||
|
||||
var match = Regex.Match(term, @".*(?=\s(?:[Ee]\d+|\d+)$)");
|
||||
if (match.Success)
|
||||
@@ -166,6 +169,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
@@ -173,7 +178,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -182,7 +187,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -191,7 +196,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -200,7 +205,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
var pageableRequests = new IndexerPageableRequestChain();
|
||||
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm), searchCriteria.Categories));
|
||||
pageableRequests.Add(GetPagedRequests(string.Format("{0}", searchCriteria.SanitizedSearchTerm)));
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
@@ -213,14 +218,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly BakaBTSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly string _baseUrl;
|
||||
private readonly List<IndexerCategory> _defaultCategories = new List<IndexerCategory> { NewznabStandardCategory.TVAnime };
|
||||
|
||||
public BakaBTParser(BakaBTSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
|
||||
public BakaBTParser(BakaBTSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
@@ -299,11 +302,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
release.Categories = currentCategories;
|
||||
|
||||
//release.Description = row.QuerySelector("span.tags")?.TextContent;
|
||||
release.Guid = _baseUrl + qTitleLink.GetAttribute("href");
|
||||
release.Description = row.QuerySelector("span.tags")?.TextContent;
|
||||
release.Guid = _settings.BaseUrl + qTitleLink.GetAttribute("href");
|
||||
release.InfoUrl = release.Guid;
|
||||
|
||||
release.DownloadUrl = _baseUrl + row.QuerySelector(".peers a").GetAttribute("href");
|
||||
release.DownloadUrl = _settings.BaseUrl + row.QuerySelector(".peers a").GetAttribute("href");
|
||||
|
||||
var grabs = row.QuerySelectorAll(".peers")[0].FirstChild.NodeValue.TrimEnd().TrimEnd('/').TrimEnd();
|
||||
grabs = grabs.Replace("k", "000");
|
||||
@@ -392,7 +395,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
public class BakaBTSettings : IProviderConfig
|
||||
public class BakaBTSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly BakaBTSettingsValidator Validator = new BakaBTSettingsValidator();
|
||||
|
||||
@@ -402,18 +405,24 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
Password = "";
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "Username", HelpText = "Site username")]
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Username", HelpText = "Site Username", Privacy = PrivacyLevel.UserName)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "Password", Type = FieldType.Password, HelpText = "Site password", Privacy = PrivacyLevel.Password)]
|
||||
[FieldDefinition(3, Label = "Password", HelpText = "Site Password", Privacy = PrivacyLevel.Password, Type = FieldType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[FieldDefinition(3, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]
|
||||
[FieldDefinition(4, Label = "Add Romaji Title", Type = FieldType.Checkbox, HelpText = "Add releases for Romaji Title")]
|
||||
public bool AddRomajiTitle { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "Append Season", Type = FieldType.Checkbox, HelpText = "Append Season for Sonarr Compatibility")]
|
||||
[FieldDefinition(5, Label = "Append Season", Type = FieldType.Checkbox, HelpText = "Append Season for Sonarr Compatibility")]
|
||||
public bool AppendSeason { get; set; }
|
||||
|
||||
[FieldDefinition(6)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
||||
@@ -16,7 +16,6 @@ using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Definitions
|
||||
@@ -25,7 +24,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public override string Name => "BeyondHD";
|
||||
|
||||
public override string BaseUrl => "https://beyond-hd.me/";
|
||||
public override string[] IndexerUrls => new string[] { "https://beyond-hd.me/" };
|
||||
public override string Description => "BeyondHD (BHD) is a Private Torrent Tracker for HD MOVIES / TV";
|
||||
public override DownloadProtocol Protocol => DownloadProtocol.Torrent;
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
@@ -37,12 +37,12 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new BeyondHDRequestGenerator() { Settings = Settings, Capabilities = Capabilities, BaseUrl = BaseUrl };
|
||||
return new BeyondHDRequestGenerator() { Settings = Settings, Capabilities = Capabilities };
|
||||
}
|
||||
|
||||
public override IParseIndexerResponse GetParser()
|
||||
{
|
||||
return new BeyondHDParser(Settings, Capabilities.Categories, BaseUrl);
|
||||
return new BeyondHDParser(Settings, Capabilities.Categories);
|
||||
}
|
||||
|
||||
private IndexerCapabilities SetCapabilities()
|
||||
@@ -70,7 +70,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
public BeyondHDSettings Settings { get; set; }
|
||||
public IndexerCapabilities Capabilities { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
public BeyondHDRequestGenerator()
|
||||
{
|
||||
@@ -106,7 +105,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
body.Add("categories", string.Join(",", cats));
|
||||
}
|
||||
|
||||
var searchUrl = BaseUrl + "api/torrents/" + Settings.ApiKey;
|
||||
var searchUrl = Settings.BaseUrl + "api/torrents/" + Settings.ApiKey;
|
||||
|
||||
var request = new HttpRequest(searchUrl, HttpAccept.Json);
|
||||
|
||||
@@ -172,13 +171,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
private readonly BeyondHDSettings _settings;
|
||||
private readonly IndexerCapabilitiesCategories _categories;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories, string baseUrl)
|
||||
public BeyondHDParser(BeyondHDSettings settings, IndexerCapabilitiesCategories categories)
|
||||
{
|
||||
_settings = settings;
|
||||
_categories = categories;
|
||||
_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
|
||||
@@ -190,11 +187,11 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response status {indexerResponse.HttpResponse.StatusCode} code from API request");
|
||||
}
|
||||
|
||||
// TODO Have BHD fix their API response content type so we can proper check here
|
||||
// if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
// {
|
||||
// throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
// }
|
||||
if (!indexerResponse.HttpResponse.Headers.ContentType.Contains(HttpAccept.Json.Value))
|
||||
{
|
||||
throw new IndexerException(indexerResponse, $"Unexpected response header {indexerResponse.HttpResponse.Headers.ContentType} from API request, expected {HttpAccept.Json.Value}");
|
||||
}
|
||||
|
||||
var jsonResponse = new HttpResponse<BeyondHDResponse>(indexerResponse.HttpResponse);
|
||||
|
||||
foreach (var row in jsonResponse.Resource.Results)
|
||||
@@ -217,10 +214,10 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
ImdbId = ParseUtil.GetImdbID(row.ImdbId).GetValueOrDefault(),
|
||||
TmdbId = row.TmdbId.IsNullOrWhiteSpace() ? 0 : ParseUtil.CoerceInt(row.TmdbId.Split("/")[1]),
|
||||
Peers = row.Leechers + row.Seeders,
|
||||
DownloadVolumeFactor = row.Freeleech ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
|
||||
DownloadVolumeFactor = row.Freeleech || row.Limited ? 0 : row.Promo75 ? 0.25 : row.Promo50 ? 0.5 : row.Promo25 ? 0.75 : 1,
|
||||
UploadVolumeFactor = 1,
|
||||
MinimumRatio = 1,
|
||||
MinimumSeedTime = 172800, // 48 hours
|
||||
MinimumSeedTime = 172800, // 120 hours
|
||||
};
|
||||
|
||||
torrentInfos.Add(release);
|
||||
@@ -242,7 +239,7 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
}
|
||||
}
|
||||
|
||||
public class BeyondHDSettings : IProviderConfig
|
||||
public class BeyondHDSettings : IIndexerSettings
|
||||
{
|
||||
private static readonly BeyondHDSettingsValidator Validator = new BeyondHDSettingsValidator();
|
||||
|
||||
@@ -250,12 +247,18 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
{
|
||||
}
|
||||
|
||||
[FieldDefinition(1, Label = "API Key", HelpText = "API Key from Site", Privacy = PrivacyLevel.ApiKey)]
|
||||
[FieldDefinition(1, Label = "Base Url", Type = FieldType.Select, SelectOptionsProviderAction = "getUrls", HelpText = "Select which baseurl Prowlarr will use for requests to the site")]
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "API Key", HelpText = "API Key from the Site (Found in My Security => API Key)", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[FieldDefinition(2, Label = "RSS Key", HelpText = "RSS Key from Site", Privacy = PrivacyLevel.ApiKey)]
|
||||
[FieldDefinition(3, Label = "RSS Key", HelpText = "RSS Key from the Site (Found in My Security => RSS Key)", Privacy = PrivacyLevel.ApiKey)]
|
||||
public string RssKey { get; set; }
|
||||
|
||||
[FieldDefinition(4)]
|
||||
public IndexerBaseSettings BaseSettings { get; set; } = new IndexerBaseSettings();
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
@@ -300,5 +303,6 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public bool Promo25 { get; set; }
|
||||
public bool Promo50 { get; set; }
|
||||
public bool Promo75 { get; set; }
|
||||
public bool Limited { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace NzbDrone.Core.Indexers.Definitions
|
||||
public class Blutopia : Unit3dBase
|
||||
{
|
||||
public override string Name => "Blutopia";
|
||||
public override string BaseUrl => "https://blutopia.xyz/";
|
||||
public override string[] IndexerUrls => new string[] { "https://blutopia.xyz/" };
|
||||
public override string Description => "Blutopia (BLU) is a Private Torrent Tracker for HD MOVIES / TV";
|
||||
public override IndexerPrivacy Privacy => IndexerPrivacy.Private;
|
||||
|
||||
public Blutopia(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
public override int PageSize => 100;
|
||||
public override IndexerCapabilities Capabilities => SetCapabilities();
|
||||
|
||||
public override string BaseUrl => "http://api.broadcasthe.net/";
|
||||
public override string[] IndexerUrls => new string[] { "http://api.broadcasthe.net/" };
|
||||
public override string Description => "BroadcasTheNet (BTN) is an invite-only torrent tracker focused on TV shows";
|
||||
|
||||
public BroadcastheNet(IHttpClient httpClient, IEventAggregator eventAggregator, IIndexerStatusService indexerStatusService, IConfigService configService, Logger logger)
|
||||
: base(httpClient, eventAggregator, indexerStatusService, configService, logger)
|
||||
@@ -26,7 +27,7 @@ namespace NzbDrone.Core.Indexers.BroadcastheNet
|
||||
|
||||
public override IIndexerRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, BaseUrl = BaseUrl, Capabilities = Capabilities };
|
||||
var requestGenerator = new BroadcastheNetRequestGenerator() { Settings = Settings, PageSize = PageSize, Capabilities = Capabilities };
|
||||
|
||||
var releaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id);
|
||||
if (releaseInfo != null)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user