Compare commits
1 Commits
master
...
revert-909
Author | SHA1 | Date | |
---|---|---|---|
![]() |
db25a68072 |
@ -1,8 +0,0 @@
|
||||
FROM rssbridge/rss-bridge:latest
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --yes --no-install-recommends \
|
||||
git && \
|
||||
pecl install xdebug && \
|
||||
pear install PHP_CodeSniffer && \
|
||||
docker-php-ext-enable xdebug
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "rss-bridge dev",
|
||||
"build": { "dockerfile": "Dockerfile" },
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"php.validate.executablePath": "/usr/local/bin/php",
|
||||
"phpSniffer.executablesFolder": "/usr/local/bin/",
|
||||
"phpcs.executablePath": "/usr/local/bin/phpcs",
|
||||
"phpcs.lintOnType": false
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"xdebug.php-debug",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"philfontaine.autolaunch",
|
||||
"eamodio.gitlens",
|
||||
"shevaua.phpcs"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3100, 9000, 9003],
|
||||
"postCreateCommand": "cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf && cp .devcontainer/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && mkdir .vscode && cp .devcontainer/launch.json .vscode && echo '*' > whitelist.txt && chmod a+x \"$(pwd)\" && rm -rf /var/www/html && ln -s \"$(pwd)\" /var/www/html && nginx && php-fpm -D"
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Listen for Xdebug",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"port": 9003,
|
||||
"auto": true
|
||||
},
|
||||
{
|
||||
"name": "Launch currently open script",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"cwd": "${fileDirname}",
|
||||
"port": 0,
|
||||
"runtimeArgs": [
|
||||
"-dxdebug.start_with_request=yes"
|
||||
],
|
||||
"env": {
|
||||
"XDEBUG_MODE": "debug,develop",
|
||||
"XDEBUG_CONFIG": "client_port=${port}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Built-in web server",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"-dxdebug.mode=debug",
|
||||
"-dxdebug.start_with_request=yes",
|
||||
"-S",
|
||||
"localhost:0"
|
||||
],
|
||||
"program": "",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"port": 9003,
|
||||
"serverReadyAction": {
|
||||
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
|
||||
"uriFormat": "http://localhost:%s",
|
||||
"action": "openExternally"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
server {
|
||||
listen 3100 default_server;
|
||||
root /workspaces/rss-bridge;
|
||||
access_log /var/log/nginx/rssbridge.access.log;
|
||||
error_log /var/log/nginx/rssbridge.error.log;
|
||||
index index.php;
|
||||
|
||||
location ~ /(\.|vendor|tests) {
|
||||
deny all;
|
||||
return 403; # Forbidden
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
[xdebug]
|
||||
xdebug.mode=develop,debug
|
||||
xdebug.client_host=localhost
|
||||
xdebug.client_port=9003
|
||||
xdebug.start_with_request=yes
|
||||
xdebug.discover_client_host=false
|
||||
xdebug.log='/var/www/html/xdebug.log'
|
@ -1,16 +1,8 @@
|
||||
.git
|
||||
!.git/HEAD
|
||||
!.git/refs/heads/*
|
||||
.gitattributes
|
||||
.github/*
|
||||
.travis.yml
|
||||
cache/*
|
||||
CONTRIBUTING.md
|
||||
DEBUG
|
||||
Dockerfile
|
||||
phpcompatibility.xml
|
||||
whitelist.txt
|
||||
phpcs.xml
|
||||
phpcs.xml
|
||||
scalingo.json
|
||||
tests/*
|
||||
whitelist.txt
|
||||
CHANGELOG.md
|
||||
CONTRIBUTING.md
|
@ -1,4 +0,0 @@
|
||||
# Reformat code base to PSR12
|
||||
4f75591060d95208a301bc6bf460d875631b29cc
|
||||
# Fix coding style missed by phpbcf
|
||||
951092eef374db048b77bac85e75e3547bfac702
|
66
.gitattributes
vendored
66
.gitattributes
vendored
@ -1,6 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
@ -11,58 +10,13 @@
|
||||
*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
|
||||
# Ignore files in git archive (i.e. GitHub release builds)
|
||||
|
||||
## Docker
|
||||
Dockerfile export-ignore
|
||||
.dockerignore export-ignore
|
||||
|
||||
## Travis
|
||||
.travis.yml export-ignore
|
||||
|
||||
## GitHub
|
||||
.github/ export-ignore
|
||||
|
||||
## Git
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
## Scalingo
|
||||
scalingo.json export-ignore
|
||||
|
||||
## RSS-Bridge
|
||||
phpunit.xml export-ignore
|
||||
phpcs.xml export-ignore
|
||||
phpcompatibility.xml export-ignore
|
||||
tests/ export-ignore
|
||||
cache/.gitkeep export-ignore
|
||||
|
||||
## Composer
|
||||
#
|
||||
# Keep the following lines commented out. Heroku does
|
||||
# not function if the composer files are ignored during
|
||||
# export. For more information see
|
||||
# https://github.com/rss-bridge/rss-bridge/issues/1165
|
||||
#
|
||||
# composer.json export-ignore
|
||||
# composer.lock export-ignore
|
||||
|
||||
## Heroku
|
||||
#
|
||||
# Keep the following line commented out. Heroku does
|
||||
# not function if app.json is ignored during export.
|
||||
# For more information see
|
||||
# https://github.com/rss-bridge/rss-bridge/issues/1165
|
||||
#
|
||||
# app.json export-ignore
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
|
7
.github/.gitignore
vendored
7
.github/.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
|
||||
# Generated files
|
||||
comment*.md
|
||||
comment*.txt
|
||||
*.html
|
7
.github/CONTRIBUTING.md
vendored
7
.github/CONTRIBUTING.md
vendored
@ -1,7 +0,0 @@
|
||||
### Pull request policy
|
||||
|
||||
See the [Pull request policy page on the documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/Pull_Request_policy.html) for more information on the pull request policy.
|
||||
|
||||
### Coding style
|
||||
|
||||
See the [Coding style policy page on the documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/Coding_style_policy.html) for more information on the coding style of the project.
|
64
.github/ISSUE_TEMPLATE/bridge-request.md
vendored
64
.github/ISSUE_TEMPLATE/bridge-request.md
vendored
@ -1,64 +0,0 @@
|
||||
---
|
||||
name: Bridge request
|
||||
about: Use this template for requesting a new bridge
|
||||
title: Bridge request for ...
|
||||
labels: Bridge-Request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Bridge request
|
||||
|
||||
<!--
|
||||
This is a bridge request. Start by adding a descriptive title (i.e. `Bridge request for GitHub`). Use the "Preview" button to see a preview of your request. Make sure your request is complete before submitting!
|
||||
|
||||
Notice: This comment is only visible to you while you work on your request. Please do not remove any of the lines in the template (you may add your own outside the "<!--" and "- ->" lines!)
|
||||
-->
|
||||
|
||||
## General information
|
||||
|
||||
<!--
|
||||
Please describe what you expect from the bridge. Whenever possible provide sample links and screenshots (you can just paste them here) to express your expectations and help others understand your request. If possible, mark relevant areas in your screenshot. Use the following questions for reference:
|
||||
-->
|
||||
|
||||
- _Host URI for the bridge_ (i.e. `https://github.com`):
|
||||
|
||||
- Which information would you like to see?
|
||||
|
||||
|
||||
|
||||
- How should the information be displayed/formatted?
|
||||
|
||||
|
||||
|
||||
- Which of the following parameters do you expect?
|
||||
|
||||
- [X] Title
|
||||
- [X] URI (link to the original article)
|
||||
- [ ] Author
|
||||
- [ ] Timestamp
|
||||
- [X] Content (the content of the article)
|
||||
- [ ] Enclosures (pictures, videos, etc...)
|
||||
- [ ] Categories (categories, tags, etc...)
|
||||
|
||||
## Options
|
||||
|
||||
<!--Select options from the list below. Add your own option if one is missing:-->
|
||||
|
||||
- [ ] Limit number of returned items
|
||||
- _Default limit_: 5
|
||||
- [ ] Load full articles
|
||||
- _Cache articles_ (articles are stored in a local cache on first request): yes
|
||||
- _Cache timeout_ : 24 hours
|
||||
- [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage)
|
||||
- _Timeout_ (default = 5 minutes): 5 minutes
|
||||
|
||||
<!--Be aware that some options might not be available for your specific request due to technical limitations!-->
|
||||
|
||||
<!--
|
||||
## Additional notes
|
||||
|
||||
Keep in mind that opening a request does not guarantee the bridge being implemented! That depends entirely on the interest and time of others to make the bridge for you.
|
||||
|
||||
You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Documentation](https://rss-bridge.github.io/rss-bridge/For_Developers/index.html) developer section.
|
||||
-->
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: Bug-Report
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
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 this project
|
||||
title: ''
|
||||
labels: 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.
|
2
.github/prtester-requirements.txt
vendored
2
.github/prtester-requirements.txt
vendored
@ -1,2 +0,0 @@
|
||||
beautifulsoup4>=4.10.0
|
||||
requests>=2.26.0
|
208
.github/prtester.py
vendored
208
.github/prtester.py
vendored
@ -1,208 +0,0 @@
|
||||
import argparse
|
||||
import requests
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
from typing import Iterable
|
||||
import os
|
||||
import glob
|
||||
import urllib
|
||||
|
||||
# This script is specifically written to be used in automation for https://github.com/RSS-Bridge/rss-bridge
|
||||
#
|
||||
# This will scrape the whitelisted bridges in the current state (port 3000) and the PR state (port 3001) of
|
||||
# RSS-Bridge, generate a feed for each of the bridges and save the output as html files.
|
||||
# It also add a <base> tag with the url of em's public instance, so viewing
|
||||
# the HTML file locally will actually work as designed.
|
||||
|
||||
ARTIFACT_FILE_EXTENSION = '.html'
|
||||
|
||||
class Instance:
|
||||
name = ''
|
||||
url = ''
|
||||
|
||||
def main(instances: Iterable[Instance], with_upload: bool, with_reduced_upload: bool, title: str, output_file: str):
|
||||
start_date = datetime.now()
|
||||
|
||||
prid = os.getenv('PR')
|
||||
artifact_base_url = f'https://rss-bridge.github.io/rss-bridge-tests/prs/{prid}'
|
||||
artifact_directory = os.getcwd()
|
||||
for file in glob.glob(f'*{ARTIFACT_FILE_EXTENSION}', root_dir=artifact_directory):
|
||||
os.remove(file)
|
||||
|
||||
table_rows = []
|
||||
for instance in instances:
|
||||
page = requests.get(instance.url) # Use python requests to grab the rss-bridge main page
|
||||
soup = BeautifulSoup(page.content, "html.parser") # use bs4 to turn the page into soup
|
||||
bridge_cards = soup.select('.bridge-card') # get a soup-formatted list of all bridges on the rss-bridge page
|
||||
table_rows += testBridges(
|
||||
instance=instance,
|
||||
bridge_cards=bridge_cards,
|
||||
with_upload=with_upload,
|
||||
with_reduced_upload=with_reduced_upload,
|
||||
artifact_directory=artifact_directory,
|
||||
artifact_base_url=artifact_base_url) # run the main scraping code with the list of bridges
|
||||
with open(file=output_file, mode='w+', encoding='utf-8') as file:
|
||||
table_rows_value = '\n'.join(sorted(table_rows))
|
||||
file.write(f'''
|
||||
## {title}
|
||||
| Bridge | Context | Status |
|
||||
| - | - | - |
|
||||
{table_rows_value}
|
||||
|
||||
*last change: {start_date.strftime("%A %Y-%m-%d %H:%M:%S")}*
|
||||
'''.strip())
|
||||
|
||||
def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, with_reduced_upload: bool, artifact_directory: str, artifact_base_url: str) -> Iterable:
|
||||
instance_suffix = ''
|
||||
if instance.name:
|
||||
instance_suffix = f' ({instance.name})'
|
||||
table_rows = []
|
||||
for bridge_card in bridge_cards:
|
||||
bridgeid = bridge_card.get('id')
|
||||
bridgeid = bridgeid.split('-')[1] # this extracts a readable bridge name from the bridge metadata
|
||||
print(f'{bridgeid}{instance_suffix}')
|
||||
bridge_name = bridgeid.replace('Bridge', '')
|
||||
context_forms = bridge_card.find_all("form")
|
||||
form_number = 1
|
||||
for context_form in context_forms:
|
||||
# a bridge can have multiple contexts, named 'forms' in html
|
||||
# this code will produce a fully working url that should create a working feed when called
|
||||
# this will create an example feed for every single context, to test them all
|
||||
context_parameters = {}
|
||||
error_messages = []
|
||||
context_name = '*untitled*'
|
||||
context_name_element = context_form.find_previous_sibling('h5')
|
||||
if context_name_element and context_name_element.text.strip() != '':
|
||||
context_name = context_name_element.text
|
||||
parameters = context_form.find_all("input")
|
||||
lists = context_form.find_all("select")
|
||||
# this for/if mess cycles through all available input parameters, checks if it required, then pulls
|
||||
# the default or examplevalue and then combines it all together into the url parameters
|
||||
# if an example or default value is missing for a required attribute, it will throw an error
|
||||
# any non-required fields are not tested!!!
|
||||
for parameter in parameters:
|
||||
parameter_type = parameter.get('type')
|
||||
parameter_name = parameter.get('name')
|
||||
if parameter_type == 'hidden':
|
||||
context_parameters[parameter_name] = parameter.get('value')
|
||||
if parameter_type == 'number' or parameter_type == 'text':
|
||||
if parameter.has_attr('required'):
|
||||
if parameter.get('placeholder') == '':
|
||||
if parameter.get('value') == '':
|
||||
error_messages.append(f'Missing example or default value for parameter "{parameter_name}"')
|
||||
else:
|
||||
context_parameters[parameter_name] = parameter.get('value')
|
||||
else:
|
||||
context_parameters[parameter_name] = parameter.get('placeholder')
|
||||
# same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the url parameters
|
||||
if parameter_type == 'checkbox':
|
||||
if parameter.has_attr('checked'):
|
||||
context_parameters[parameter_name] = 'on'
|
||||
for listing in lists:
|
||||
selectionvalue = ''
|
||||
listname = listing.get('name')
|
||||
cleanlist = []
|
||||
options = listing.find_all('option')
|
||||
for option in options:
|
||||
if 'optgroup' in option.name:
|
||||
cleanlist.extend(option)
|
||||
else:
|
||||
cleanlist.append(option)
|
||||
firstselectionentry = 1
|
||||
for selectionentry in cleanlist:
|
||||
if firstselectionentry:
|
||||
selectionvalue = selectionentry.get('value')
|
||||
firstselectionentry = 0
|
||||
else:
|
||||
if 'selected' in selectionentry.attrs:
|
||||
selectionvalue = selectionentry.get('value')
|
||||
break
|
||||
context_parameters[listname] = selectionvalue
|
||||
artifact_url = 'about:blank'
|
||||
if error_messages:
|
||||
status = '<br>'.join(map(lambda m: f'❌ `{m}`', error_messages))
|
||||
else:
|
||||
# if all example/default values are present, form the full request url, run the request, add a <base> tag with
|
||||
# the url of em's public instance to the response text (so that relative paths work, e.g. to the static css file) and
|
||||
# then save it to a html file.
|
||||
context_parameters.update({
|
||||
'action': 'display',
|
||||
'bridge': bridgeid,
|
||||
'format': 'Html',
|
||||
})
|
||||
request_url = f'{instance.url}/?{urllib.parse.urlencode(context_parameters)}'
|
||||
response = requests.get(request_url)
|
||||
page_text = response.text.replace('<head>','<head><base href="https://rss-bridge.org/bridge01/" target="_blank">')
|
||||
page_text = page_text.encode("utf_8")
|
||||
soup = BeautifulSoup(page_text, "html.parser")
|
||||
status_messages = []
|
||||
if response.status_code != 200:
|
||||
status_messages += [f'❌ `HTTP status {response.status_code} {response.reason}`']
|
||||
else:
|
||||
feed_items = soup.select('.feeditem')
|
||||
feed_items_length = len(feed_items)
|
||||
if feed_items_length <= 0:
|
||||
status_messages += [f'⚠️ `The feed has no items`']
|
||||
elif feed_items_length == 1 and len(soup.select('.error')) > 0:
|
||||
status_messages += [f'❌ `{getFirstLine(feed_items[0].text)}`']
|
||||
status_messages += map(lambda e: f'❌ `{getFirstLine(e.text)}`', soup.select('.error .error-type') + soup.select('.error .error-message'))
|
||||
for item_element in soup.select('.feeditem'): # remove all feed items to not accidentally selected <pre> tags from item content
|
||||
item_element.decompose()
|
||||
status_messages += map(lambda e: f'⚠️ `{getFirstLine(e.text)}`', soup.find_all('pre'))
|
||||
status_messages = list(dict.fromkeys(status_messages)) # remove duplicates
|
||||
status = '<br>'.join(status_messages)
|
||||
status_is_ok = status == '';
|
||||
if status_is_ok:
|
||||
status = '✔️'
|
||||
if with_upload and (not with_reduced_upload or not status_is_ok):
|
||||
filename = f'{bridge_name} {form_number}{instance_suffix}{ARTIFACT_FILE_EXTENSION}'
|
||||
filename = re.sub(r'[^a-z0-9 \_\-\.]', '', filename, flags=re.I).replace(' ', '_')
|
||||
with open(file=f'{artifact_directory}/{filename}', mode='wb') as file:
|
||||
file.write(page_text)
|
||||
artifact_url = f'{artifact_base_url}/{filename}'
|
||||
table_rows.append(f'| {bridge_name} | [{form_number} {context_name}{instance_suffix}]({artifact_url}) | {status} |')
|
||||
form_number += 1
|
||||
return table_rows
|
||||
|
||||
def getFirstLine(value: str) -> str:
|
||||
# trim whitespace and remove text that can break the table or is simply unnecessary
|
||||
clean_value = re.sub(r'^\[[^\]]+\]\s*rssbridge\.|[\|`]', '', value.strip())
|
||||
first_line = next(iter(clean_value.splitlines()), '')
|
||||
max_length = 250
|
||||
if (len(first_line) > max_length):
|
||||
first_line = first_line[:max_length] + '...'
|
||||
return first_line
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--instances', nargs='+')
|
||||
parser.add_argument('--no-upload', action='store_true')
|
||||
parser.add_argument('--reduced-upload', action='store_true')
|
||||
parser.add_argument('--title', default='Pull request artifacts')
|
||||
parser.add_argument('--output-file', default=os.getcwd() + '/comment.txt')
|
||||
args = parser.parse_args()
|
||||
instances = []
|
||||
if args.instances:
|
||||
for instance_arg in args.instances:
|
||||
instance_arg_parts = instance_arg.split('::')
|
||||
instance = Instance()
|
||||
instance.name = instance_arg_parts[1].strip() if len(instance_arg_parts) >= 2 else ''
|
||||
instance.url = instance_arg_parts[0].strip().rstrip("/")
|
||||
instances.append(instance)
|
||||
else:
|
||||
instance = Instance()
|
||||
instance.name = 'current'
|
||||
instance.url = 'http://localhost:3000'
|
||||
instances.append(instance)
|
||||
instance = Instance()
|
||||
instance.name = 'pr'
|
||||
instance.url = 'http://localhost:3001'
|
||||
instances.append(instance)
|
||||
main(
|
||||
instances=instances,
|
||||
with_upload=not args.no_upload,
|
||||
with_reduced_upload=args.reduced_upload and not args.no_upload,
|
||||
title=args.title,
|
||||
output_file=args.output_file
|
||||
);
|
61
.github/workflows/dockerbuild.yml
vendored
61
.github/workflows/dockerbuild.yml
vendored
@ -1,61 +0,0 @@
|
||||
name: Build Image on Commit and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
tags:
|
||||
- '20*'
|
||||
|
||||
env:
|
||||
DOCKERHUB_SLUG: rssbridge/rss-bridge
|
||||
GHCR_SLUG: ghcr.io/rss-bridge/rss-bridge
|
||||
|
||||
jobs:
|
||||
bake:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_SLUG }}
|
||||
${{ env.GHCR_SLUG }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=sha
|
||||
type=ref,event=tag,enable=${{ startsWith(github.ref, 'refs/tags/20') }}
|
||||
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/20') }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/bake-action@v5
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
${{ steps.docker_meta.outputs.bake-file }}
|
||||
targets: image-all
|
||||
push: true
|
27
.github/workflows/documentation.yml
vendored
27
.github/workflows/documentation.yml
vendored
@ -1,27 +0,0 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
documentation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.0
|
||||
- name: Install dependencies
|
||||
run: composer global require daux/daux.io
|
||||
- name: Generate documentation
|
||||
run: daux generate
|
||||
- name: Deploy same repository 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
folder: "static"
|
||||
branch: gh-pages
|
49
.github/workflows/lint.yml
vendored
49
.github/workflows/lint.yml
vendored
@ -1,49 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
phpcs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpcs
|
||||
- run: phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p
|
||||
|
||||
phpcompatibility:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
- run: composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
|
||||
- run: composer global require dealerdirect/phpcodesniffer-composer-installer
|
||||
- run: composer global require phpcompatibility/php-compatibility
|
||||
- run: ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p
|
||||
|
||||
executable_php_files_check:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
if find -name "*.php" -executable -type f -print -exec false {} +
|
||||
then
|
||||
echo 'Good, no executable php scripts found'
|
||||
else
|
||||
echo 'Please unmark php scripts above as non-executable'
|
||||
exit 1
|
||||
fi
|
126
.github/workflows/prhtmlgenerator.yml
vendored
126
.github/workflows/prhtmlgenerator.yml
vendored
@ -1,126 +0,0 @@
|
||||
name: 'PR Testing'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
check-bridges:
|
||||
name: Check if bridges were changed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
BRIDGES: ${{ steps.check1.outputs.BRIDGES }}
|
||||
steps:
|
||||
- name: Check number of bridges
|
||||
id: check1
|
||||
run: |
|
||||
PR=${{github.event.number}};
|
||||
wget https://patch-diff.githubusercontent.com/raw/$GITHUB_REPOSITORY/pull/$PR.patch;
|
||||
bridgeamount=$(cat $PR.patch | grep "\bbridges/[A-Za-z0-9]*Bridge\.php\b" | sed "s=.*\bbridges/\([A-Za-z0-9]*\)Bridge\.php\b.*=\1=g" | sort | uniq | wc -l);
|
||||
echo "BRIDGES=$bridgeamount" >> "$GITHUB_OUTPUT"
|
||||
test-pr:
|
||||
name: Generate HTML
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-bridges
|
||||
if: needs.check-bridges.outputs.BRIDGES > 0
|
||||
env:
|
||||
PYTHONUNBUFFERED: 1
|
||||
# Needs additional permissions https://github.com/actions/first-interaction/issues/10#issuecomment-1041402989
|
||||
steps:
|
||||
- name: Check out self
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- name: Check out rss-bridge
|
||||
run: |
|
||||
PR=${{github.event.number}};
|
||||
wget -O requirements.txt https://raw.githubusercontent.com/$GITHUB_REPOSITORY/${{ github.event.pull_request.base.ref }}/.github/prtester-requirements.txt;
|
||||
wget https://raw.githubusercontent.com/$GITHUB_REPOSITORY/${{ github.event.pull_request.base.ref }}/.github/prtester.py;
|
||||
wget https://patch-diff.githubusercontent.com/raw/$GITHUB_REPOSITORY/pull/$PR.patch;
|
||||
touch DEBUG;
|
||||
cat $PR.patch | grep "\bbridges/[A-Za-z0-9]*Bridge\.php\b" | sed "s=.*\bbridges/\([A-Za-z0-9]*\)Bridge\.php\b.*=\1=g" | sort | uniq > whitelist.txt
|
||||
- name: Start Docker - Current
|
||||
run: |
|
||||
docker run -d -v $GITHUB_WORKSPACE/whitelist.txt:/app/whitelist.txt -v $GITHUB_WORKSPACE/DEBUG:/app/DEBUG -p 3000:80 ghcr.io/rss-bridge/rss-bridge:latest
|
||||
- name: Start Docker - PR
|
||||
run: |
|
||||
docker build -t prbuild .;
|
||||
docker run -d -v $GITHUB_WORKSPACE/whitelist.txt:/app/whitelist.txt -v $GITHUB_WORKSPACE/DEBUG:/app/DEBUG -p 3001:80 prbuild
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
- name: Install requirements
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
pip install -r requirements.txt
|
||||
- name: Run bridge tests
|
||||
id: testrun
|
||||
run: |
|
||||
mkdir results;
|
||||
python prtester.py;
|
||||
body="$(cat comment.txt)";
|
||||
body="${body//'%'/'%25'}";
|
||||
body="${body//$'\n'/'%0A'}";
|
||||
body="${body//$'\r'/'%0D'}";
|
||||
echo "bodylength=${#body}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
PR: ${{ github.event.number }}
|
||||
- name: Upload generated tests
|
||||
uses: actions/upload-artifact@v4
|
||||
id: upload-generated-tests
|
||||
with:
|
||||
name: tests
|
||||
path: '*.html'
|
||||
- name: Find Comment
|
||||
if: ${{ steps.testrun.outputs.bodylength > 130 }}
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Pull request artifacts
|
||||
- name: Create or update comment
|
||||
if: ${{ steps.testrun.outputs.bodylength > 130 }}
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-file: comment.txt
|
||||
edit-mode: replace
|
||||
upload_tests:
|
||||
name: Upload tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-pr
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'RSS-Bridge/rss-bridge-tests'
|
||||
ref: 'main'
|
||||
token: ${{ secrets.RSSTESTER_ACTION }}
|
||||
|
||||
- name: Setup git config
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "<>"
|
||||
|
||||
- name: Download tests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tests
|
||||
|
||||
- name: Move tests
|
||||
run: |
|
||||
cd prs
|
||||
mkdir -p ${{github.event.number}}
|
||||
cd ${{github.event.number}}
|
||||
mv -f $GITHUB_WORKSPACE/*.html .
|
||||
|
||||
- name: Commit and push generated tests
|
||||
run: |
|
||||
export COMMIT_MESSAGE="Added tests for PR ${{github.event.number}}"
|
||||
git add .
|
||||
git commit -m "$COMMIT_MESSAGE"
|
||||
git push
|
23
.github/workflows/tests.yml
vendored
23
.github/workflows/tests.yml
vendored
@ -1,23 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
phpunit8:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0', '8.1']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
env:
|
||||
update: true
|
||||
- run: composer install
|
||||
- run: composer test
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -6,6 +6,7 @@ data/
|
||||
*.pydevproject
|
||||
.project
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
@ -212,7 +213,6 @@ pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.phpunit.result.cache
|
||||
.tox
|
||||
|
||||
#Translations
|
||||
@ -228,10 +228,6 @@ pip-log.txt
|
||||
/whitelist.txt
|
||||
DEBUG
|
||||
config.ini.php
|
||||
config/*
|
||||
!config/nginx.conf
|
||||
!config/php-fpm.conf
|
||||
!config/php.ini
|
||||
|
||||
######################
|
||||
## VisualStudioCode ##
|
||||
@ -240,10 +236,3 @@ config/*
|
||||
|
||||
#Builder
|
||||
.buildconfig
|
||||
|
||||
#Auth
|
||||
.htaccess
|
||||
.htpasswd
|
||||
|
||||
#Crawler
|
||||
robots.txt
|
||||
|
38
.travis.yml
Normal file
38
.travis.yml
Normal file
@ -0,0 +1,38 @@
|
||||
dist: trusty
|
||||
sudo: false
|
||||
language: php
|
||||
|
||||
install:
|
||||
- if [[ $TRAVIS_PHP_VERSION == "hhvm" ]]; then
|
||||
composer global require squizlabs/PHP_CodeSniffer;
|
||||
else
|
||||
pear channel-update pear.php.net;
|
||||
pear install PHP_CodeSniffer;
|
||||
fi
|
||||
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
|
||||
composer global require phpunit/phpunit ^6;
|
||||
fi
|
||||
|
||||
script:
|
||||
- phpenv rehash
|
||||
- if [[ $TRAVIS_PHP_VERSION == "hhvm" ]]; then
|
||||
/home/travis/.composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
|
||||
else
|
||||
phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
|
||||
fi
|
||||
- if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
|
||||
phpunit --configuration=phpunit.xml --include-path=lib/;
|
||||
fi
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
include:
|
||||
- php: 5.6
|
||||
- php: 7.0
|
||||
- php: hhvm
|
||||
- php: nightly
|
||||
|
||||
allow_failures:
|
||||
- php: hhvm
|
||||
- php: nightly
|
263
CHANGELOG.md
Normal file
263
CHANGELOG.md
Normal file
@ -0,0 +1,263 @@
|
||||
rss-bridge Changelog
|
||||
===
|
||||
|
||||
RSS-Bridge 2017-08-19
|
||||
==
|
||||
|
||||
## General changes
|
||||
* whitelist: Do case-insensitive whitelist matching
|
||||
* [FeedExpander] Fix Serialization of 'SimpleXMLElement' is not allowed
|
||||
* [FeedExpander] Remove whitespace from source content
|
||||
* [index] Add GET parameter 'q' for search queries
|
||||
- **Example**: You can now add `&q=Twitter` to load into the search field
|
||||
* [index] Check permissions for cache folder and whitelist file
|
||||
* [index] Show bridge options when loading with URL fragment
|
||||
- **Example**: You can now add `#bridge-Twitter` to load the card with all
|
||||
parameters visible
|
||||
* [style] Center search cursor and hide placeholder
|
||||
* [validation] Fix error on undefined optional numeric value
|
||||
|
||||
## Modified bridges
|
||||
* [DanbooruBridge] Allow descendant classes to override tag collection
|
||||
* [DribbbleBridge] Add dribble bridge listing last dribble popular shots (#558)
|
||||
* [FacebookBridge] Fix & in URLs
|
||||
* [GelbooruBridge] Fix bridge not getting tags correctly
|
||||
* [GoComicsBridge] Fix for page structure changes (#568)
|
||||
* [LeBonCoinBridge] Fix bridge is marked executable
|
||||
* [LWNprevBridge] Fix everchanging url
|
||||
* [YoutubeBridge] Fix error on certain keywords
|
||||
* [YoutubeBridge] Fix issues loading playlists
|
||||
|
||||
## Removed bridges
|
||||
* VineBridge
|
||||
|
||||
RSS-Bridge 2017-08-03
|
||||
==
|
||||
|
||||
## Important changes
|
||||
* RSS-Bridge now has [contribution guidelines](CONTRIBUTING.md)
|
||||
* [phpcs rules](phpcs.xml) follow the [contribution guidelines](CONTRIBUTING.md)
|
||||
|
||||
## General changes
|
||||
* Added a search bar to make searching for bridges easier
|
||||
* Added user friendly error page for when a bridge fails
|
||||
* Added caching of extraInfos (name, uri)
|
||||
* Added an indicator to warn for bridges using HTTP instead of HTTPS
|
||||
* Various bug fixes and improvements
|
||||
|
||||
## Modified bridges
|
||||
* AllocineFRBridge] Update Faux Raccord link
|
||||
* [DanbooruBridge] Fix broken URI
|
||||
* [DuckDuckGoBridge] Disable DuckDuckGo redirects so that the links returned are correct.
|
||||
* [FacebookBridge] Add option to hide posts with facebook videos
|
||||
* [FacebookBridge] Add requester languages to HTTP header
|
||||
* [FacebookBridge] Handle summary posts
|
||||
* [FacebookBridge] Replace 'novideo' with 'media_type'
|
||||
* [FilterBridge] Initial implementation of basic title permit and block
|
||||
* [FlickrTagBridge] Fix and improve bridge by using the FlickrExploreBridge approach
|
||||
* [GooglePlusPostBridge] Autofix user names
|
||||
* [GooglePlusPostBridge] Fix bridge implementation
|
||||
* [GooglePlusPostBridge] Fix content loading
|
||||
* [InstagramBridge] Add option to filter for videos and pictures
|
||||
* [LWNprevBridge] full rewrite
|
||||
* [MangareaderBridge] Fix double forward slashes
|
||||
* [NasaApodBridge] Use HTTPS instead of HTTP
|
||||
* [PinterestBridge] Fix checkbox not working
|
||||
* [PinterestBridge] Fix implementation after DOM changes
|
||||
* [RTBFBridge] Update URI
|
||||
* [SexactuBridge] Fix URI and timestamp
|
||||
* [SexactuBridge] Use most modern version of bridge api and cached pages (#504)
|
||||
* [ShanaprojectBridge] Don't throw error if timestamp is missing
|
||||
* [TwitterBridge] Add option to hide retweets
|
||||
* [TwitterBridge] Avoid empty content caused by new login policy
|
||||
* [TwitterBridge] Fix double slashes in URI
|
||||
* [TwitterBridge] Fix missing spaces
|
||||
* [TwitterBridge] Fix title includes anchors in plaintext format
|
||||
* [TwitterBridge] ignore promoted tweets
|
||||
* [TwitterBridge] Optimize returned image sizes
|
||||
* [TwitterBridge] Show quotes and pictures
|
||||
* [WebfailBridge] Properly handle gifs (DOM changed)
|
||||
* [YoutubeBridge] Improve readability of feed contents
|
||||
* [YoutubeBridge] Improve URL handling in video descriptions
|
||||
|
||||
## New bridges
|
||||
* AmazonBridge
|
||||
* DiceBridge
|
||||
* EtsyBridge
|
||||
* FB2Bridge
|
||||
* FilterBridge
|
||||
* FlickrBridge
|
||||
* GithubSearchBridge
|
||||
* GoComicsBridge
|
||||
* KATBridge
|
||||
* KernelBugTrackerBridge
|
||||
* MixCloudBridge
|
||||
* MoinMoinBridge
|
||||
* RainbowSixSiegeBridge
|
||||
* SteamBridge
|
||||
* TheTVDBBridge
|
||||
* Torrent9Bridge
|
||||
* UsbekEtRicaBridge
|
||||
* WikiLeaksBridge
|
||||
* WordPressPluginUpdateBridge
|
||||
|
||||
Alpha 0.2
|
||||
===
|
||||
|
||||
## Important changes
|
||||
* RSS-Bridge has been [UNLICENSED](UNLICENSE)
|
||||
* RSS-Bridge is now a community-managed project on [GitHub](https://github.com/rss-bridge/rss-bridge)
|
||||
* RSS-Bridge now has a [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
|
||||
* RSS-Bridge now supports [Travis-CI](https://travis-ci.org)
|
||||
|
||||
## General changes
|
||||
* Added [CHANGELOG](CHANGELOG.md) (this file)
|
||||
* Added [PHP Simple HTML DOM Parser](http://simplehtmldom.sourceforge.net) to [vendor](vendor/simplehtmldom/)
|
||||
* Added cache purging function (cache will be force-purged after 24 hours or as defined by bridge)
|
||||
* Added new format [MrssFormat](formats/MrssFormat.php)
|
||||
* Added parameter `author` - for display of the feed author name - to all formats
|
||||
* Added new abstraction of the BridgeInterface:
|
||||
- [FeedExpander](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API)
|
||||
* Added optional support for proxy usage on each individual bridge
|
||||
* Added support for [custom bridge parameter](https://github.com/RSS-Bridge/rss-bridge/wiki/BridgeAbstract#format-specifications) (text, number, list, checkbox)
|
||||
* Changed design of the welcome screen
|
||||
* Changed design of HtmlFormat
|
||||
* Changed behavior of debug mode:
|
||||
- Enable debug mode by placing a file called "DEBUG" in the root folder
|
||||
- Debug mode automatically disables cache file loading
|
||||
* Changed implementation of bridges - see [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
|
||||
- Changed comment-style metadata to constants
|
||||
- Added support for multiple utilizations per bridge
|
||||
- Changed the parameter loading algorithm to be loaded by RSS-Bridge core
|
||||
* Improved checks for PHP version, configuration and extensions
|
||||
* Many bug fixes
|
||||
|
||||
## Modified Bridges
|
||||
* FlickrExploreBridge
|
||||
* GoogleSearchBridge
|
||||
* TwitterBridge
|
||||
|
||||
## New Bridges
|
||||
* ABCTabsBridge
|
||||
* AcrimedBridge
|
||||
* AllocineFRBridge
|
||||
* AnimeUltimeBridge
|
||||
* Arte7Bridge
|
||||
* AskfmBridge
|
||||
* BandcampBridge
|
||||
* BastaBridge
|
||||
* BlaguesDeMerdeBridge
|
||||
* BooruprojectBridge
|
||||
* CADBridge
|
||||
* CNETBridge
|
||||
* CastorusBridge
|
||||
* CollegeDeFranceBridge
|
||||
* CommonDreamsBridge
|
||||
* CopieDoubleBridge
|
||||
* CourrierInternationalBridge
|
||||
* CpasbienBridge
|
||||
* CryptomeBridge
|
||||
* DailymotionBridge
|
||||
* DanbooruBridge
|
||||
* DansTonChatBridge
|
||||
* DauphineLibereBridge
|
||||
* DemoBridge
|
||||
* DeveloppezDotComBridge
|
||||
* DilbertBridge
|
||||
* DollbooruBridge
|
||||
* DuckDuckGoBridge
|
||||
* EZTVBridge
|
||||
* EliteDangerousGalnetBridge
|
||||
* ElsevierBridge
|
||||
* EstCeQuonMetEnProdBridge
|
||||
* FacebookBridge
|
||||
* FierPandaBridge
|
||||
* FlickrTagBridge
|
||||
* FootitoBridge
|
||||
* FourchanBridge
|
||||
* FuturaSciencesBridge
|
||||
* GBAtempBridge
|
||||
* GelbooruBridge
|
||||
* GiphyBridge
|
||||
* GithubIssueBridge
|
||||
* GizmodoBridge
|
||||
* GooglePlusPostBridge
|
||||
* HDWallpapersBridge
|
||||
* HentaiHavenBridge
|
||||
* IdenticaBridge
|
||||
* InstagramBridge
|
||||
* IsoHuntBridge
|
||||
* JapanExpoBridge
|
||||
* KonachanBridge
|
||||
* KoreusBridge
|
||||
* KununuBridge
|
||||
* LWNprevBridge
|
||||
* LeBonCoinBridge
|
||||
* LegifranceJOBridge
|
||||
* LeMondeInformatiqueBridge
|
||||
* LesJoiesDuCodeBridge
|
||||
* LichessBridge
|
||||
* LinkedInCompanyBridge
|
||||
* LolibooruBridge
|
||||
* MangareaderBridge
|
||||
* MilbooruBridge
|
||||
* MoebooruBridge
|
||||
* MondeDiploBridge
|
||||
* MsnMondeBridge
|
||||
* MspabooruBridge
|
||||
* NasaApodBridge
|
||||
* NeuviemeArtBridge
|
||||
* NextInpactBridge
|
||||
* NextgovBridge
|
||||
* NiceMatinBridge
|
||||
* NovelUpdatesBridge
|
||||
* OpenClassroomsBridge
|
||||
* ParuVenduImmoBridge
|
||||
* PickyWallpapersBridge
|
||||
* PinterestBridge
|
||||
* PlanetLibreBridge
|
||||
* RTBFBridge
|
||||
* ReadComicsBridge
|
||||
* Releases3DSBridge
|
||||
* ReporterreBridge
|
||||
* Rue89Bridge
|
||||
* Rule34Bridge
|
||||
* Rule34pahealBridge
|
||||
* SafebooruBridge
|
||||
* SakugabooruBridge
|
||||
* ScmbBridge
|
||||
* ScoopItBridge
|
||||
* SensCritiqueBridge
|
||||
* SexactuBridge
|
||||
* ShanaprojectBridge
|
||||
* Shimmie2Bridge
|
||||
* SoundcloudBridge
|
||||
* StripeAPIChangeLogBridge
|
||||
* SuperbWallpapersBridge
|
||||
* T411Bridge
|
||||
* TagBoardBridge
|
||||
* TbibBridge
|
||||
* TheCodingLoveBridge
|
||||
* TheHackerNewsBridge
|
||||
* ThePirateBayBridge
|
||||
* UnsplashBridge
|
||||
* ViadeoCompanyBridge
|
||||
* VineBridge
|
||||
* VkBridge
|
||||
* WallpaperStopBridge
|
||||
* WebfailBridge
|
||||
* WeLiveSecurityBridge
|
||||
* WhydBridge
|
||||
* WikipediaBridge
|
||||
* WordPressBridge
|
||||
* WorldOfTanksBridge
|
||||
* XbooruBridge
|
||||
* YandereBridge
|
||||
* YoutubeBridge
|
||||
* ZDNetBridge
|
||||
|
||||
Alpha 0.1
|
||||
===
|
||||
* First tagged version.
|
||||
* Includes refactoring.
|
||||
* Unstable.
|
47
CONTRIBUTING.md
Normal file
47
CONTRIBUTING.md
Normal file
@ -0,0 +1,47 @@
|
||||
### Pull request policy
|
||||
|
||||
* [Fix one issue per pull request](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#fix-one-issue-per-pull-request)
|
||||
* [Respect the coding style policy](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#respect-the-coding-style-policy)
|
||||
* [Properly name your commits](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#properly-name-your-commits)
|
||||
* When fixing a bridge (located in the `bridges` directory), write `[BridgeName] Feature` <br>(i.e. `[YoutubeBridge] Fix typo in video titles`).
|
||||
* When fixing other files, use `[FileName] Feature` <br>(i.e. `[index.php] Add multilingual support`).
|
||||
* When fixing a general problem that applies to multiple files, write `category: feature` <br>(i.e. `bridges: Fix various typos`).
|
||||
|
||||
Note that all pull-requests must pass all tests before they can be merged.
|
||||
|
||||
### Coding style
|
||||
|
||||
* [Whitespace](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace)
|
||||
* [Add a new line at the end of a file](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
|
||||
* [Do not add a whitespace before a semicolon](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
|
||||
* [Do not add whitespace at start or end of a file or end of a line](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#do-not-add-whitespace-at-start-or-end-of-a-file-or-end-of-a-line)
|
||||
* [Indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation)
|
||||
* [Use tabs for indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation#use-tabs-for-indentation)
|
||||
* [Maximum line length](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length)
|
||||
* [The maximum line length should not exceed 80 characters](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length#the-maximum-line-length-should-not-exceed-80-characters)
|
||||
* [Strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings)
|
||||
* [Whenever possible use single quoted strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#whenever-possible-use-single-quote-strings)
|
||||
* [Add spaces around the concatenation operator](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#add-spaces-around-the-concatenation-operator)
|
||||
* [Use a single string instead of concatenating](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#use-a-single-string-instead-of-concatenating)
|
||||
* [Constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants)
|
||||
* [Use UPPERCASE for constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants#use-uppercase-for-constants)
|
||||
* [Keywords](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords)
|
||||
* [Use lowercase for `true`, `false` and `null`](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords#use-lowercase-for-true-false-and-null)
|
||||
* [Operators](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators)
|
||||
* [Operators must have a space around them](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators#operators-must-have-a-space-around-them)
|
||||
* [Functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions)
|
||||
* [Parameters with default values must appear last in functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#parameters-with-default-values-must-appear-last-in-functions)
|
||||
* [Calling functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#calling-functions)
|
||||
* [Do not add spaces after opening or before closing bracket](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#do-not-add-spaces-after-opening-or-before-closing-bracket)
|
||||
* [Structures](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures)
|
||||
* [Structures must always be formatted as multi-line blocks](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures#structures-must-always-be-formatted-as-multi-line-blocks)
|
||||
* [If-Statement](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement)
|
||||
* [Use `elseif` instead of `else if`](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#use-elseif-instead-of-else-if)
|
||||
* [Do not write empty statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-empty-statements)
|
||||
* [Do not write unconditional if-statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-unconditional-if-statements)
|
||||
* [Classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes)
|
||||
* [Use PascalCase for class names](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#use-pascalcase-for-class-names)
|
||||
* [Do not use final statements inside final classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-use-final-statements-inside-final-classes)
|
||||
* [Do not override methods to call their parent](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-override-methods-to-call-their-parent)
|
||||
* [Casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting)
|
||||
* [Do not add spaces when casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting#do-not-add-spaces-when-casting)
|
225
CONTRIBUTORS.md
225
CONTRIBUTORS.md
@ -1,225 +0,0 @@
|
||||
# Contributors
|
||||
|
||||
* [16mhz](https://github.com/16mhz)
|
||||
* [adamchainz](https://github.com/adamchainz)
|
||||
* [Ahiles3005](https://github.com/Ahiles3005)
|
||||
* [akirk](https://github.com/akirk)
|
||||
* [Albirew](https://github.com/Albirew)
|
||||
* [aledeg](https://github.com/aledeg)
|
||||
* [alex73](https://github.com/alex73)
|
||||
* [alexAubin](https://github.com/alexAubin)
|
||||
* [Alkarex](https://github.com/Alkarex)
|
||||
* [AmauryCarrade](https://github.com/AmauryCarrade)
|
||||
* [arnd-s](https://github.com/arnd-s)
|
||||
* [ArthurHoaro](https://github.com/ArthurHoaro)
|
||||
* [Astalaseven](https://github.com/Astalaseven)
|
||||
* [Astyan-42](https://github.com/Astyan-42)
|
||||
* [austinhuang0131](https://github.com/austinhuang0131)
|
||||
* [axor-mst](https://github.com/axor-mst)
|
||||
* [ayacoo](https://github.com/ayacoo)
|
||||
* [az5he6ch](https://github.com/az5he6ch)
|
||||
* [b1nj](https://github.com/b1nj)
|
||||
* [benasse](https://github.com/benasse)
|
||||
* [Binnette](https://github.com/Binnette)
|
||||
* [BoboTiG](https://github.com/BoboTiG)
|
||||
* [Bockiii](https://github.com/Bockiii)
|
||||
* [brtsos](https://github.com/brtsos)
|
||||
* [captn3m0](https://github.com/captn3m0)
|
||||
* [chemel](https://github.com/chemel)
|
||||
* [Chouchen](https://github.com/Chouchen)
|
||||
* [ckiw](https://github.com/ckiw)
|
||||
* [cn-tools](https://github.com/cn-tools)
|
||||
* [cnlpete](https://github.com/cnlpete)
|
||||
* [corenting](https://github.com/corenting)
|
||||
* [couraudt](https://github.com/couraudt)
|
||||
* [csisoap](https://github.com/csisoap)
|
||||
* [da2x](https://github.com/da2x)
|
||||
* [dabenzel](https://github.com/dabenzel)
|
||||
* [Daiyousei](https://github.com/Daiyousei)
|
||||
* [dawidsowa](https://github.com/dawidsowa)
|
||||
* [DevonHess](https://github.com/DevonHess)
|
||||
* [dhuschde](https://github.com/dhuschde)
|
||||
* [disk0x](https://github.com/disk0x)
|
||||
* [DJCrashdummy](https://github.com/DJCrashdummy)
|
||||
* [Djuuu](https://github.com/Djuuu)
|
||||
* [DnAp](https://github.com/DnAp)
|
||||
* [dominik-th](https://github.com/dominik-th)
|
||||
* [Draeli](https://github.com/Draeli)
|
||||
* [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
|
||||
* [drego85](https://github.com/drego85)
|
||||
* [drklee3](https://github.com/drklee3)
|
||||
* [DRogueRonin](https://github.com/DRogueRonin)
|
||||
* [dvikan](https://github.com/dvikan)
|
||||
* [eggwhalefrog](https://github.com/eggwhalefrog)
|
||||
* [em92](https://github.com/em92)
|
||||
* [eMerzh](https://github.com/eMerzh)
|
||||
* [EtienneM](https://github.com/EtienneM)
|
||||
* [f0086](https://github.com/f0086)
|
||||
* [fanch317](https://github.com/fanch317)
|
||||
* [fatuuse](https://github.com/fatuuse)
|
||||
* [fivefilters](https://github.com/fivefilters)
|
||||
* [floviolleau](https://github.com/floviolleau)
|
||||
* [fluffy-critter](https://github.com/fluffy-critter)
|
||||
* [fmachen](https://github.com/fmachen)
|
||||
* [Frenzie](https://github.com/Frenzie)
|
||||
* [fulmeek](https://github.com/fulmeek)
|
||||
* [ggiessen](https://github.com/ggiessen)
|
||||
* [gileri](https://github.com/gileri)
|
||||
* [Ginko-Aloe](https://github.com/Ginko-Aloe)
|
||||
* [girlpunk](https://github.com/girlpunk)
|
||||
* [Glandos](https://github.com/Glandos)
|
||||
* [gloony](https://github.com/gloony)
|
||||
* [GregThib](https://github.com/GregThib)
|
||||
* [griffaurel](https://github.com/griffaurel)
|
||||
* [Grummfy](https://github.com/Grummfy)
|
||||
* [gsantner](https://github.com/gsantner)
|
||||
* [guigot](https://github.com/guigot)
|
||||
* [hollowleviathan](https://github.com/hollowleviathan)
|
||||
* [hpacleb](https://github.com/hpacleb)
|
||||
* [hunhejj](https://github.com/hunhejj)
|
||||
* [husim0](https://github.com/husim0)
|
||||
* [IceWreck](https://github.com/IceWreck)
|
||||
* [imagoiq](https://github.com/imagoiq)
|
||||
* [j0k3r](https://github.com/j0k3r)
|
||||
* [JackNUMBER](https://github.com/JackNUMBER)
|
||||
* [jacquesh](https://github.com/jacquesh)
|
||||
* [jakubvalenta](https://github.com/jakubvalenta)
|
||||
* [JasonGhent](https://github.com/JasonGhent)
|
||||
* [jcgoette](https://github.com/jcgoette)
|
||||
* [jdesgats](https://github.com/jdesgats)
|
||||
* [jdigilio](https://github.com/jdigilio)
|
||||
* [JeremyRand](https://github.com/JeremyRand)
|
||||
* [JimDog546](https://github.com/JimDog546)
|
||||
* [jNullj](https://github.com/jNullj)
|
||||
* [Jocker666z](https://github.com/Jocker666z)
|
||||
* [johnnygroovy](https://github.com/johnnygroovy)
|
||||
* [johnpc](https://github.com/johnpc)
|
||||
* [joni1993](https://github.com/joni1993)
|
||||
* [jtojnar](https://github.com/jtojnar)
|
||||
* [KamaleiZestri](https://github.com/KamaleiZestri)
|
||||
* [kkoyung](https://github.com/kkoyung)
|
||||
* [klimplant](https://github.com/klimplant)
|
||||
* [KN4CK3R](https://github.com/KN4CK3R)
|
||||
* [kolarcz](https://github.com/kolarcz)
|
||||
* [kranack](https://github.com/kranack)
|
||||
* [kraoc](https://github.com/kraoc)
|
||||
* [krisu5](https://github.com/krisu5)
|
||||
* [l1n](https://github.com/l1n)
|
||||
* [laBecasse](https://github.com/laBecasse)
|
||||
* [lagaisse](https://github.com/lagaisse)
|
||||
* [lalannev](https://github.com/lalannev)
|
||||
* [langfingaz](https://github.com/langfingaz)
|
||||
* [lassana](https://github.com/lassana)
|
||||
* [ldidry](https://github.com/ldidry)
|
||||
* [Leomaradan](https://github.com/Leomaradan)
|
||||
* [leyrer](https://github.com/leyrer)
|
||||
* [liamka](https://github.com/liamka)
|
||||
* [Limero](https://github.com/Limero)
|
||||
* [LogMANOriginal](https://github.com/LogMANOriginal)
|
||||
* [lorenzos](https://github.com/lorenzos)
|
||||
* [lukasklinger](https://github.com/lukasklinger)
|
||||
* [m0zes](https://github.com/m0zes)
|
||||
* [Mar-Koeh](https://github.com/Mar-Koeh)
|
||||
* [marcus-at-localhost](https://github.com/marcus-at-localhost)
|
||||
* [marius8510000-bot](https://github.com/marius8510000-bot)
|
||||
* [matthewseal](https://github.com/matthewseal)
|
||||
* [mcbyte-it](https://github.com/mcbyte-it)
|
||||
* [mdemoss](https://github.com/mdemoss)
|
||||
* [melangue](https://github.com/melangue)
|
||||
* [metaMMA](https://github.com/metaMMA)
|
||||
* [mibe](https://github.com/mibe)
|
||||
* [mickaelBert](https://github.com/mickaelBert)
|
||||
* [mightymt](https://github.com/mightymt)
|
||||
* [mitsukarenai](https://github.com/mitsukarenai)
|
||||
* [Monocularity](https://github.com/Monocularity)
|
||||
* [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
|
||||
* [mr-flibble](https://github.com/mr-flibble)
|
||||
* [mro](https://github.com/mro)
|
||||
* [mschwld](https://github.com/mschwld)
|
||||
* [muekoeff](https://github.com/muekoeff)
|
||||
* [mw80](https://github.com/mw80)
|
||||
* [mxmehl](https://github.com/mxmehl)
|
||||
* [Mynacol](https://github.com/Mynacol)
|
||||
* [nel50n](https://github.com/nel50n)
|
||||
* [niawag](https://github.com/niawag)
|
||||
* [Niehztog](https://github.com/Niehztog)
|
||||
* [NikNikYkt](https://github.com/NikNikYkt)
|
||||
* [Nono-m0le](https://github.com/Nono-m0le)
|
||||
* [NotsoanoNimus](https://github.com/NotsoanoNimus)
|
||||
* [obsiwitch](https://github.com/obsiwitch)
|
||||
* [Ololbu](https://github.com/Ololbu)
|
||||
* [ORelio](https://github.com/ORelio)
|
||||
* [otakuf](https://github.com/otakuf)
|
||||
* [Park0](https://github.com/Park0)
|
||||
* [Paroleen](https://github.com/Paroleen)
|
||||
* [Patricol](https://github.com/Patricol)
|
||||
* [paulchen](https://github.com/paulchen)
|
||||
* [PaulVayssiere](https://github.com/PaulVayssiere)
|
||||
* [pellaeon](https://github.com/pellaeon)
|
||||
* [PeterDaveHello](https://github.com/PeterDaveHello)
|
||||
* [Peterr-K](https://github.com/Peterr-K)
|
||||
* [Piranhaplant](https://github.com/Piranhaplant)
|
||||
* [pirnz](https://github.com/pirnz)
|
||||
* [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
|
||||
* [pitchoule](https://github.com/pitchoule)
|
||||
* [pmaziere](https://github.com/pmaziere)
|
||||
* [Pofilo](https://github.com/Pofilo)
|
||||
* [prysme01](https://github.com/prysme01)
|
||||
* [pubak42](https://github.com/pubak42)
|
||||
* [Qluxzz](https://github.com/Qluxzz)
|
||||
* [quentinus95](https://github.com/quentinus95)
|
||||
* [quickwick](https://github.com/quickwick)
|
||||
* [rakoo](https://github.com/rakoo)
|
||||
* [RawkBob](https://github.com/RawkBob)
|
||||
* [regisenguehard](https://github.com/regisenguehard)
|
||||
* [Riduidel](https://github.com/Riduidel)
|
||||
* [rogerdc](https://github.com/rogerdc)
|
||||
* [Roliga](https://github.com/Roliga)
|
||||
* [ronansalmon](https://github.com/ronansalmon)
|
||||
* [rremizov](https://github.com/rremizov)
|
||||
* [s0lesurviv0r](https://github.com/s0lesurviv0r)
|
||||
* [sal0max](https://github.com/sal0max)
|
||||
* [sebsauvage](https://github.com/sebsauvage)
|
||||
* [shutosg](https://github.com/shutosg)
|
||||
* [simon816](https://github.com/simon816)
|
||||
* [Simounet](https://github.com/Simounet)
|
||||
* [somini](https://github.com/somini)
|
||||
* [SpangleLabs](https://github.com/SpangleLabs)
|
||||
* [SqrtMinusOne](https://github.com/SqrtMinusOne)
|
||||
* [squeek502](https://github.com/squeek502)
|
||||
* [StelFux](https://github.com/StelFux)
|
||||
* [stjohnjohnson](https://github.com/stjohnjohnson)
|
||||
* [Stopka](https://github.com/Stopka)
|
||||
* [Strubbl](https://github.com/Strubbl)
|
||||
* [sublimz](https://github.com/sublimz)
|
||||
* [sunchaserinfo](https://github.com/sunchaserinfo)
|
||||
* [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||
* [sysadminstory](https://github.com/sysadminstory)
|
||||
* [t0stiman](https://github.com/t0stiman)
|
||||
* [tameroski](https://github.com/tameroski)
|
||||
* [teromene](https://github.com/teromene)
|
||||
* [tgkenney](https://github.com/tgkenney)
|
||||
* [thefranke](https://github.com/thefranke)
|
||||
* [TheRadialActive](https://github.com/TheRadialActive)
|
||||
* [theScrabi](https://github.com/theScrabi)
|
||||
* [thezeroalpha](https://github.com/thezeroalpha)
|
||||
* [thibaultcouraud](https://github.com/thibaultcouraud)
|
||||
* [timendum](https://github.com/timendum)
|
||||
* [TitiTestScalingo](https://github.com/TitiTestScalingo)
|
||||
* [tomaszkane](https://github.com/tomaszkane)
|
||||
* [tomershvueli](https://github.com/tomershvueli)
|
||||
* [TotalCaesar659](https://github.com/TotalCaesar659)
|
||||
* [tpikonen](https://github.com/tpikonen)
|
||||
* [TReKiE](https://github.com/TReKiE)
|
||||
* [triatic](https://github.com/triatic)
|
||||
* [User123698745](https://github.com/User123698745)
|
||||
* [VerifiedJoseph](https://github.com/VerifiedJoseph)
|
||||
* [vitkabele](https://github.com/vitkabele)
|
||||
* [WalterBarrett](https://github.com/WalterBarrett)
|
||||
* [wtuuju](https://github.com/wtuuju)
|
||||
* [xurxof](https://github.com/xurxof)
|
||||
* [yamanq](https://github.com/yamanq)
|
||||
* [yardenac](https://github.com/yardenac)
|
||||
* [ymeister](https://github.com/ymeister)
|
||||
* [yue-dongchen](https://github.com/yue-dongchen)
|
||||
* [ZeNairolf](https://github.com/ZeNairolf)
|
73
Dockerfile
73
Dockerfile
@ -1,72 +1,5 @@
|
||||
FROM debian:12-slim AS rssbridge
|
||||
FROM ulsmith/alpine-apache-php7
|
||||
|
||||
LABEL description="RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one."
|
||||
LABEL repository="https://github.com/RSS-Bridge/rss-bridge"
|
||||
LABEL website="https://github.com/RSS-Bridge/rss-bridge"
|
||||
COPY ./ /app/public/
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -xe && \
|
||||
apt-get update && \
|
||||
apt-get install --yes --no-install-recommends \
|
||||
ca-certificates \
|
||||
nginx \
|
||||
nss-plugin-pem \
|
||||
php-curl \
|
||||
php-fpm \
|
||||
php-intl \
|
||||
# php-json is enabled by default with PHP 8.2 in Debian 12
|
||||
php-mbstring \
|
||||
php-memcached \
|
||||
# php-opcache is enabled by default with PHP 8.2 in Debian 12
|
||||
# php-openssl is enabled by default with PHP 8.2 in Debian 12
|
||||
php-sqlite3 \
|
||||
php-xml \
|
||||
php-zip \
|
||||
# php-zlib is enabled by default with PHP 8.2 in Debian 12
|
||||
# for downloading libcurl-impersonate
|
||||
curl \
|
||||
&& \
|
||||
# install curl-impersonate library
|
||||
curlimpersonate_version=0.6.0 && \
|
||||
{ \
|
||||
{ \
|
||||
[ $(arch) = 'aarch64' ] && \
|
||||
archive="libcurl-impersonate-v${curlimpersonate_version}.aarch64-linux-gnu.tar.gz" && \
|
||||
sha512sum="d04b1eabe71f3af06aa1ce99b39a49c5e1d33b636acedcd9fad163bc58156af5c3eb3f75aa706f335515791f7b9c7a6c40ffdfa47430796483ecef929abd905d" \
|
||||
; } \
|
||||
|| { \
|
||||
[ $(arch) = 'armv7l' ] && \
|
||||
archive="libcurl-impersonate-v${curlimpersonate_version}.arm-linux-gnueabihf.tar.gz" && \
|
||||
sha512sum="05906b4efa1a6ed8f3b716fd83d476b6eea6bfc68e3dbc5212d65a2962dcaa7bd1f938c9096a7535252b11d1d08fb93adccc633585ff8cb8cec5e58bfe969bc9" \
|
||||
; } \
|
||||
|| { \
|
||||
[ $(arch) = 'x86_64' ] && \
|
||||
archive="libcurl-impersonate-v${curlimpersonate_version}.x86_64-linux-gnu.tar.gz" && \
|
||||
sha512sum="480bbe9452cd9aff2c0daaaf91f1057b3a96385f79011628a9237223757a9b0d090c59cb5982dc54ea0d07191657299ea91ca170a25ced3d7d410fcdff130ace" \
|
||||
; } \
|
||||
} && \
|
||||
curl -LO "https://github.com/lwthiker/curl-impersonate/releases/download/v${curlimpersonate_version}/${archive}" && \
|
||||
echo "$sha512sum $archive" | sha512sum -c - && \
|
||||
mkdir -p /usr/local/lib/curl-impersonate && \
|
||||
tar xaf "$archive" -C /usr/local/lib/curl-impersonate --wildcards 'libcurl-impersonate-ff.so*' && \
|
||||
rm "$archive" && \
|
||||
apt-get purge --assume-yes curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV LD_PRELOAD /usr/local/lib/curl-impersonate/libcurl-impersonate-ff.so
|
||||
ENV CURL_IMPERSONATE ff91esr
|
||||
|
||||
# logs should go to stdout / stderr
|
||||
RUN ln -sfT /dev/stderr /var/log/nginx/error.log; \
|
||||
ln -sfT /dev/stdout /var/log/nginx/access.log; \
|
||||
chown -R --no-dereference www-data:adm /var/log/nginx/
|
||||
|
||||
COPY ./config/nginx.conf /etc/nginx/sites-available/default
|
||||
COPY ./config/php-fpm.conf /etc/php/8.2/fpm/pool.d/rss-bridge.conf
|
||||
COPY ./config/php.ini /etc/php/8.2/fpm/conf.d/90-rss-bridge.ini
|
||||
|
||||
COPY --chown=www-data:www-data ./ /app/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
RUN chown -R apache:root /app/public
|
674
README.md
674
README.md
@ -1,527 +1,217 @@
|
||||
# RSS-Bridge
|
||||
rss-bridge
|
||||
===
|
||||
[](UNLICENSE) [](https://github.com/rss-bridge/rss-bridge/releases/latest) [](https://tracker.debian.org/pkg/rss-bridge) [](https://www.gnu.org/software/guix/packages/R/) [](https://travis-ci.org/RSS-Bridge/rss-bridge) [](https://hub.docker.com/r/rssbridge/rss-bridge/)
|
||||
|
||||

|
||||
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one. It can be used on webservers or as stand alone application in CLI mode.
|
||||
|
||||
RSS-Bridge is a PHP web application.
|
||||
**Important**: RSS-Bridge is __not__ a feed reader or feed aggregator, but a tool to generate feeds that are consumed by feed readers and feed aggregators. Find a list of feed aggregators on [Wikipedia](https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators).
|
||||
|
||||
It generates web feeds for websites that don't have one.
|
||||
Supported sites/pages (examples)
|
||||
===
|
||||
|
||||
Officially hosted instance: https://rss-bridge.org/bridge01/
|
||||
* `Bandcamp` : Returns last release from [bandcamp](https://bandcamp.com/) for a tag
|
||||
* `Cryptome` : Returns the most recent documents from [Cryptome.org](http://cryptome.org/)
|
||||
* `DansTonChat`: Most recent quotes from [danstonchat.com](http://danstonchat.com/)
|
||||
* `DuckDuckGo`: Most recent results from [DuckDuckGo.com](https://duckduckgo.com/)
|
||||
* `Facebook` : Returns the latest posts on a page or profile on [Facebook](https://facebook.com/)
|
||||
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
|
||||
* `GooglePlus` : Most recent posts of user timeline
|
||||
* `GoogleSearch` : Most recent results from Google Search
|
||||
* `Identi.ca` : Identica user timeline (Should be compatible with other Pump.io instances)
|
||||
* `Instagram`: Most recent photos from an Instagram user
|
||||
* `OpenClassrooms`: Lastest tutorials from [fr.openclassrooms.com](http://fr.openclassrooms.com/)
|
||||
* `Pinterest`: Most recent photos from user or search
|
||||
* `ScmbBridge`: Newest stories from [secouchermoinsbete.fr](http://secouchermoinsbete.fr/)
|
||||
* `ThePirateBay` : Returns the newest indexed torrents from [The Pirate Bay](https://thepiratebay.se/) with keywords
|
||||
* `Twitter` : Return keyword/hashtag search or user timeline
|
||||
* `Wikipedia`: highlighted articles from [Wikipedia](https://wikipedia.org/) in English, German, French or Esperanto
|
||||
* `YouTube` : YouTube user channel, playlist or search
|
||||
|
||||
IRC channel #rssbridge at https://libera.chat/
|
||||
And [many more](bridges/), thanks to the community!
|
||||
|
||||
[Full documentation](https://rss-bridge.github.io/rss-bridge/index.html)
|
||||
Output format
|
||||
===
|
||||
|
||||
Alternatively find another
|
||||
[public instance](https://rss-bridge.github.io/rss-bridge/General/Public_Hosts.html).
|
||||
RSS-Bridge is capable of producing several output formats:
|
||||
|
||||
Requires minimum PHP 7.4.
|
||||
* `Atom` : Atom feed, for use in feed readers
|
||||
* `Html` : Simple HTML page
|
||||
* `Json` : JSON, for consumption by other applications
|
||||
* `Mrss` : MRSS feed, for use in feed readers
|
||||
* `Plaintext` : Raw text, for consumption by other applications
|
||||
|
||||
You can extend RSS-Bridge with your own format, using the [Format API](https://github.com/RSS-Bridge/rss-bridge/wiki/Format-API)!
|
||||
|
||||
[](UNLICENSE)
|
||||
[](https://github.com/rss-bridge/rss-bridge/releases/latest)
|
||||
[](https://web.libera.chat/#rssbridge)
|
||||
[](https://github.com/RSS-Bridge/rss-bridge/actions)
|
||||
Screenshot
|
||||
===
|
||||
|
||||
|||
|
||||
|:-:|:-:|
|
||||
|||
|
||||
|||
|
||||
|||
|
||||
Welcome screen:
|
||||
|
||||
## A subset of bridges (15/447)
|
||||

|
||||
|
||||
* `CssSelectorBridge`: [Scrape out a feed using CSS selectors](https://rss-bridge.org/bridge01/#bridge-CssSelectorBridge)
|
||||
* `FeedMergeBridge`: [Combine multiple feeds into one](https://rss-bridge.org/bridge01/#bridge-FeedMergeBridge)
|
||||
* `FeedReducerBridge`: [Reduce a noisy feed by some percentage](https://rss-bridge.org/bridge01/#bridge-FeedReducerBridge)
|
||||
* `FilterBridge`: [Filter a feed by excluding/including items by keyword](https://rss-bridge.org/bridge01/#bridge-FilterBridge)
|
||||
* `GettrBridge`: [Fetches the latest posts from a GETTR user](https://rss-bridge.org/bridge01/#bridge-GettrBridge)
|
||||
* `MastodonBridge`: [Fetches statuses from a Mastodon (ActivityPub) instance](https://rss-bridge.org/bridge01/#bridge-MastodonBridge)
|
||||
* `RedditBridge`: [Fetches posts from a user/subredit (with filtering options)](https://rss-bridge.org/bridge01/#bridge-RedditBridge)
|
||||
* `RumbleBridge`: [Fetches channel/user videos](https://rss-bridge.org/bridge01/#bridge-RumbleBridge)
|
||||
* `SoundcloudBridge`: [Fetches music by username](https://rss-bridge.org/bridge01/#bridge-SoundcloudBridge)
|
||||
* `TelegramBridge`: [Fetches posts from a public channel](https://rss-bridge.org/bridge01/#bridge-TelegramBridge)
|
||||
* `ThePirateBayBridge:` [Fetches torrents by search/user/category](https://rss-bridge.org/bridge01/#bridge-ThePirateBayBridge)
|
||||
* `TikTokBridge`: [Fetches posts by username](https://rss-bridge.org/bridge01/#bridge-TikTokBridge)
|
||||
* `TwitchBridge`: [Fetches videos from channel](https://rss-bridge.org/bridge01/#bridge-TwitchBridge)
|
||||
* `XPathBridge`: [Scrape out a feed using XPath expressions](https://rss-bridge.org/bridge01/#bridge-XPathBridge)
|
||||
* `YoutubeBridge`: [Fetches videos by username/channel/playlist/search](https://rss-bridge.org/bridge01/#bridge-YoutubeBridge)
|
||||
* `YouTubeCommunityTabBridge`: [Fetches posts from a channel's community tab](https://rss-bridge.org/bridge01/#bridge-YouTubeCommunityTabBridge)
|
||||
***
|
||||
|
||||
## Tutorial
|
||||
RSS-Bridge hashtag (#rss-bridge) search on Twitter, in Atom format (as displayed by Firefox):
|
||||
|
||||
### How to install on traditional shared web hosting
|
||||

|
||||
|
||||
RSS-Bridge can basically be unzipped into a web folder. Should be working instantly.
|
||||
Requirements
|
||||
===
|
||||
|
||||
Latest zip:
|
||||
https://github.com/RSS-Bridge/rss-bridge/archive/refs/heads/master.zip (2MB)
|
||||
RSS-Bridge requires PHP 5.6 or higher with following extensions enabled:
|
||||
|
||||
### How to install on Debian 12 (nginx + php-fpm)
|
||||
- [`openssl`](https://secure.php.net/manual/en/book.openssl.php)
|
||||
- [`libxml`](https://secure.php.net/manual/en/book.libxml.php)
|
||||
- [`mbstring`](https://secure.php.net/manual/en/book.mbstring.php)
|
||||
- [`simplexml`](https://secure.php.net/manual/en/book.simplexml.php)
|
||||
- [`curl`](https://secure.php.net/manual/en/book.curl.php)
|
||||
- [`json`](https://secure.php.net/manual/en/book.json.php)
|
||||
|
||||
These instructions have been tested on a fresh Debian 12 VM from Digital Ocean (1vcpu-512mb-10gb, 5 USD/month).
|
||||
Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
|
||||
|
||||
```shell
|
||||
timedatectl set-timezone Europe/Oslo
|
||||
Enable / Disable bridges
|
||||
===
|
||||
|
||||
apt install git nginx php8.2-fpm php-mbstring php-simplexml php-curl php-intl
|
||||
RSS-Bridge allows you to take full control over which bridges are displayed to the user. That way you can host your own RSS-Bridge service with your favorite collection of bridges!
|
||||
|
||||
# Create a user account
|
||||
useradd --shell /bin/bash --create-home rss-bridge
|
||||
Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
|
||||
|
||||
cd /var/www
|
||||
**Notice**: By default RSS-Bridge will only show a small subset of bridges. Make sure to read up on [whitelisting](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting) to unlock the full potential of RSS-Bridge!
|
||||
|
||||
# Create folder and change its ownership to rss-bridge
|
||||
mkdir rss-bridge && chown rss-bridge:rss-bridge rss-bridge/
|
||||
Deploy
|
||||
===
|
||||
|
||||
# Become rss-bridge
|
||||
su rss-bridge
|
||||
|
||||
# Clone master branch into existing folder
|
||||
git clone https://github.com/RSS-Bridge/rss-bridge.git rss-bridge/
|
||||
cd rss-bridge
|
||||
|
||||
# Copy over the default config (OPTIONAL)
|
||||
cp -v config.default.ini.php config.ini.php
|
||||
|
||||
# Recursively give full permissions to user/owner
|
||||
chmod 700 --recursive ./
|
||||
|
||||
# Give read and execute to others on folder ./static
|
||||
chmod o+rx ./ ./static
|
||||
|
||||
# Recursively give give read to others on folder ./static
|
||||
chmod o+r --recursive ./static
|
||||
```
|
||||
|
||||
Nginx config:
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-enabled/rss-bridge.conf
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
# TODO: change to your own server name
|
||||
server_name example.com;
|
||||
|
||||
access_log /var/log/nginx/rss-bridge.access.log;
|
||||
error_log /var/log/nginx/rss-bridge.error.log;
|
||||
log_not_found off;
|
||||
|
||||
# Intentionally not setting a root folder
|
||||
|
||||
# Static content only served here
|
||||
location /static/ {
|
||||
alias /var/www/rss-bridge/static/;
|
||||
}
|
||||
|
||||
# Pass off to php-fpm only when location is EXACTLY == /
|
||||
location = / {
|
||||
root /var/www/rss-bridge/;
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_read_timeout 45s;
|
||||
fastcgi_pass unix:/run/php/rss-bridge.sock;
|
||||
}
|
||||
|
||||
# Reduce log noise
|
||||
location = /favicon.ico {
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Reduce log noise
|
||||
location = /robots.txt {
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
PHP FPM pool config:
|
||||
```ini
|
||||
; /etc/php/8.2/fpm/pool.d/rss-bridge.conf
|
||||
|
||||
[rss-bridge]
|
||||
|
||||
user = rss-bridge
|
||||
group = rss-bridge
|
||||
|
||||
listen = /run/php/rss-bridge.sock
|
||||
|
||||
listen.owner = www-data
|
||||
listen.group = www-data
|
||||
|
||||
; Create 10 workers standing by to serve requests
|
||||
pm = static
|
||||
pm.max_children = 10
|
||||
|
||||
; Respawn worker after 500 requests (workaround for memory leaks etc.)
|
||||
pm.max_requests = 500
|
||||
```
|
||||
|
||||
PHP ini config:
|
||||
```ini
|
||||
; /etc/php/8.2/fpm/conf.d/30-rss-bridge.ini
|
||||
|
||||
max_execution_time = 15
|
||||
memory_limit = 64M
|
||||
```
|
||||
|
||||
Restart fpm and nginx:
|
||||
|
||||
```shell
|
||||
# Lint and restart php-fpm
|
||||
php-fpm8.2 -t && systemctl restart php8.2-fpm
|
||||
|
||||
# Lint and restart nginx
|
||||
nginx -t && systemctl restart nginx
|
||||
```
|
||||
|
||||
### How to install from Composer
|
||||
|
||||
Install the latest release.
|
||||
|
||||
```shell
|
||||
cd /var/www
|
||||
composer create-project -v --no-dev --no-scripts rss-bridge/rss-bridge
|
||||
```
|
||||
|
||||
### How to install with Caddy
|
||||
|
||||
TODO. See https://github.com/RSS-Bridge/rss-bridge/issues/3785
|
||||
|
||||
### Install from Docker Hub:
|
||||
|
||||
Install by downloading the docker image from Docker Hub:
|
||||
|
||||
```bash
|
||||
# Create container
|
||||
docker create --name=rss-bridge --publish 3000:80 --volume $(pwd)/config:/config rssbridge/rss-bridge
|
||||
```
|
||||
|
||||
You can put custom `config.ini.php` and bridges into `./config`.
|
||||
|
||||
**You must restart container for custom changes to take effect.**
|
||||
|
||||
See `docker-entrypoint.sh` for details.
|
||||
|
||||
```bash
|
||||
# Start container
|
||||
docker start rss-bridge
|
||||
```
|
||||
|
||||
Browse http://localhost:3000/
|
||||
|
||||
### Install by locally building from Dockerfile
|
||||
|
||||
```bash
|
||||
# Build image from Dockerfile
|
||||
docker build -t rss-bridge .
|
||||
|
||||
# Create container
|
||||
docker create --name rss-bridge --publish 3000:80 --volume $(pwd)/config:/config rss-bridge
|
||||
```
|
||||
|
||||
You can put custom `config.ini.php` and bridges into `./config`.
|
||||
|
||||
**You must restart container for custom changes to take effect.**
|
||||
|
||||
See `docker-entrypoint.sh` for details.
|
||||
|
||||
```bash
|
||||
# Start container
|
||||
docker start rss-bridge
|
||||
```
|
||||
|
||||
Browse http://localhost:3000/
|
||||
|
||||
### Install with docker-compose (using Docker Hub)
|
||||
|
||||
You can put custom `config.ini.php` and bridges into `./config`.
|
||||
|
||||
**You must restart container for custom changes to take effect.**
|
||||
|
||||
See `docker-entrypoint.sh` for details.
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Browse http://localhost:3000/
|
||||
|
||||
### Other installation methods
|
||||
Thanks to the community, hosting your own instance of RSS-Bridge is as easy as clicking a button!
|
||||
|
||||
[](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
|
||||
[](https://heroku.com/deploy)
|
||||
[](https://www.cloudron.io/store/com.rssbridgeapp.cloudronapp.html)
|
||||
[](https://www.pikapods.com/pods?run=rssbridge)
|
||||
|
||||
The Heroku quick deploy currently does not work. It might work if you fork this repo and
|
||||
modify the `repository` in `scalingo.json`. See https://github.com/RSS-Bridge/rss-bridge/issues/2688
|
||||
|
||||
Learn more in
|
||||
[Installation](https://rss-bridge.github.io/rss-bridge/For_Hosts/Installation.html).
|
||||
|
||||
## How-to
|
||||
|
||||
### How to fix "Access denied."
|
||||
|
||||
Output is from php-fpm. It is unable to read index.php.
|
||||
|
||||
chown rss-bridge:rss-bridge /var/www/rss-bridge/index.php
|
||||
|
||||
### How to password-protect the instance (token)
|
||||
|
||||
Modify `config.ini.php`:
|
||||
|
||||
[authentication]
|
||||
|
||||
token = "hunter2"
|
||||
|
||||
### How to remove all cache items
|
||||
|
||||
As current user:
|
||||
|
||||
bin/cache-clear
|
||||
|
||||
As user rss-bridge:
|
||||
|
||||
sudo -u rss-bridge bin/cache-clear
|
||||
|
||||
As root:
|
||||
|
||||
sudo bin/cache-clear
|
||||
|
||||
### How to remove all expired cache items
|
||||
|
||||
bin/cache-prune
|
||||
|
||||
### How to fix "PHP Fatal error: Uncaught Exception: The FileCache path is not writable"
|
||||
|
||||
```shell
|
||||
# Give rss-bridge ownership
|
||||
chown rss-bridge:rss-bridge -R /var/www/rss-bridge/cache
|
||||
|
||||
# Or, give www-data ownership
|
||||
chown www-data:www-data -R /var/www/rss-bridge/cache
|
||||
|
||||
# Or, give everyone write permission
|
||||
chmod 777 -R /var/www/rss-bridge/cache
|
||||
|
||||
# Or last ditch effort (CAREFUL)
|
||||
rm -rf /var/www/rss-bridge/cache/ && mkdir /var/www/rss-bridge/cache/
|
||||
```
|
||||
|
||||
### How to fix "attempt to write a readonly database"
|
||||
|
||||
The sqlite files (db, wal and shm) are not writeable.
|
||||
|
||||
chown -v rss-bridge:rss-bridge cache/*
|
||||
|
||||
### How to fix "Unable to prepare statement: 1, no such table: storage"
|
||||
|
||||
rm cache/*
|
||||
|
||||
### How to create a new bridge from scratch
|
||||
|
||||
Create the new bridge in e.g. `bridges/BearBlogBridge.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
class BearBlogBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'BearBlog (bearblog.dev)';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$dom = getSimpleHTMLDOM('https://herman.bearblog.dev/blog/');
|
||||
foreach ($dom->find('.blog-posts li') as $li) {
|
||||
$a = $li->find('a', 0);
|
||||
$this->items[] = [
|
||||
'title' => $a->plaintext,
|
||||
'uri' => 'https://herman.bearblog.dev' . $a->href,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Learn more in [bridge api](https://rss-bridge.github.io/rss-bridge/Bridge_API/index.html).
|
||||
|
||||
### How to enable all bridges
|
||||
|
||||
enabled_bridges[] = *
|
||||
|
||||
### How to enable some bridges
|
||||
|
||||
```
|
||||
enabled_bridges[] = TwitchBridge
|
||||
enabled_bridges[] = GettrBridge
|
||||
```
|
||||
|
||||
### How to enable debug mode
|
||||
|
||||
The
|
||||
[debug mode](https://rss-bridge.github.io/rss-bridge/For_Developers/Debug_mode.html)
|
||||
disables the majority of caching operations.
|
||||
|
||||
enable_debug_mode = true
|
||||
|
||||
### How to switch to memcached as cache backend
|
||||
|
||||
```
|
||||
[cache]
|
||||
|
||||
; Cache backend: file (default), sqlite, memcached, null
|
||||
type = "memcached"
|
||||
```
|
||||
|
||||
### How to switch to sqlite3 as cache backend
|
||||
|
||||
type = "sqlite"
|
||||
|
||||
### How to disable bridge errors (as feed items)
|
||||
|
||||
When a bridge fails, RSS-Bridge will produce a feed with a single item describing the error.
|
||||
|
||||
This way, feed readers pick it up and you are notified.
|
||||
|
||||
If you don't want this behaviour, switch the error output to `http`:
|
||||
|
||||
[error]
|
||||
|
||||
; Defines how error messages are returned by RSS-Bridge
|
||||
;
|
||||
; "feed" = As part of the feed (default)
|
||||
; "http" = As HTTP error message
|
||||
; "none" = No errors are reported
|
||||
output = "http"
|
||||
|
||||
### How to accumulate errors before finally reporting it
|
||||
|
||||
Modify `report_limit` so that an error must occur 3 times before it is reported.
|
||||
|
||||
; Defines how often an error must occur before it is reported to the user
|
||||
report_limit = 3
|
||||
|
||||
The report count is reset to 0 each day.
|
||||
|
||||
### How to password-protect the instance (HTTP Basic Auth)
|
||||
|
||||
[authentication]
|
||||
|
||||
enable = true
|
||||
username = "alice"
|
||||
password = "cat"
|
||||
|
||||
Will typically require feed readers to be configured with the credentials.
|
||||
|
||||
It may also be possible to manually include the credentials in the URL:
|
||||
|
||||
https://alice:cat@rss-bridge.org/bridge01/?action=display&bridge=FabriceBellardBridge&format=Html
|
||||
|
||||
### How to create a new output format
|
||||
|
||||
See `formats/PlaintextFormat.php` for an example.
|
||||
|
||||
### How to run unit tests and linter
|
||||
|
||||
These commands require that you have installed the dev dependencies in `composer.json`.
|
||||
|
||||
Run all tests:
|
||||
|
||||
./vendor/bin/phpunit
|
||||
|
||||
Run a single test class:
|
||||
|
||||
./vendor/bin/phpunit --filter UrlTest
|
||||
|
||||
Run linter:
|
||||
|
||||
./vendor/bin/phpcs --standard=phpcs.xml --warning-severity=0 --extensions=php -p ./
|
||||
|
||||
https://github.com/squizlabs/PHP_CodeSniffer/wiki
|
||||
|
||||
### How to spawn a minimal development environment
|
||||
|
||||
php -S 127.0.0.1:9001
|
||||
|
||||
http://127.0.0.1:9001/
|
||||
|
||||
## Explanation
|
||||
|
||||
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage,
|
||||
webmaster of
|
||||
[sebsauvage.net](https://sebsauvage.net), author of
|
||||
[Shaarli](https://sebsauvage.net/wiki/doku.php?id=php:shaarli) and
|
||||
[ZeroBin](https://sebsauvage.net/wiki/doku.php?id=php:zerobin).
|
||||
|
||||
See [CONTRIBUTORS.md](CONTRIBUTORS.md)
|
||||
|
||||
RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds.
|
||||
The specific cache duration can be different between bridges.
|
||||
|
||||
RSS-Bridge allows you to take full control over which bridges are displayed to the user.
|
||||
That way you can host your own RSS-Bridge service with your favorite collection of bridges!
|
||||
|
||||
Current maintainers (as of 2024): @dvikan and @Mynacol #2519
|
||||
|
||||
## Reference
|
||||
|
||||
### Feed item structure
|
||||
|
||||
This is the feed item structure that bridges are expected to produce.
|
||||
|
||||
```php
|
||||
$item = [
|
||||
'uri' => 'https://example.com/blog/hello',
|
||||
'title' => 'Hello world',
|
||||
// Publication date in unix timestamp
|
||||
'timestamp' => 1668706254,
|
||||
'author' => 'Alice',
|
||||
'content' => 'Here be item content',
|
||||
'enclosures' => [
|
||||
'https://example.com/foo.png',
|
||||
'https://example.com/bar.png'
|
||||
],
|
||||
'categories' => [
|
||||
'news',
|
||||
'tech',
|
||||
],
|
||||
// Globally unique id
|
||||
'uid' => 'e7147580c8747aad',
|
||||
]
|
||||
```
|
||||
|
||||
### Output formats
|
||||
|
||||
* `Atom`: Atom feed, for use in feed readers
|
||||
* `Html`: Simple HTML page
|
||||
* `Json`: JSON, for consumption by other applications
|
||||
* `Mrss`: MRSS feed, for use in feed readers
|
||||
* `Plaintext`: Raw text, for consumption by other applications
|
||||
* `Sfeed`: Text, TAB separated
|
||||
|
||||
### Cache backends
|
||||
|
||||
* `File`
|
||||
* `SQLite`
|
||||
* `Memcached`
|
||||
* `Array`
|
||||
* `Null`
|
||||
|
||||
### Licenses
|
||||
[](https://cloud.docker.com/stack/deploy/?repo=https://github.com/rss-bridge/rss-bridge)
|
||||
|
||||
Getting involved
|
||||
===
|
||||
|
||||
There are many ways for you to getting involved with RSS-Bridge. Here are a few things:
|
||||
|
||||
- Share RSS-Bridge with your friends (Twitter, Facebook, ..._you name it_...)
|
||||
- Report broken bridges or bugs by opening [Issues](https://github.com/RSS-Bridge/rss-bridge/issues) on GitHub
|
||||
- Request new features or suggest ideas (via [Issues](https://github.com/RSS-Bridge/rss-bridge/issues))
|
||||
- Discuss bugs, features, ideas or [issues](https://github.com/RSS-Bridge/rss-bridge/issues)
|
||||
- Add new bridges or improve the API
|
||||
- Improve the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki)
|
||||
- Host an instance of RSS-Bridge for your personal use or make it available to the community :sparkling_heart:
|
||||
|
||||
Authors
|
||||
===
|
||||
|
||||
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage, webmaster of [sebsauvage.net](http://sebsauvage.net), author of [Shaarli](http://sebsauvage.net/wiki/doku.php?id=php:shaarli) and [ZeroBin](http://sebsauvage.net/wiki/doku.php?id=php:zerobin).
|
||||
|
||||
**Contributors** (sorted alphabetically):
|
||||
<!--
|
||||
Use this script to generate the list automatically (using the GitHub API):
|
||||
https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
|
||||
-->
|
||||
|
||||
* [16mhz](https://api.github.com/users/16mhz)
|
||||
* [Ahiles3005](https://api.github.com/users/Ahiles3005)
|
||||
* [Albirew](https://api.github.com/users/Albirew)
|
||||
* [AmauryCarrade](https://api.github.com/users/AmauryCarrade)
|
||||
* [ArthurHoaro](https://api.github.com/users/ArthurHoaro)
|
||||
* [Astalaseven](https://api.github.com/users/Astalaseven)
|
||||
* [Astyan-42](https://api.github.com/users/Astyan-42)
|
||||
* [Daiyousei](https://api.github.com/users/Daiyousei)
|
||||
* [Djuuu](https://api.github.com/users/Djuuu)
|
||||
* [Draeli](https://api.github.com/users/Draeli)
|
||||
* [EtienneM](https://api.github.com/users/EtienneM)
|
||||
* [Frenzie](https://api.github.com/users/Frenzie)
|
||||
* [Ginko-Aloe](https://api.github.com/users/Ginko-Aloe)
|
||||
* [Glandos](https://api.github.com/users/Glandos)
|
||||
* [GregThib](https://api.github.com/users/GregThib)
|
||||
* [Grummfy](https://api.github.com/users/Grummfy)
|
||||
* [JackNUMBER](https://api.github.com/users/JackNUMBER)
|
||||
* [JeremyRand](https://api.github.com/users/JeremyRand)
|
||||
* [Jocker666z](https://api.github.com/users/Jocker666z)
|
||||
* [LogMANOriginal](https://api.github.com/users/LogMANOriginal)
|
||||
* [MonsieurPoutounours](https://api.github.com/users/MonsieurPoutounours)
|
||||
* [ORelio](https://api.github.com/users/ORelio)
|
||||
* [PaulVayssiere](https://api.github.com/users/PaulVayssiere)
|
||||
* [Piranhaplant](https://api.github.com/users/Piranhaplant)
|
||||
* [Riduidel](https://api.github.com/users/Riduidel)
|
||||
* [Strubbl](https://api.github.com/users/Strubbl)
|
||||
* [TheRadialActive](https://api.github.com/users/TheRadialActive)
|
||||
* [TwizzyDizzy](https://api.github.com/users/TwizzyDizzy)
|
||||
* [WalterBarrett](https://api.github.com/users/WalterBarrett)
|
||||
* [ZeNairolf](https://api.github.com/users/ZeNairolf)
|
||||
* [adamchainz](https://api.github.com/users/adamchainz)
|
||||
* [aledeg](https://api.github.com/users/aledeg)
|
||||
* [alexAubin](https://api.github.com/users/alexAubin)
|
||||
* [az5he6ch](https://api.github.com/users/az5he6ch)
|
||||
* [b1nj](https://api.github.com/users/b1nj)
|
||||
* [benasse](https://api.github.com/users/benasse)
|
||||
* [captn3m0](https://api.github.com/users/captn3m0)
|
||||
* [chemel](https://api.github.com/users/chemel)
|
||||
* [ckiw](https://api.github.com/users/ckiw)
|
||||
* [cnlpete](https://api.github.com/users/cnlpete)
|
||||
* [corenting](https://api.github.com/users/corenting)
|
||||
* [da2x](https://api.github.com/users/da2x)
|
||||
* [eMerzh](https://api.github.com/users/eMerzh)
|
||||
* [em92](https://api.github.com/users/em92)
|
||||
* [griffaurel](https://api.github.com/users/griffaurel)
|
||||
* [hunhejj](https://api.github.com/users/hunhejj)
|
||||
* [j0k3r](https://api.github.com/users/j0k3r)
|
||||
* [jdigilio](https://api.github.com/users/jdigilio)
|
||||
* [kranack](https://api.github.com/users/kranack)
|
||||
* [kraoc](https://api.github.com/users/kraoc)
|
||||
* [laBecasse](https://api.github.com/users/laBecasse)
|
||||
* [lagaisse](https://api.github.com/users/lagaisse)
|
||||
* [lalannev](https://api.github.com/users/lalannev)
|
||||
* [ldidry](https://api.github.com/users/ldidry)
|
||||
* [m0zes](https://api.github.com/users/m0zes)
|
||||
* [matthewseal](https://api.github.com/users/matthewseal)
|
||||
* [mcbyte-it](https://api.github.com/users/mcbyte-it)
|
||||
* [mdemoss](https://api.github.com/users/mdemoss)
|
||||
* [melangue](https://api.github.com/users/melangue)
|
||||
* [metaMMA](https://api.github.com/users/metaMMA)
|
||||
* [mickael-bertrand](https://api.github.com/users/mickael-bertrand)
|
||||
* [mitsukarenai](https://api.github.com/users/mitsukarenai)
|
||||
* [mro](https://api.github.com/users/mro)
|
||||
* [mxmehl](https://api.github.com/users/mxmehl)
|
||||
* [nel50n](https://api.github.com/users/nel50n)
|
||||
* [niawag](https://api.github.com/users/niawag)
|
||||
* [pellaeon](https://api.github.com/users/pellaeon)
|
||||
* [pit-fgfjiudghdf](https://api.github.com/users/pit-fgfjiudghdf)
|
||||
* [pitchoule](https://api.github.com/users/pitchoule)
|
||||
* [pmaziere](https://api.github.com/users/pmaziere)
|
||||
* [prysme01](https://api.github.com/users/prysme01)
|
||||
* [quentinus95](https://api.github.com/users/quentinus95)
|
||||
* [qwertygc](https://api.github.com/users/qwertygc)
|
||||
* [regisenguehard](https://api.github.com/users/regisenguehard)
|
||||
* [rogerdc](https://api.github.com/users/rogerdc)
|
||||
* [sebsauvage](https://api.github.com/users/sebsauvage)
|
||||
* [sublimz](https://api.github.com/users/sublimz)
|
||||
* [sysadminstory](https://api.github.com/users/sysadminstory)
|
||||
* [tameroski](https://api.github.com/users/tameroski)
|
||||
* [teromene](https://api.github.com/users/teromene)
|
||||
* [triatic](https://api.github.com/users/triatic)
|
||||
* [wtuuju](https://api.github.com/users/wtuuju)
|
||||
|
||||
Licenses
|
||||
===
|
||||
|
||||
The source code for RSS-Bridge is [Public Domain](UNLICENSE).
|
||||
|
||||
RSS-Bridge uses third party libraries with their own license:
|
||||
|
||||
* [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`PHP Simple HTML DOM Parser`](https://simplehtmldom.sourceforge.io/docs/1.9/index.html) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`Laravel framework`](https://github.com/laravel/framework/) licensed under the [MIT License](https://opensource.org/licenses/MIT)
|
||||
* [`PHP Simple HTML DOM Parser`](http://simplehtmldom.sourceforge.net/) licensed under the [MIT License](http://opensource.org/licenses/MIT)
|
||||
* [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)
|
||||
|
||||
## Rant
|
||||
Technical notes
|
||||
===
|
||||
|
||||
* RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds. The specific cache duration can be different between bridges. Cached files are deleted automatically after 24 hours.
|
||||
* You can implement your own bridge, [following these instructions](https://github.com/RSS-Bridge/rss-bridge/wiki/Bridge-API).
|
||||
* You can enable debug mode to disable caching. Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Debug-mode)
|
||||
|
||||
Rant
|
||||
===
|
||||
|
||||
*Dear so-called "social" websites.*
|
||||
|
||||
@ -531,6 +221,6 @@ You're not social when you hamper sharing by removing feeds. You're happy to hav
|
||||
|
||||
We want to share with friends, using open protocols: RSS, Atom, XMPP, whatever. Because no one wants to have *your* service with *your* applications using *your* API force-feeding them. Friends must be free to choose whatever software and service they want.
|
||||
|
||||
We are rebuilding bridges you have willfully destroyed.
|
||||
We are rebuilding bridges you have wilfully destroyed.
|
||||
|
||||
Get your shit together: Put RSS/Atom back in.
|
||||
|
@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Checks if the website for a given bridge is reachable.
|
||||
*
|
||||
* **Remarks**
|
||||
* - This action is only available in debug mode.
|
||||
* - Returns the bridge status as Json-formatted string.
|
||||
* - Returns an error if the bridge is not whitelisted.
|
||||
* - Returns a responsive web page that automatically checks all whitelisted
|
||||
* bridges (using JavaScript) if no bridge is specified.
|
||||
*/
|
||||
class ConnectivityAction implements ActionInterface
|
||||
{
|
||||
private BridgeFactory $bridgeFactory;
|
||||
|
||||
public function __construct(
|
||||
BridgeFactory $bridgeFactory
|
||||
) {
|
||||
$this->bridgeFactory = $bridgeFactory;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
if (!Debug::isEnabled()) {
|
||||
return new Response('This action is only available in debug mode!', 403);
|
||||
}
|
||||
|
||||
$bridgeName = $request->get('bridge');
|
||||
if (!$bridgeName) {
|
||||
return new Response(render_template('connectivity.html.php'));
|
||||
}
|
||||
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
|
||||
if (!$bridgeClassName) {
|
||||
return new Response('Bridge not found', 404);
|
||||
}
|
||||
return $this->reportBridgeConnectivity($bridgeClassName);
|
||||
}
|
||||
|
||||
private function reportBridgeConnectivity($bridgeClassName)
|
||||
{
|
||||
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
|
||||
throw new \Exception('Bridge is not whitelisted!');
|
||||
}
|
||||
|
||||
$bridge = $this->bridgeFactory->create($bridgeClassName);
|
||||
$curl_opts = [
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
];
|
||||
$result = [
|
||||
'bridge' => $bridgeClassName,
|
||||
'successful' => false,
|
||||
'http_code' => null,
|
||||
];
|
||||
try {
|
||||
$response = getContents($bridge::URI, [], $curl_opts, true);
|
||||
$result['http_code'] = $response->getCode();
|
||||
if (in_array($result['http_code'], [200])) {
|
||||
$result['successful'] = true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
return new Response(Json::encode($result), 200, ['content-type' => 'text/json']);
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
class DetectAction implements ActionInterface
|
||||
{
|
||||
private BridgeFactory $bridgeFactory;
|
||||
|
||||
public function __construct(
|
||||
BridgeFactory $bridgeFactory
|
||||
) {
|
||||
$this->bridgeFactory = $bridgeFactory;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$url = $request->get('url');
|
||||
$format = $request->get('format');
|
||||
|
||||
if (!$url) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a url']));
|
||||
}
|
||||
if (!$format) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']));
|
||||
}
|
||||
|
||||
foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
|
||||
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bridge = $this->bridgeFactory->create($bridgeClassName);
|
||||
|
||||
$bridgeParams = $bridge->detectParameters($url);
|
||||
|
||||
if (!$bridgeParams) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$query = [
|
||||
'action' => 'display',
|
||||
'bridge' => $bridgeClassName,
|
||||
'format' => $format,
|
||||
];
|
||||
$query = array_merge($query, $bridgeParams);
|
||||
return new Response('', 301, ['location' => '?' . http_build_query($query)]);
|
||||
}
|
||||
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', [
|
||||
'message' => 'No bridge found for given URL: ' . $url,
|
||||
]));
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
<?php
|
||||
|
||||
class DisplayAction implements ActionInterface
|
||||
{
|
||||
private CacheInterface $cache;
|
||||
private Logger $logger;
|
||||
private BridgeFactory $bridgeFactory;
|
||||
|
||||
public function __construct(
|
||||
CacheInterface $cache,
|
||||
Logger $logger,
|
||||
BridgeFactory $bridgeFactory
|
||||
) {
|
||||
$this->cache = $cache;
|
||||
$this->logger = $logger;
|
||||
$this->bridgeFactory = $bridgeFactory;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$bridgeName = $request->get('bridge');
|
||||
$format = $request->get('format');
|
||||
$noproxy = $request->get('_noproxy');
|
||||
|
||||
if (!$bridgeName) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge name parameter']), 400);
|
||||
}
|
||||
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
|
||||
if (!$bridgeClassName) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Bridge not found']), 404);
|
||||
}
|
||||
|
||||
if (!$format) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']), 400);
|
||||
}
|
||||
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'This bridge is not whitelisted']), 400);
|
||||
}
|
||||
|
||||
// Disable proxy (if enabled and per user's request)
|
||||
if (
|
||||
Configuration::getConfig('proxy', 'url')
|
||||
&& Configuration::getConfig('proxy', 'by_bridge')
|
||||
&& $noproxy
|
||||
) {
|
||||
// This const is only used once in getContents()
|
||||
define('NOPROXY', true);
|
||||
}
|
||||
|
||||
$cacheKey = 'http_' . json_encode($request->toArray());
|
||||
|
||||
$bridge = $this->bridgeFactory->create($bridgeClassName);
|
||||
|
||||
$response = $this->createResponse($request, $bridge, $format);
|
||||
|
||||
if ($response->getCode() === 200) {
|
||||
$ttl = $request->get('_cache_timeout');
|
||||
if (Configuration::getConfig('cache', 'custom_timeout') && $ttl) {
|
||||
$ttl = (int) $ttl;
|
||||
} else {
|
||||
$ttl = $bridge->getCacheTimeout();
|
||||
}
|
||||
$this->cache->set($cacheKey, $response, $ttl);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function createResponse(Request $request, BridgeAbstract $bridge, string $format)
|
||||
{
|
||||
$items = [];
|
||||
|
||||
try {
|
||||
$bridge->loadConfiguration();
|
||||
// Remove parameters that don't concern bridges
|
||||
$remove = [
|
||||
'token',
|
||||
'action',
|
||||
'bridge',
|
||||
'format',
|
||||
'_noproxy',
|
||||
'_cache_timeout',
|
||||
'_error_time',
|
||||
'_', // Some RSS readers add a cache-busting parameter (_=<timestamp>) to feed URLs, detect and ignore them.
|
||||
];
|
||||
$requestArray = $request->toArray();
|
||||
$input = array_diff_key($requestArray, array_fill_keys($remove, ''));
|
||||
$bridge->setInput($input);
|
||||
$bridge->collectData();
|
||||
$items = $bridge->getItems();
|
||||
} catch (\Throwable $e) {
|
||||
if ($e instanceof RateLimitException) {
|
||||
// These are internally generated by bridges
|
||||
$this->logger->info(sprintf('RateLimitException in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
||||
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 429);
|
||||
}
|
||||
if ($e instanceof HttpException) {
|
||||
if (in_array($e->getCode(), [429, 503])) {
|
||||
// Log with debug, immediately reproduce and return
|
||||
$this->logger->debug(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
|
||||
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), $e->getCode());
|
||||
}
|
||||
// Some other status code which we let fail normally (but don't log it)
|
||||
} else {
|
||||
// Log error if it's not an HttpException
|
||||
$this->logger->error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]);
|
||||
}
|
||||
$errorOutput = Configuration::getConfig('error', 'output');
|
||||
$reportLimit = Configuration::getConfig('error', 'report_limit');
|
||||
$errorCount = 1;
|
||||
if ($reportLimit > 1) {
|
||||
$errorCount = $this->logBridgeError($bridge->getName(), $e->getCode());
|
||||
}
|
||||
// Let clients know about the error if we are passed the report limit
|
||||
if ($errorCount >= $reportLimit) {
|
||||
if ($errorOutput === 'feed') {
|
||||
// Render the exception as a feed item
|
||||
$items = [$this->createFeedItemFromException($e, $bridge)];
|
||||
} elseif ($errorOutput === 'http') {
|
||||
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 500);
|
||||
} elseif ($errorOutput === 'none') {
|
||||
// Do nothing (produces an empty feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$formatFactory = new FormatFactory();
|
||||
$format = $formatFactory->create($format);
|
||||
|
||||
$format->setItems($items);
|
||||
$format->setFeed($bridge->getFeed());
|
||||
$now = time();
|
||||
$format->setLastModified($now);
|
||||
$headers = [
|
||||
'last-modified' => gmdate('D, d M Y H:i:s ', $now) . 'GMT',
|
||||
'content-type' => $format->getMimeType() . '; charset=UTF-8',
|
||||
];
|
||||
$body = $format->render();
|
||||
|
||||
// This is supposed to remove non-utf8 byte sequences, but I'm unsure if it works
|
||||
ini_set('mbstring.substitute_character', 'none');
|
||||
$body = mb_convert_encoding($body, 'UTF-8', 'UTF-8');
|
||||
|
||||
return new Response($body, 200, $headers);
|
||||
}
|
||||
|
||||
private function createFeedItemFromException($e, BridgeAbstract $bridge): array
|
||||
{
|
||||
$item = [];
|
||||
|
||||
// Create a unique identifier every 24 hours
|
||||
$uniqueIdentifier = urlencode((int)(time() / 86400));
|
||||
$title = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $uniqueIdentifier);
|
||||
|
||||
$item['title'] = $title;
|
||||
$item['uri'] = get_current_url();
|
||||
$item['timestamp'] = time();
|
||||
|
||||
// Create an item identifier for feed readers e.g. "staysafetv twitch videos_19389"
|
||||
$item['uid'] = $bridge->getName() . '_' . $uniqueIdentifier;
|
||||
|
||||
$content = render_template(__DIR__ . '/../templates/bridge-error.html.php', [
|
||||
'error' => render_template(__DIR__ . '/../templates/exception.html.php', ['e' => $e]),
|
||||
'searchUrl' => self::createGithubSearchUrl($bridge),
|
||||
'issueUrl' => self::createGithubIssueUrl($bridge, $e),
|
||||
'maintainer' => $bridge->getMaintainer(),
|
||||
]);
|
||||
$item['content'] = $content;
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function logBridgeError($bridgeName, $code)
|
||||
{
|
||||
// todo: it's not really necessary to json encode $report
|
||||
$cacheKey = 'error_reporting_' . $bridgeName . '_' . $code;
|
||||
$report = $this->cache->get($cacheKey);
|
||||
if ($report) {
|
||||
$report = Json::decode($report);
|
||||
$report['time'] = time();
|
||||
$report['count']++;
|
||||
} else {
|
||||
$report = [
|
||||
'error' => $code,
|
||||
'time' => time(),
|
||||
'count' => 1,
|
||||
];
|
||||
}
|
||||
$ttl = 86400 * 5;
|
||||
$this->cache->set($cacheKey, Json::encode($report), $ttl);
|
||||
return $report['count'];
|
||||
}
|
||||
|
||||
private static function createGithubIssueUrl(BridgeAbstract $bridge, \Throwable $e): string
|
||||
{
|
||||
$maintainer = $bridge->getMaintainer();
|
||||
if (str_contains($maintainer, ',')) {
|
||||
$maintainers = explode(',', $maintainer);
|
||||
} else {
|
||||
$maintainers = [$maintainer];
|
||||
}
|
||||
$maintainers = array_map('trim', $maintainers);
|
||||
|
||||
$queryString = $_SERVER['QUERY_STRING'] ?? '';
|
||||
$query = [
|
||||
'title' => $bridge->getName() . ' failed with: ' . $e->getMessage(),
|
||||
'body' => sprintf(
|
||||
"```\n%s\n\n%s\n\nQuery string: %s\nVersion: %s\nOs: %s\nPHP version: %s\n```\nMaintainer: @%s",
|
||||
create_sane_exception_message($e),
|
||||
implode("\n", trace_to_call_points(trace_from_exception($e))),
|
||||
$queryString,
|
||||
Configuration::getVersion(),
|
||||
PHP_OS_FAMILY,
|
||||
phpversion() ?: 'Unknown',
|
||||
implode(', @', $maintainers),
|
||||
),
|
||||
'labels' => 'Bridge-Broken',
|
||||
'assignee' => $maintainer[0],
|
||||
];
|
||||
|
||||
return 'https://github.com/RSS-Bridge/rss-bridge/issues/new?' . http_build_query($query);
|
||||
}
|
||||
|
||||
private static function createGithubSearchUrl($bridge): string
|
||||
{
|
||||
return sprintf(
|
||||
'https://github.com/RSS-Bridge/rss-bridge/issues?q=%s',
|
||||
urlencode('is:issue is:open ' . $bridge->getName())
|
||||
);
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This action is used by the frontpage form search.
|
||||
* It finds a bridge based off of a user input url.
|
||||
* It uses bridges' detectParameters implementation.
|
||||
*/
|
||||
class FindfeedAction implements ActionInterface
|
||||
{
|
||||
private BridgeFactory $bridgeFactory;
|
||||
|
||||
public function __construct(
|
||||
BridgeFactory $bridgeFactory
|
||||
) {
|
||||
$this->bridgeFactory = $bridgeFactory;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$url = $request->get('url');
|
||||
$format = $request->get('format');
|
||||
|
||||
if (!$url) {
|
||||
return new Response('You must specify a url', 400);
|
||||
}
|
||||
if (!$format) {
|
||||
return new Response('You must specify a format', 400);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
|
||||
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bridge = $this->bridgeFactory->create($bridgeClassName);
|
||||
|
||||
$bridgeParams = $bridge->detectParameters($url);
|
||||
|
||||
if ($bridgeParams === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// It's allowed to have no 'context' in a bridge (only a default context without any name)
|
||||
// In this case, the reference to the parameters are found in the first element of the PARAMETERS array
|
||||
|
||||
$context = $bridgeParams['context'] ?? 0;
|
||||
|
||||
$bridgeData = [];
|
||||
// Construct the array of parameters
|
||||
foreach ($bridgeParams as $key => $value) {
|
||||
// 'context' is a special case : it's a bridge parameters, there is no "name" for this parameter
|
||||
if ($key == 'context') {
|
||||
$bridgeData[$key]['name'] = 'Context';
|
||||
$bridgeData[$key]['value'] = $value;
|
||||
} else {
|
||||
$bridgeData[$key]['name'] = $this->getParameterName($bridge, $context, $key);
|
||||
$bridgeData[$key]['value'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$bridgeParams['bridge'] = $bridgeClassName;
|
||||
$bridgeParams['format'] = $format;
|
||||
$content = [
|
||||
'url' => './?action=display&' . http_build_query($bridgeParams),
|
||||
'bridgeParams' => $bridgeParams,
|
||||
'bridgeData' => $bridgeData,
|
||||
'bridgeMeta' => [
|
||||
'name' => $bridge::NAME,
|
||||
'description' => $bridge::DESCRIPTION,
|
||||
'parameters' => $bridge::PARAMETERS,
|
||||
'icon' => $bridge->getIcon(),
|
||||
],
|
||||
];
|
||||
$results[] = $content;
|
||||
}
|
||||
if ($results === []) {
|
||||
return new Response(Json::encode(['message' => 'No bridge found for given url']), 404, ['content-type' => 'application/json']);
|
||||
}
|
||||
return new Response(Json::encode($results), 200, ['content-type' => 'application/json']);
|
||||
}
|
||||
|
||||
// Get parameter name in the actual context, or in the global parameter
|
||||
private function getParameterName($bridge, $context, $key)
|
||||
{
|
||||
if (isset($bridge::PARAMETERS[$context][$key]['name'])) {
|
||||
$name = $bridge::PARAMETERS[$context][$key]['name'];
|
||||
} else if (isset($bridge::PARAMETERS['global'][$key]['name'])) {
|
||||
$name = $bridge::PARAMETERS['global'][$key]['name'];
|
||||
} else {
|
||||
$name = 'Variable "' . $key . '" (No name provided)';
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
final class FrontpageAction implements ActionInterface
|
||||
{
|
||||
private BridgeFactory $bridgeFactory;
|
||||
|
||||
public function __construct(
|
||||
BridgeFactory $bridgeFactory
|
||||
) {
|
||||
$this->bridgeFactory = $bridgeFactory;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$token = $request->getAttribute('token');
|
||||
|
||||
$messages = [];
|
||||
$activeBridges = 0;
|
||||
|
||||
$bridgeClassNames = $this->bridgeFactory->getBridgeClassNames();
|
||||
|
||||
foreach ($this->bridgeFactory->getMissingEnabledBridges() as $missingEnabledBridge) {
|
||||
$messages[] = [
|
||||
'body' => sprintf('Warning : Bridge "%s" not found', $missingEnabledBridge),
|
||||
'level' => 'warning'
|
||||
];
|
||||
}
|
||||
|
||||
$body = '';
|
||||
foreach ($bridgeClassNames as $bridgeClassName) {
|
||||
if ($this->bridgeFactory->isEnabled($bridgeClassName)) {
|
||||
$body .= BridgeCard::render($this->bridgeFactory, $bridgeClassName, $token);
|
||||
$activeBridges++;
|
||||
}
|
||||
}
|
||||
|
||||
$response = new Response(render(__DIR__ . '/../templates/frontpage.html.php', [
|
||||
'messages' => $messages,
|
||||
'admin_email' => Configuration::getConfig('admin', 'email'),
|
||||
'admin_telegram' => Configuration::getConfig('admin', 'telegram'),
|
||||
'bridges' => $body,
|
||||
'active_bridges' => $activeBridges,
|
||||
'total_bridges' => count($bridgeClassNames),
|
||||
]));
|
||||
|
||||
// TODO: The rendered template could be cached, but beware config changes that changes the html
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class HealthAction implements ActionInterface
|
||||
{
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$response = [
|
||||
'code' => 200,
|
||||
'message' => 'all is good',
|
||||
];
|
||||
return new Response(Json::encode($response), 200, ['content-type' => 'application/json']);
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ListAction implements ActionInterface
|
||||
{
|
||||
private BridgeFactory $bridgeFactory;
|
||||
|
||||
public function __construct(
|
||||
BridgeFactory $bridgeFactory
|
||||
) {
|
||||
$this->bridgeFactory = $bridgeFactory;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$list = new \stdClass();
|
||||
$list->bridges = [];
|
||||
$list->total = 0;
|
||||
|
||||
foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
|
||||
$bridge = $this->bridgeFactory->create($bridgeClassName);
|
||||
|
||||
$list->bridges[$bridgeClassName] = [
|
||||
'status' => $this->bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
|
||||
'uri' => $bridge->getURI(),
|
||||
'donationUri' => $bridge->getDonationURI(),
|
||||
'name' => $bridge->getName(),
|
||||
'icon' => $bridge->getIcon(),
|
||||
'parameters' => $bridge->getParameters(),
|
||||
'maintainer' => $bridge->getMaintainer(),
|
||||
'description' => $bridge->getDescription()
|
||||
];
|
||||
}
|
||||
$list->total = count($list->bridges);
|
||||
return new Response(Json::encode($list), 200, ['content-type' => 'application/json']);
|
||||
}
|
||||
}
|
8
app.json
8
app.json
@ -1,8 +0,0 @@
|
||||
{
|
||||
"service": "Heroku",
|
||||
"name": "rss-bridge-heroku",
|
||||
"description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one.",
|
||||
"repository": "https://github.com/RSS-Bridge/rss-bridge?1651005770",
|
||||
"keywords": ["php", "rss-bridge", "rss"]
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Remove all items from the cache
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../lib/bootstrap.php';
|
||||
require __DIR__ . '/../lib/config.php';
|
||||
|
||||
$container = require __DIR__ . '/../lib/dependencies.php';
|
||||
|
||||
/** @var CacheInterface $cache */
|
||||
$cache = $container['cache'];
|
||||
|
||||
$cache->clear();
|
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Remove all expired items from the cache
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../lib/bootstrap.php';
|
||||
require __DIR__ . '/../lib/config.php';
|
||||
|
||||
$container = require __DIR__ . '/../lib/dependencies.php';
|
||||
|
||||
if (
|
||||
Configuration::getConfig('cache', 'type') === 'file'
|
||||
&& !Configuration::getConfig('FileCache', 'enable_purge')
|
||||
) {
|
||||
// Override enable_purge for this particular execution
|
||||
Configuration::setConfig('FileCache', 'enable_purge', true);
|
||||
}
|
||||
|
||||
/** @var CacheInterface $cache */
|
||||
$cache = $container['cache'];
|
||||
|
||||
$cache->prune();
|
20
bin/test
20
bin/test
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Add log records to all three levels (for testing purposes)
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../lib/bootstrap.php';
|
||||
require __DIR__ . '/../lib/config.php';
|
||||
|
||||
$container = require __DIR__ . '/../lib/dependencies.php';
|
||||
|
||||
/** @var Logger $logger */
|
||||
$logger = $container['logger'];
|
||||
|
||||
$logger->debug('This is a test debug message');
|
||||
|
||||
$logger->info('This is a test info message');
|
||||
|
||||
$logger->error('This is a test error message');
|
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ABCNewsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ABC News Bridge';
|
||||
const URI = 'https://www.abc.net.au';
|
||||
const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation';
|
||||
const MAINTAINER = 'yue-dongchen';
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'topic' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Region',
|
||||
'title' => 'Choose state',
|
||||
'values' => [
|
||||
'ACT' => 'act',
|
||||
'NSW' => 'nsw',
|
||||
'NT' => 'nt',
|
||||
'QLD' => 'qld',
|
||||
'SA' => 'sa',
|
||||
'TAS' => 'tas',
|
||||
'VIC' => 'vic',
|
||||
'WA' => 'wa'
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = sprintf('https://www.abc.net.au/news/%s', $this->getInput('topic'));
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
$dom = $dom->find('div[data-component="PaginationList"]', 0);
|
||||
if (!$dom) {
|
||||
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
|
||||
}
|
||||
$dom = defaultLinkTo($dom, $this->getURI());
|
||||
foreach ($dom->find('article[data-component="DetailCard"]') as $article) {
|
||||
$a = $article->find('a', 0);
|
||||
$this->items[] = [
|
||||
'title' => $a->plaintext,
|
||||
'uri' => $a->href,
|
||||
'content' => $article->find('p', 0)->plaintext,
|
||||
'timestamp' => strtotime($article->find('time', 0)->datetime),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
42
bridges/ABCTabsBridge.php
Normal file
42
bridges/ABCTabsBridge.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
class ABCTabsBridge extends BridgeAbstract {
|
||||
|
||||
const MAINTAINER = 'kranack';
|
||||
const NAME = 'ABC Tabs Bridge';
|
||||
const URI = 'https://www.abc-tabs.com/';
|
||||
const DESCRIPTION = 'Returns 22 newest tabs';
|
||||
|
||||
public function collectData(){
|
||||
$html = '';
|
||||
$html = getSimpleHTMLDOM(static::URI . 'tablatures/nouveautes.html')
|
||||
or returnClientError('No results for this query.');
|
||||
|
||||
$table = $html->find('table#myTable', 0)->children(1);
|
||||
|
||||
foreach ($table->find('tr') as $tab) {
|
||||
$item = array();
|
||||
$item['author'] = $tab->find('td', 1)->plaintext
|
||||
. ' - '
|
||||
. $tab->find('td', 2)->plaintext;
|
||||
|
||||
$item['title'] = $tab->find('td', 1)->plaintext
|
||||
. ' - '
|
||||
. $tab->find('td', 2)->plaintext;
|
||||
|
||||
$item['content'] = 'Le '
|
||||
. $tab->find('td', 0)->plaintext
|
||||
. '<br> Par: '
|
||||
. $tab->find('td', 5)->plaintext
|
||||
. '<br> Type: '
|
||||
. $tab->find('td', 3)->plaintext;
|
||||
|
||||
$item['id'] = static::URI
|
||||
. $tab->find('td', 2)->find('a', 0)->getAttribute('href');
|
||||
|
||||
$item['uri'] = static::URI
|
||||
. $tab->find('td', 2)->find('a', 0)->getAttribute('href');
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ABolaBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'A Bola';
|
||||
const URI = 'https://abola.pt/';
|
||||
const DESCRIPTION = 'Returns news from the Portuguese sports newspaper A BOLA.PT';
|
||||
const MAINTAINER = 'rmscoelho';
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'feed' => [
|
||||
'name' => 'News Feed',
|
||||
'type' => 'list',
|
||||
'title' => 'Feeds from the Portuguese sports newspaper A BOLA.PT',
|
||||
'values' => [
|
||||
'Últimas' => 'Nnh/Noticias',
|
||||
'Seleção Nacional' => 'Selecao/Noticias',
|
||||
'Futebol Nacional' => [
|
||||
'Notícias' => 'Nacional/Noticias',
|
||||
'Primeira Liga' => 'Nacional/Liga/Noticias',
|
||||
'Liga 2' => 'Nacional/Liga2/Noticias',
|
||||
'Liga 3' => 'Nacional/Liga3/Noticias',
|
||||
'Liga Revelação' => 'Nacional/Liga-Revelacao/Noticias',
|
||||
'Campeonato de Portugal' => 'Nacional/Campeonato-Portugal/Noticias',
|
||||
'Distritais' => 'Nacional/Distritais/Noticias',
|
||||
'Taça de Portugal' => 'Nacional/TPortugal/Noticias',
|
||||
'Futebol Feminino' => 'Nacional/FFeminino/Noticias',
|
||||
'Futsal' => 'Nacional/Futsal/Noticias',
|
||||
],
|
||||
'Futebol Internacional' => [
|
||||
'Notícias' => 'Internacional/Noticias/Noticias',
|
||||
'Liga dos Campeões' => 'Internacional/Liga-dos-campeoes/Noticias',
|
||||
'Liga Europa' => 'Internacional/Liga-europa/Noticias',
|
||||
'Liga Conferência' => 'Internacional/Liga-conferencia/Noticias',
|
||||
'Liga das Nações' => 'Internacional/Liga-das-nacoes/Noticias',
|
||||
'UEFA Youth League' => 'Internacional/Uefa-Youth-League/Noticias',
|
||||
],
|
||||
'Mercado' => 'Mercado',
|
||||
'Modalidades' => 'Modalidades/Noticias',
|
||||
'Motores' => 'Motores/Noticias',
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://abola.pt/img/icons/favicon-96x96.png';
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return !is_null($this->getKey('feed')) ? self::NAME . ' | ' . $this->getKey('feed') : self::NAME;
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
return self::URI . $this->getInput('feed');
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = sprintf('https://abola.pt/%s', $this->getInput('feed'));
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
if ($this->getInput('feed') !== 'Mercado') {
|
||||
$dom = $dom->find('div#body_Todas1_upNoticiasTodas', 0);
|
||||
} else {
|
||||
$dom = $dom->find('div#body_NoticiasMercado_upNoticiasTodas', 0);
|
||||
}
|
||||
if (!$dom) {
|
||||
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
|
||||
}
|
||||
$dom = defaultLinkTo($dom, $this->getURI());
|
||||
foreach ($dom->find('div.media') as $key => $article) {
|
||||
//Get thumbnail
|
||||
$image = $article->find('.media-img', 0)->style;
|
||||
$image = preg_replace('/background-image: url\(/i', '', $image);
|
||||
$image = substr_replace($image, '', -4);
|
||||
$image = preg_replace('/https:\/\//i', '', $image);
|
||||
$image = preg_replace('/www\./i', '', $image);
|
||||
$image = preg_replace('/\/\//', '/', $image);
|
||||
$image = preg_replace('/\/\/\//', '//', $image);
|
||||
$image = substr($image, 7);
|
||||
$image = 'https://' . $image;
|
||||
$image = preg_replace('/ptimg/', 'pt/img', $image);
|
||||
$image = preg_replace('/\/\/bola/', 'www.abola', $image);
|
||||
//Timestamp
|
||||
$date = date('Y/m/d');
|
||||
if (!is_null($article->find("span#body_Todas1_rptNoticiasTodas_lblData_$key", 0))) {
|
||||
$date = $article->find("span#body_Todas1_rptNoticiasTodas_lblData_$key", 0)->plaintext;
|
||||
$date = preg_replace('/\./', '/', $date);
|
||||
}
|
||||
$time = $article->find("span#body_Todas1_rptNoticiasTodas_lblHora_$key", 0)->plaintext;
|
||||
$date = explode('/', $date);
|
||||
$time = explode(':', $time);
|
||||
$year = $date[0];
|
||||
$month = $date[1];
|
||||
$day = $date[2];
|
||||
$hour = $time[0];
|
||||
$minute = $time[1];
|
||||
$timestamp = mktime($hour, $minute, 0, $month, $day, $year);
|
||||
//Content
|
||||
$image = '<img src="' . $image . '" alt="' . $article->find('h4 span', 0)->plaintext . '" />';
|
||||
$description = '<p>' . $article->find('.media-texto > span', 0)->plaintext . '</p>';
|
||||
$content = $image . '</br>' . $description;
|
||||
$a = $article->find('.media-body > a', 0);
|
||||
$this->items[] = [
|
||||
'title' => $a->find('h4 span', 0)->plaintext,
|
||||
'uri' => $a->href,
|
||||
'content' => $content,
|
||||
'timestamp' => $timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,238 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AO3Bridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'AO3';
|
||||
const URI = 'https://archiveofourown.org/';
|
||||
const CACHE_TIMEOUT = 1800;
|
||||
const DESCRIPTION = 'Returns works or chapters from Archive of Our Own';
|
||||
const MAINTAINER = 'Obsidienne';
|
||||
const PARAMETERS = [
|
||||
'List' => [
|
||||
'url' => [
|
||||
'name' => 'url',
|
||||
'required' => true,
|
||||
// Example: F/F tag
|
||||
'exampleValue' => 'https://archiveofourown.org/tags/F*s*F/works',
|
||||
],
|
||||
'range' => [
|
||||
'name' => 'Chapter Content',
|
||||
'title' => 'Chapter(s) to include in each work\'s feed entry',
|
||||
'defaultValue' => null,
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'None' => null,
|
||||
'First' => 'first',
|
||||
'Latest' => 'last',
|
||||
'Entire work' => 'all',
|
||||
],
|
||||
],
|
||||
'limit' => self::LIMIT,
|
||||
],
|
||||
'Bookmarks' => [
|
||||
'user' => [
|
||||
'name' => 'user',
|
||||
'required' => true,
|
||||
// Example: Nyaaru's bookmarks
|
||||
'exampleValue' => 'Nyaaru',
|
||||
],
|
||||
],
|
||||
'Work' => [
|
||||
'id' => [
|
||||
'name' => 'id',
|
||||
'required' => true,
|
||||
// Example: latest chapters from A Better Past by LysSerris
|
||||
'exampleValue' => '18181853',
|
||||
],
|
||||
]
|
||||
];
|
||||
private $title;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Bookmarks':
|
||||
$this->collectList($this->getURI());
|
||||
break;
|
||||
case 'List':
|
||||
$this->collectList($this->getURI());
|
||||
break;
|
||||
case 'Work':
|
||||
$this->collectWork($this->getURI());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed for lists of works (e.g. recent works, search results, filtered tags,
|
||||
* bookmarks, series, collections).
|
||||
*/
|
||||
private function collectList($url)
|
||||
{
|
||||
$version = 'v0.0.1';
|
||||
$headers = [
|
||||
"useragent: rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)"
|
||||
];
|
||||
$response = getContents($url, $headers);
|
||||
|
||||
$html = \str_get_html($response);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
// Get list title. Will include page range + count in some cases
|
||||
$heading = ($html->find('#main h2', 0));
|
||||
if ($heading->find('a.tag')) {
|
||||
$heading = $heading->find('a.tag', 0);
|
||||
}
|
||||
$this->title = $heading->plaintext;
|
||||
|
||||
$limit = $this->getInput('limit') ?? 3;
|
||||
$count = 0;
|
||||
foreach ($html->find('.index.group > li') as $element) {
|
||||
$item = [];
|
||||
|
||||
$title = $element->find('div h4 a', 0);
|
||||
if (!isset($title)) {
|
||||
continue; // discard deleted works
|
||||
}
|
||||
$item['title'] = $title->plaintext;
|
||||
$item['uri'] = $title->href;
|
||||
|
||||
$strdate = $element->find('div p.datetime', 0)->plaintext;
|
||||
$item['timestamp'] = strtotime($strdate);
|
||||
|
||||
// detach from rest of page because remove() is buggy
|
||||
$element = str_get_html($element->outertext());
|
||||
$tags = $element->find('ul.required-tags', 0);
|
||||
foreach ($tags->childNodes() as $tag) {
|
||||
$item['categories'][] = html_entity_decode($tag->plaintext);
|
||||
}
|
||||
$tags->remove();
|
||||
$tags = $element->find('ul.tags', 0);
|
||||
foreach ($tags->childNodes() as $tag) {
|
||||
$item['categories'][] = html_entity_decode($tag->plaintext);
|
||||
}
|
||||
$tags->remove();
|
||||
|
||||
$item['content'] = implode('', $element->childNodes());
|
||||
|
||||
$chapters = $element->find('dl dd.chapters', 0);
|
||||
// bookmarked series and external works do not have a chapters count
|
||||
$chapters = (isset($chapters) ? $chapters->plaintext : 0);
|
||||
$item['uid'] = $item['uri'] . "/$strdate/$chapters";
|
||||
|
||||
// Fetch workskin of desired chapter(s) in list
|
||||
if ($this->getInput('range') && ($limit == 0 || $count++ < $limit)) {
|
||||
$url = $item['uri'];
|
||||
switch ($this->getInput('range')) {
|
||||
case ('all'):
|
||||
$url .= '?view_full_work=true';
|
||||
break;
|
||||
case ('first'):
|
||||
break;
|
||||
case ('last'):
|
||||
// only way to get this is using the navigate page unfortunately
|
||||
$url .= '/navigate';
|
||||
$response = getContents($url, $headers);
|
||||
$html = \str_get_html($response);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
$url = $html->find('ol.index.group > li > a', -1)->href;
|
||||
break;
|
||||
}
|
||||
$response = getContents($url, $headers);
|
||||
|
||||
$html = \str_get_html($response);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
// remove duplicate fic summary
|
||||
if ($ficsum = $html->find('#workskin > .preface > .summary', 0)) {
|
||||
$ficsum->remove();
|
||||
}
|
||||
$item['content'] .= $html->find('#workskin', 0);
|
||||
}
|
||||
|
||||
// Use predictability of download links to generate enclosures
|
||||
$wid = explode('/', $item['uri'])[4];
|
||||
foreach (['azw3', 'epub', 'mobi', 'pdf', 'html'] as $ext) {
|
||||
$item['enclosures'][] = 'https://archiveofourown.org/downloads/' . $wid . '/work.' . $ext;
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed for recent chapters of a specific work.
|
||||
*/
|
||||
private function collectWork($url)
|
||||
{
|
||||
$version = 'v0.0.1';
|
||||
$headers = [
|
||||
"useragent: rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)"
|
||||
];
|
||||
$response = getContents($url . '/navigate', $headers);
|
||||
|
||||
$html = \str_get_html($response);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
$response = getContents($url . '?view_full_work=true', $headers);
|
||||
|
||||
$workhtml = \str_get_html($response);
|
||||
$workhtml = defaultLinkTo($workhtml, self::URI);
|
||||
|
||||
$this->title = $html->find('h2 a', 0)->plaintext;
|
||||
|
||||
$nav = $html->find('ol.index.group > li');
|
||||
for ($i = 0; $i < count($nav); $i++) {
|
||||
$item = [];
|
||||
|
||||
$element = $nav[$i];
|
||||
$item['title'] = $element->find('a', 0)->plaintext;
|
||||
$item['content'] = $workhtml->find('#chapter-' . ($i + 1), 0);
|
||||
$item['uri'] = $element->find('a', 0)->href;
|
||||
|
||||
$strdate = $element->find('span.datetime', 0)->plaintext;
|
||||
$strdate = str_replace('(', '', $strdate);
|
||||
$strdate = str_replace(')', '', $strdate);
|
||||
$item['timestamp'] = strtotime($strdate);
|
||||
|
||||
$item['uid'] = $item['uri'] . "/$strdate";
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
$this->items = array_reverse($this->items);
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
$name = parent::getName() . " $this->queriedContext";
|
||||
if (isset($this->title)) {
|
||||
$name .= " - $this->title";
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return self::URI . '/favicon.ico';
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
$url = parent::getURI();
|
||||
switch ($this->queriedContext) {
|
||||
case 'Bookmarks':
|
||||
$user = $this->getInput('user');
|
||||
$url = self::URI
|
||||
. '/users/' . $user
|
||||
. '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
|
||||
break;
|
||||
case 'List':
|
||||
$url = $this->getInput('url');
|
||||
break;
|
||||
case 'Work':
|
||||
$url = self::URI . '/works/' . $this->getInput('id');
|
||||
break;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ARDAudiothekBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ARD-Audiothek Bridge';
|
||||
const URI = 'https://www.ardaudiothek.de';
|
||||
const DESCRIPTION = 'Feed of any show in the ARD-Audiothek, specified by its path';
|
||||
const MAINTAINER = 'Mar-Koeh';
|
||||
/*
|
||||
* The URL Prefix of the API
|
||||
* @const APIENDPOINT https-URL of the used endpoint, ending in `/`
|
||||
*/
|
||||
const APIENDPOINT = 'https://api.ardaudiothek.de/';
|
||||
/*
|
||||
* The requested width of the preview image
|
||||
* 448 and 128 have been observed on the wild
|
||||
* @const IMAGEWIDTH width in px of the preview image
|
||||
*/
|
||||
const IMAGEWIDTH = 448;
|
||||
/*
|
||||
* Placeholder that will be replace by IMAGEWIDTH in the preview image URL
|
||||
* @const IMAGEWIDTHPLACEHOLDER
|
||||
*/
|
||||
const IMAGEWIDTHPLACEHOLDER = '{width}';
|
||||
/*
|
||||
* File extension appended to image link in $this->icon
|
||||
* @const IMAGEEXTENSION
|
||||
*/
|
||||
const IMAGEEXTENSION = '.jpg';
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'path' => [
|
||||
'name' => 'Show Link or ID',
|
||||
'required' => true,
|
||||
'title' => 'Link to the show page or just its numeric suffix',
|
||||
'defaultValue' => 'https://www.ardaudiothek.de/sendung/kalk-welk/10777871/'
|
||||
],
|
||||
'limit' => self::LIMIT,
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Holds the title of the current show
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* Holds the URI of the show
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $uri;
|
||||
|
||||
/**
|
||||
* Holds the icon of the feed
|
||||
*
|
||||
*/
|
||||
private $icon;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$path = $this->getInput('path');
|
||||
$limit = $this->getInput('limit');
|
||||
|
||||
$oldTz = date_default_timezone_get();
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
$pathComponents = explode('/', $path);
|
||||
if (empty($pathComponents)) {
|
||||
returnClientError('Path may not be empty');
|
||||
}
|
||||
if (count($pathComponents) < 2) {
|
||||
$showID = $pathComponents[0];
|
||||
} else {
|
||||
$lastKey = count($pathComponents) - 1;
|
||||
$showID = $pathComponents[$lastKey];
|
||||
if (strlen($showID) === 0) {
|
||||
$showID = $pathComponents[$lastKey - 1];
|
||||
}
|
||||
}
|
||||
|
||||
$url = self::APIENDPOINT . 'programsets/' . $showID . '/';
|
||||
$json1 = getContents($url);
|
||||
$data1 = Json::decode($json1, false);
|
||||
$processedJSON = $data1->data->programSet;
|
||||
if (!$processedJSON) {
|
||||
throw new \Exception('Unable to find show id: ' . $showID);
|
||||
}
|
||||
|
||||
$answerLength = 1;
|
||||
$offset = 0;
|
||||
$numberOfElements = 1;
|
||||
|
||||
while ($answerLength != 0 && $offset < $numberOfElements && (is_null($limit) || $offset < $limit)) {
|
||||
$json2 = getContents($url . '?offset=' . $offset);
|
||||
$data2 = Json::decode($json2, false);
|
||||
$processedJSON = $data2->data->programSet;
|
||||
|
||||
$answerLength = count($processedJSON->items->nodes);
|
||||
$offset = $offset + $answerLength;
|
||||
$numberOfElements = $processedJSON->numberOfElements;
|
||||
|
||||
foreach ($processedJSON->items->nodes as $audio) {
|
||||
$item = [];
|
||||
$item['uri'] = $audio->sharingUrl;
|
||||
$item['title'] = $audio->title;
|
||||
$imageSquare = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $audio->image->url1X1);
|
||||
$image = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $audio->image->url);
|
||||
$item['enclosures'] = [
|
||||
$audio->audios[0]->url,
|
||||
$imageSquare
|
||||
];
|
||||
// synopsis in list is shortened, full synopsis is available using one request per item
|
||||
$item['content'] = '<img src="' . $image . '" /><p>' . $audio->synopsis . '</p>';
|
||||
$item['timestamp'] = $audio->publicationStartDateAndTime;
|
||||
$item['uid'] = $audio->id;
|
||||
$item['author'] = $audio->programSet->publicationService->title;
|
||||
|
||||
$category = $audio->programSet->editorialCategories->title ?? null;
|
||||
if ($category) {
|
||||
$item['categories'] = [$category];
|
||||
}
|
||||
|
||||
$item['itunes'] = [
|
||||
'duration' => $audio->duration,
|
||||
];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
$this->title = $processedJSON->title;
|
||||
$this->uri = $processedJSON->sharingUrl;
|
||||
$this->icon = str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $processedJSON->image->url1X1);
|
||||
// add image file extension to URL so icon is shown in generated RSS feeds, see
|
||||
// https://github.com/RSS-Bridge/rss-bridge/blob/4aed05c7b678b5673386d61374bba13637d15487/formats/MrssFormat.php#L76
|
||||
$this->icon = $this->icon . self::IMAGEEXTENSION;
|
||||
|
||||
$this->items = array_slice($this->items, 0, $limit);
|
||||
|
||||
date_default_timezone_set($oldTz);
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getURI()
|
||||
{
|
||||
if (!empty($this->uri)) {
|
||||
return $this->uri;
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getName()
|
||||
{
|
||||
if (!empty($this->title)) {
|
||||
return $this->title;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getIcon()
|
||||
{
|
||||
if (!empty($this->icon)) {
|
||||
return $this->icon;
|
||||
}
|
||||
return parent::getIcon();
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ARDMediathekBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ARD-Mediathek Bridge';
|
||||
const URI = 'https://www.ardmediathek.de';
|
||||
const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path';
|
||||
const MAINTAINER = 'yue-dongchen';
|
||||
/*
|
||||
* Number of Items to be requested from ARDmediathek API
|
||||
* 12 has been observed on the wild
|
||||
* 29 is the highest successfully tested value
|
||||
* More Items could be fetched via pagination
|
||||
* The JSON-field pagination holds more information on that
|
||||
* @const PAGESIZE number of requested items
|
||||
*/
|
||||
const PAGESIZE = 29;
|
||||
/*
|
||||
* The URL Prefix of the (Webapp-)API
|
||||
* @const APIENDPOINT https-URL of the used endpoint
|
||||
*/
|
||||
const APIENDPOINT = 'https://api.ardmediathek.de/page-gateway/widgets/ard/asset/';
|
||||
/*
|
||||
* The URL prefix of the video link
|
||||
* URLs from the webapp include a slug containing titles of show, episode, and tv station.
|
||||
* It seems to work without that.
|
||||
* @const VIDEOLINKPREFIX https-URL prefix of video links
|
||||
*/
|
||||
const VIDEOLINKPREFIX = 'https://www.ardmediathek.de/video/';
|
||||
/*
|
||||
* The requested width of the preview image
|
||||
* 432 has been observed on the wild
|
||||
* The webapp seems to also compute and add the height value
|
||||
* It seems to works without that.
|
||||
* @const IMAGEWIDTH width in px of the preview image
|
||||
*/
|
||||
const IMAGEWIDTH = 432;
|
||||
/*
|
||||
* Placeholder that will be replace by IMAGEWIDTH in the preview image URL
|
||||
* @const IMAGEWIDTHPLACEHOLDER
|
||||
*/
|
||||
const IMAGEWIDTHPLACEHOLDER = '{width}';
|
||||
/**
|
||||
* Title of the current show
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'path' => [
|
||||
'name' => 'Show Link or ID',
|
||||
'required' => true,
|
||||
'title' => 'Link to the show page or just its alphanumeric suffix',
|
||||
'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$oldTz = date_default_timezone_get();
|
||||
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
$pathComponents = explode('/', $this->getInput('path'));
|
||||
if (empty($pathComponents)) {
|
||||
returnClientError('Path may not be empty');
|
||||
}
|
||||
if (count($pathComponents) < 2) {
|
||||
$showID = $pathComponents[0];
|
||||
} else {
|
||||
$lastKey = count($pathComponents) - 1;
|
||||
$showID = $pathComponents[$lastKey];
|
||||
if (strlen($showID) === 0) {
|
||||
$showID = $pathComponents[$lastKey - 1];
|
||||
}
|
||||
}
|
||||
|
||||
$url = self::APIENDPOINT . $showID . '?pageSize=' . self::PAGESIZE;
|
||||
$rawJSON = getContents($url);
|
||||
$processedJSON = json_decode($rawJSON);
|
||||
|
||||
foreach ($processedJSON->teasers as $video) {
|
||||
$item = [];
|
||||
// there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId
|
||||
$item['uri'] = self::VIDEOLINKPREFIX . $video->id . '/';
|
||||
// there is also ->mediumTitle and ->shortTitle
|
||||
$item['title'] = $video->longTitle;
|
||||
// in the test, aspect16x9 was the only child of images, not sure whether that is always true
|
||||
$item['enclosures'] = [
|
||||
str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $video->images->aspect16x9->src)
|
||||
];
|
||||
$item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>';
|
||||
$item['timestamp'] = $video->broadcastedOn;
|
||||
$item['uid'] = $video->id;
|
||||
$item['author'] = $video->publicationService->name;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
$this->title = $processedJSON->title;
|
||||
|
||||
date_default_timezone_set($oldTz);
|
||||
}
|
||||
|
||||
/** {@inheritdoc} */
|
||||
public function getName()
|
||||
{
|
||||
if (!empty($this->title)) {
|
||||
return $this->title;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ASRockNewsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ASRock News Bridge';
|
||||
const URI = 'https://www.asrock.com';
|
||||
const DESCRIPTION = 'Returns latest news articles';
|
||||
const MAINTAINER = 'VerifiedJoseph';
|
||||
const PARAMETERS = [];
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI . '/news/index.asp');
|
||||
|
||||
$html = defaultLinkTo($html, self::URI . '/news/');
|
||||
|
||||
foreach ($html->find('div.inner > a') as $index => $a) {
|
||||
$item = [];
|
||||
|
||||
$articlePath = $a->href;
|
||||
|
||||
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT);
|
||||
|
||||
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
|
||||
|
||||
$contents = $articlePageHtml->find('div.Contents', 0);
|
||||
|
||||
$item['uri'] = $articlePath;
|
||||
$item['title'] = $contents->find('h3', 0)->innertext;
|
||||
|
||||
$contents->find('h3', 0)->outertext = '';
|
||||
|
||||
$item['content'] = $contents->innertext;
|
||||
$item['timestamp'] = $this->extractDate($a->plaintext);
|
||||
|
||||
$img = $a->find('img', 0);
|
||||
if ($img) {
|
||||
$item['enclosures'][] = $img->src;
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
if (count($this->items) >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function extractDate($text)
|
||||
{
|
||||
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
|
||||
|
||||
$text = trim($text);
|
||||
|
||||
if (preg_match($dateRegex, $text, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,37 +1,25 @@
|
||||
<?php
|
||||
class AcrimedBridge extends FeedExpander {
|
||||
|
||||
class AcrimedBridge extends FeedExpander
|
||||
{
|
||||
const MAINTAINER = 'qwertygc';
|
||||
const NAME = 'Acrimed Bridge';
|
||||
const URI = 'https://www.acrimed.org/';
|
||||
const CACHE_TIMEOUT = 4800; //2hours
|
||||
const DESCRIPTION = 'Returns the newest articles';
|
||||
const MAINTAINER = 'qwertygc';
|
||||
const NAME = 'Acrimed Bridge';
|
||||
const URI = 'http://www.acrimed.org/';
|
||||
const CACHE_TIMEOUT = 4800; //2hours
|
||||
const DESCRIPTION = 'Returns the newest articles';
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'limit' => [
|
||||
'name' => 'limit',
|
||||
'type' => 'number',
|
||||
'defaultValue' => -1,
|
||||
]
|
||||
]
|
||||
];
|
||||
public function collectData(){
|
||||
$this->collectExpandableDatas(static::URI . 'spip.php?page=backend');
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = 'https://www.acrimed.org/spip.php?page=backend';
|
||||
$limit = $this->getInput('limit');
|
||||
$this->collectExpandableDatas($url, $limit);
|
||||
}
|
||||
protected function parseItem($newsItem){
|
||||
$item = parent::parseItem($newsItem);
|
||||
|
||||
protected function parseItem(array $item)
|
||||
{
|
||||
$articlePage = getSimpleHTMLDOM($item['uri']);
|
||||
$article = sanitize($articlePage->find('article.article1', 0)->innertext);
|
||||
$article = defaultLinkTo($article, static::URI);
|
||||
$item['content'] = $article;
|
||||
$articlePage = getSimpleHTMLDOM($newsItem->link);
|
||||
$article = sanitize($articlePage->find('article.article1', 0)->innertext);
|
||||
$article = defaultLinkTo($article, static::URI);
|
||||
$item['content'] = $article;
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ActivisionResearchBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Activision Research Blog';
|
||||
const URI = 'https://research.activision.com';
|
||||
const DESCRIPTION = 'Posts from the Activision Research blog';
|
||||
const MAINTAINER = 'thefranke';
|
||||
const CACHE_TIMEOUT = 86400; // 24h
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$dom = getSimpleHTMLDOM(static::URI);
|
||||
$dom = $dom->find('div[id="home-blog-feed"]', 0);
|
||||
if (!$dom) {
|
||||
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
|
||||
}
|
||||
$dom = defaultLinkTo($dom, $this->getURI());
|
||||
foreach ($dom->find('div[class="blog-entry"]') as $article) {
|
||||
$a = $article->find('a', 0);
|
||||
|
||||
$blogimg = extractFromDelimiters($article->find('div[class="blog-img"]', 0)->style, 'url(', ')');
|
||||
|
||||
$title = htmlspecialchars_decode($article->find('div[class="title"]', 0)->plaintext);
|
||||
$author = htmlspecialchars_decode($article->find('div[class="author]', 0)->plaintext);
|
||||
$date = $article->find('div[class="pubdate"]', 0)->plaintext;
|
||||
|
||||
$entry = getSimpleHTMLDOMCached($a->href, static::CACHE_TIMEOUT * 7 * 4);
|
||||
$entry = defaultLinkTo($entry, $this->getURI());
|
||||
|
||||
$content = $entry->find('div[class="blog-body"]', 0);
|
||||
$tagsremove = ['script', 'iframe', 'input', 'form'];
|
||||
$content = sanitize($content, $tagsremove);
|
||||
$content = '<img src="' . static::URI . $blogimg . '" alt="">' . $content;
|
||||
|
||||
$this->items[] = [
|
||||
'title' => $title,
|
||||
'author' => $author,
|
||||
'uri' => $a->href,
|
||||
'content' => $content,
|
||||
'timestamp' => strtotime($date),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AirBreizhBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'fanch317';
|
||||
const NAME = 'Air Breizh';
|
||||
const URI = 'https://www.airbreizh.asso.fr/';
|
||||
const DESCRIPTION = 'Returns newests publications on Air Breizh';
|
||||
const PARAMETERS = [
|
||||
'Publications' => [
|
||||
'theme' => [
|
||||
'name' => 'Thematique',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tout' => '',
|
||||
'Rapport d\'activite' => 'rapport-dactivite',
|
||||
'Etude' => 'etudes',
|
||||
'Information' => 'information',
|
||||
'Autres documents' => 'autres-documents',
|
||||
'Plan Régional de Surveillance de la qualité de l’air' => 'prsqa',
|
||||
'Transport' => 'transport'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = '';
|
||||
$html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'));
|
||||
|
||||
foreach ($html->find('article') as $article) {
|
||||
$item = [];
|
||||
// Title
|
||||
$item['title'] = $article->find('h2', 0)->plaintext;
|
||||
// Author
|
||||
$item['author'] = 'Air Breizh';
|
||||
// Image
|
||||
$imagelink = $article->find('.card__image', 0)->find('img', 0)->getAttribute('src');
|
||||
// Content preview
|
||||
$item['content'] = '<img src="' . $imagelink . '" />
|
||||
<br/>'
|
||||
. $article->find('.card__text', 0)->plaintext;
|
||||
// URL
|
||||
$item['uri'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href');
|
||||
// ID
|
||||
$item['id'] = $article->find('.publi__buttons', 0)->find('a', 0)->getAttribute('href');
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AlbionOnlineBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Albion Online Changelog';
|
||||
const MAINTAINER = 'otakuf';
|
||||
const URI = 'https://albiononline.com';
|
||||
const DESCRIPTION = 'Returns the changes made to the Albion Online';
|
||||
const CACHE_TIMEOUT = 3600; // 60min
|
||||
|
||||
const PARAMETERS = [ [
|
||||
'postcount' => [
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'Maximum number of items to return',
|
||||
'defaultValue' => 5,
|
||||
],
|
||||
'language' => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'English' => 'en',
|
||||
'Deutsch' => 'de',
|
||||
'Polski' => 'pl',
|
||||
'Français' => 'fr',
|
||||
'Русский' => 'ru',
|
||||
'Português' => 'pt',
|
||||
'Español' => 'es',
|
||||
],
|
||||
'title' => 'Language of changelog posts',
|
||||
'defaultValue' => 'en',
|
||||
],
|
||||
'full' => [
|
||||
'name' => 'Full changelog',
|
||||
'type' => 'checkbox',
|
||||
'required' => false,
|
||||
'title' => 'Enable to receive the full changelog post for each item'
|
||||
],
|
||||
]];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$api = 'https://albiononline.com/';
|
||||
// Example: https://albiononline.com/en/changelog/1/5
|
||||
$url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount');
|
||||
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
foreach ($html->find('li') as $data) {
|
||||
$item = [];
|
||||
$item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href');
|
||||
$item['title'] = trim(explode('|', $data->find('span', 0)->plaintext)[0]);
|
||||
// Time below work only with en lang. Need to think about solution. May be separate request like getFullChangelog, but to english list for all language
|
||||
//print_r( date_parse_from_format( 'M j, Y' , 'Sep 9, 2020') );
|
||||
//$item['timestamp'] = $this->extractDate($a->plaintext);
|
||||
$item['author'] = 'albiononline.com';
|
||||
if ($this->getInput('full')) {
|
||||
$item['content'] = $this->getFullChangelog($item['uri']);
|
||||
} else {
|
||||
//$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext));
|
||||
// Just use title, no info at all or use title and date, see above
|
||||
$item['content'] = $item['title'];
|
||||
}
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getFullChangelog($url)
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached($url);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
return $html->find('div.small-12.columns', 1)->innertext;
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AlfaBankByBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'lassana';
|
||||
const NAME = 'AlfaBank.by Новости';
|
||||
const URI = 'https://www.alfabank.by';
|
||||
const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка';
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
const PARAMETERS = [
|
||||
'News' => [
|
||||
'business' => [
|
||||
'name' => 'Альфа Бизнес',
|
||||
'type' => 'list',
|
||||
'title' => 'В зависимости от выбора, возращает уведомления для" .
|
||||
" клиентов физ. лиц либо для клиентов-юридических лиц и ИП',
|
||||
'values' => [
|
||||
'Новости' => 'news',
|
||||
'Новости бизнеса' => 'newsBusiness'
|
||||
],
|
||||
'defaultValue' => 'news'
|
||||
],
|
||||
'fullContent' => [
|
||||
'name' => 'Включать содержимое',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$business = $this->getInput('business') == 'newsBusiness';
|
||||
$fullContent = $this->getInput('fullContent') == 'on';
|
||||
|
||||
$mainPageUrl = self::URI . '/about/articles/uvedomleniya/';
|
||||
if ($business) {
|
||||
$mainPageUrl .= '?business=true';
|
||||
}
|
||||
$html = getSimpleHTMLDOM($mainPageUrl);
|
||||
$limit = 0;
|
||||
|
||||
foreach ($html->find('a.notifications__item') as $element) {
|
||||
if ($limit < 10) {
|
||||
$item = [];
|
||||
$item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id'));
|
||||
$item['title'] = $element->find('div.item-title', 0)->innertext;
|
||||
$item['timestamp'] = DateTime::createFromFormat(
|
||||
'd M Y',
|
||||
$this->ruMonthsToEn($element->find('div.item-date', 0)->innertext)
|
||||
)->getTimestamp();
|
||||
|
||||
$itemUrl = self::URI . $element->href;
|
||||
if ($business) {
|
||||
$itemUrl = str_replace('?business=true', '', $itemUrl);
|
||||
}
|
||||
$item['uri'] = $itemUrl;
|
||||
|
||||
if ($fullContent) {
|
||||
$itemHtml = getSimpleHTMLDOM($itemUrl);
|
||||
if ($itemHtml) {
|
||||
$item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
$limit++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return static::URI . '/local/images/favicon.ico';
|
||||
}
|
||||
|
||||
private function ruMonthsToEn($date)
|
||||
{
|
||||
$ruMonths = [
|
||||
'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня',
|
||||
'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' ];
|
||||
$enMonths = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December' ];
|
||||
return str_replace($ruMonths, $enMonths, $date);
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AllSidesBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'AllSides';
|
||||
const URI = 'https://www.allsides.com';
|
||||
const DESCRIPTION = 'Balanced news and media bias ratings.';
|
||||
const MAINTAINER = 'Oliver Nutter';
|
||||
const PARAMETERS = [
|
||||
'global' => [
|
||||
'limit' => [
|
||||
'name' => 'Number of posts to return',
|
||||
'type' => 'number',
|
||||
'defaultValue' => 10,
|
||||
'required' => false,
|
||||
'title' => 'Zero or negative values return all posts (ignored if not fetching full article)',
|
||||
],
|
||||
'fetch' => [
|
||||
'name' => 'Fetch full article content',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => 'checked',
|
||||
],
|
||||
],
|
||||
'Headline Roundups' => [],
|
||||
];
|
||||
|
||||
private const ROUNDUPS_URI = self::URI . '/headline-roundups';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Headline Roundups':
|
||||
$index = getSimpleHTMLDOM(self::ROUNDUPS_URI);
|
||||
defaultLinkTo($index, self::ROUNDUPS_URI);
|
||||
$entries = $index->find('table.views-table > tbody > tr');
|
||||
|
||||
$limit = (int) $this->getInput('limit');
|
||||
$fetch = (bool) $this->getInput('fetch');
|
||||
|
||||
if ($limit > 0 && $fetch) {
|
||||
$entries = array_slice($entries, 0, $limit);
|
||||
}
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$item = [
|
||||
'title' => $entry->find('.views-field-name', 0)->text(),
|
||||
'uri' => $entry->find('a', 0)->href,
|
||||
'timestamp' => $entry->find('.date-display-single', 0)->content,
|
||||
'author' => 'AllSides Staff',
|
||||
];
|
||||
|
||||
if ($fetch) {
|
||||
$article = getSimpleHTMLDOMCached($item['uri']);
|
||||
defaultLinkTo($article, $item['uri']);
|
||||
|
||||
$item['content'] = $article->find('.story-id-page-description', 0);
|
||||
|
||||
foreach ($article->find('.page-tags a') as $tag) {
|
||||
$item['categories'][] = $tag->text();
|
||||
}
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if ($this->queriedContext) {
|
||||
return self::NAME . " - {$this->queriedContext}";
|
||||
}
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Headline Roundups':
|
||||
return self::ROUNDUPS_URI;
|
||||
}
|
||||
return self::URI;
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AllegroBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Allegro';
|
||||
const URI = 'https://www.allegro.pl';
|
||||
const DESCRIPTION = 'Returns the search results from the Allegro.pl shopping and bidding portal';
|
||||
const MAINTAINER = 'wrobelda';
|
||||
const PARAMETERS = [[
|
||||
'url' => [
|
||||
'name' => 'Search URL',
|
||||
'title' => 'Copy the URL from your browser\'s address bar after searching for your items and paste it here',
|
||||
'exampleValue' => 'https://allegro.pl/kategoria/swieze-warzywa-cebula-318660',
|
||||
'required' => true,
|
||||
],
|
||||
'cookie' => [
|
||||
'name' => 'The complete cookie value',
|
||||
'title' => 'Paste the value of the cookie value from your browser if you want to prevent Allegro imposing rate limits',
|
||||
'required' => false,
|
||||
],
|
||||
'includeSponsoredOffers' => [
|
||||
'type' => 'checkbox',
|
||||
'name' => 'Include Sponsored Offers',
|
||||
'defaultValue' => 'checked'
|
||||
],
|
||||
'includePromotedOffers' => [
|
||||
'type' => 'checkbox',
|
||||
'name' => 'Include Promoted Offers',
|
||||
'defaultValue' => 'checked'
|
||||
]
|
||||
]];
|
||||
|
||||
public function getName()
|
||||
{
|
||||
$url = $this->getInput('url');
|
||||
if (!$url) {
|
||||
return parent::getName();
|
||||
}
|
||||
$parsedUrl = parse_url($url, PHP_URL_QUERY);
|
||||
if (!$parsedUrl) {
|
||||
return parent::getName();
|
||||
}
|
||||
parse_str($parsedUrl, $fields);
|
||||
|
||||
if (array_key_exists('string', $fields)) {
|
||||
$f = urldecode($fields['string']);
|
||||
} else {
|
||||
$f = false;
|
||||
}
|
||||
if ($f) {
|
||||
return $f;
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
return $this->getInput('url') ?? parent::getURI();
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
# make sure we order by the most recently listed offers
|
||||
$url = preg_replace('/([?&])order=[^&]+(&|$)/', '$1', $this->getInput('url'));
|
||||
$url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . 'order=n';
|
||||
|
||||
$opts = [];
|
||||
|
||||
// If a cookie is provided
|
||||
if ($cookie = $this->getInput('cookie')) {
|
||||
$opts[CURLOPT_COOKIE] = $cookie;
|
||||
}
|
||||
|
||||
$html = getSimpleHTMLDOM($url, [], $opts);
|
||||
|
||||
# if no results found
|
||||
if ($html->find('.mzmg_6m.m9qz_yo._6a66d_-fJr5')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$results = $html->find('article[data-analytics-view-custom-context="REGULAR"]');
|
||||
|
||||
if ($this->getInput('includeSponsoredOffers')) {
|
||||
$results = array_merge($results, $html->find('article[data-analytics-view-custom-context="SPONSORED"]'));
|
||||
}
|
||||
|
||||
if ($this->getInput('includePromotedOffers')) {
|
||||
$results = array_merge($results, $html->find('article[data-analytics-view-custom-context="PROMOTED"]'));
|
||||
}
|
||||
|
||||
foreach ($results as $post) {
|
||||
$item = [];
|
||||
|
||||
$item['uid'] = $post->{'data-analytics-view-value'};
|
||||
|
||||
$item_link = $post->find('a[href*="' . $item['uid'] . '"], a[href*="allegrolokalnie"]', 0);
|
||||
|
||||
$item['uri'] = $item_link->href;
|
||||
|
||||
$item['title'] = $item_link->find('img', 0)->alt;
|
||||
|
||||
$image = $item_link->find('img', 0)->{'data-src'} ?: $item_link->find('img', 0)->src ?? false;
|
||||
|
||||
if ($image) {
|
||||
$item['enclosures'] = [$image . '#.image'];
|
||||
}
|
||||
|
||||
$price = $post->{'data-analytics-view-json-custom-price'};
|
||||
if ($price) {
|
||||
$priceDecoded = json_decode(html_entity_decode($price));
|
||||
$price = $priceDecoded->amount . ' ' . $priceDecoded->currency;
|
||||
}
|
||||
|
||||
$descriptionPatterns = ['/<\s*dt[^>]*>\b/', '/<\/dt>/', '/<\s*dd[^>]*>\b/', '/<\/dd>/'];
|
||||
$descriptionReplacements = ['<span>', ':</span> ', '<strong>', ' </strong> '];
|
||||
$description = $post->find('.m7er_k4.mpof_5r.mpof_z0_s', 0)->innertext;
|
||||
$descriptionPretty = preg_replace($descriptionPatterns, $descriptionReplacements, $description);
|
||||
|
||||
$pricingExtraInfo = array_filter($post->find('.mqu1_g3.mgn2_12'), function ($node) {
|
||||
return empty($node->find('.mvrt_0'));
|
||||
});
|
||||
|
||||
$pricingExtraInfo = $pricingExtraInfo[0]->plaintext ?? '';
|
||||
|
||||
$offerExtraInfo = array_map(function ($node) {
|
||||
return str_contains($node->plaintext, 'zapłać później') ? '' : $node->outertext;
|
||||
}, $post->find('div.mpof_ki.mwdn_1.mj7a_4.mgn2_12'));
|
||||
|
||||
$isSmart = $post->find('img[alt="Smart!"]', 0) ?? false;
|
||||
if ($isSmart) {
|
||||
$pricingExtraInfo .= $isSmart->outertext;
|
||||
}
|
||||
|
||||
$item['categories'] = [];
|
||||
$parameters = $post->find('dd');
|
||||
foreach ($parameters as $parameter) {
|
||||
if (in_array(strtolower($parameter->innertext), ['brak', 'nie'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item['categories'][] = $parameter->innertext;
|
||||
}
|
||||
|
||||
$item['content'] = $descriptionPretty
|
||||
. '<div><strong>'
|
||||
. $price
|
||||
. '</strong></div><div>'
|
||||
. implode('</div><div>', $offerExtraInfo)
|
||||
. '</div><dl>'
|
||||
. $pricingExtraInfo
|
||||
. '</dl><hr>';
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,107 +1,87 @@
|
||||
<?php
|
||||
class AllocineFRBridge extends BridgeAbstract {
|
||||
|
||||
class AllocineFRBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'superbaillot.net';
|
||||
const NAME = 'Allo Cine Bridge';
|
||||
const CACHE_TIMEOUT = 25200; // 7h
|
||||
const URI = 'https://www.allocine.fr';
|
||||
const DESCRIPTION = 'Bridge for allocine.fr';
|
||||
const PARAMETERS = [ [
|
||||
'category' => [
|
||||
'name' => 'Emission',
|
||||
'type' => 'list',
|
||||
'title' => 'Sélectionner l\'emission',
|
||||
'values' => [
|
||||
'Faux Raccord' => 'faux-raccord',
|
||||
'Fanzone' => 'fanzone',
|
||||
'Game In Ciné' => 'game-in-cine',
|
||||
'Pour la faire courte' => 'pour-la-faire-courte',
|
||||
'Home Cinéma' => 'home-cinema',
|
||||
'PILS - Par Ici Les Sorties' => 'pils-par-ici-les-sorties',
|
||||
'AlloCiné : l\'émission, sur LeStream' => 'allocine-lemission-sur-lestream',
|
||||
'Give Me Five' => 'give-me-five',
|
||||
'Aviez-vous remarqué ?' => 'aviez-vous-remarque',
|
||||
'Et paf, il est mort' => 'et-paf-il-est-mort',
|
||||
'The Big Fan Theory' => 'the-big-fan-theory',
|
||||
'Clichés' => 'cliches',
|
||||
'Complètement...' => 'completement',
|
||||
'#Fun Facts' => 'fun-facts',
|
||||
'Origin Story' => 'origin-story',
|
||||
]
|
||||
]
|
||||
]];
|
||||
const MAINTAINER = 'superbaillot.net';
|
||||
const NAME = 'Allo Cine Bridge';
|
||||
const CACHE_TIMEOUT = 25200; // 7h
|
||||
const URI = 'http://www.allocine.fr/';
|
||||
const DESCRIPTION = 'Bridge for allocine.fr';
|
||||
const PARAMETERS = array( array(
|
||||
'category' => array(
|
||||
'name' => 'category',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'exampleValue' => 'Faux Raccord',
|
||||
'title' => 'Select your category',
|
||||
'values' => array(
|
||||
'Faux Raccord' => 'faux-raccord',
|
||||
'Top 5' => 'top-5',
|
||||
'Tueurs en Séries' => 'tueurs-en-serie'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if (!is_null($this->getInput('category'))) {
|
||||
$categories = [
|
||||
'faux-raccord' => '/video/programme-12284/',
|
||||
'fanzone' => '/video/programme-12298/',
|
||||
'game-in-cine' => '/video/programme-12288/',
|
||||
'pour-la-faire-courte' => '/video/programme-20960/',
|
||||
'home-cinema' => '/video/programme-12287/',
|
||||
'pils-par-ici-les-sorties' => '/video/programme-25789/',
|
||||
'allocine-lemission-sur-lestream' => '/video/programme-25123/',
|
||||
'give-me-five' => '/video/programme-21919/saison-34518/',
|
||||
'aviez-vous-remarque' => '/video/programme-19518/',
|
||||
'et-paf-il-est-mort' => '/video/programme-25113/',
|
||||
'the-big-fan-theory' => '/video/programme-20403/',
|
||||
'cliches' => '/video/programme-24834/',
|
||||
'completement' => '/video/programme-23859/',
|
||||
'fun-facts' => '/video/programme-23040/',
|
||||
'origin-story' => '/video/programme-25667/'
|
||||
];
|
||||
public function getURI(){
|
||||
if(!is_null($this->getInput('category'))) {
|
||||
|
||||
$category = $this->getInput('category');
|
||||
if (array_key_exists($category, $categories)) {
|
||||
return static::URI . $this->getLastSeasonURI($categories[$category]);
|
||||
} else {
|
||||
returnClientError('Emission inconnue');
|
||||
}
|
||||
}
|
||||
switch($this->getInput('category')) {
|
||||
case 'faux-raccord':
|
||||
$uri = static::URI . 'video/programme-12284/saison-32180/';
|
||||
break;
|
||||
case 'top-5':
|
||||
$uri = static::URI . 'video/programme-12299/saison-29561/';
|
||||
break;
|
||||
case 'tueurs-en-serie':
|
||||
$uri = static::URI . 'video/programme-12286/saison-22938/';
|
||||
break;
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
private function getLastSeasonURI($category)
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached(static::URI . $category, 86400);
|
||||
$seasonLink = $html->find('section[class=section-wrap section]', 0)->find('div[class=cf]', 0)->find('a', 0);
|
||||
$URI = $seasonLink->href;
|
||||
return $URI;
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('category'))) {
|
||||
return self::NAME . ' : ' . $this->getKey('category');
|
||||
}
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('category'))) {
|
||||
return self::NAME . ' : '
|
||||
. array_search(
|
||||
$this->getInput('category'),
|
||||
self::PARAMETERS[$this->queriedContext]['category']['values']
|
||||
);
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
public function collectData(){
|
||||
|
||||
foreach ($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
|
||||
$item = [];
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('Could not request ' . $this->getURI() . ' !');
|
||||
|
||||
$title = $element->find('a[class*=meta-title-link]', 0);
|
||||
$content = trim(defaultLinkTo($element->outertext, static::URI));
|
||||
$category = array_search(
|
||||
$this->getInput('category'),
|
||||
self::PARAMETERS[$this->queriedContext]['category']['values']
|
||||
);
|
||||
|
||||
// Replace image 'src' with the one in 'data-src'
|
||||
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
|
||||
$content = preg_replace('@data-src=@', 'src=', $content);
|
||||
foreach($html->find('.media-meta-list figure.media-meta-fig') as $element) {
|
||||
$item = array();
|
||||
|
||||
// Remove date in the content to prevent content update while the video is getting older
|
||||
$content = preg_replace('@<div class="meta-sub light">.*<span>[^<]*</span>[^<]*</div>@', '', $content);
|
||||
$title = $element->find('div.titlebar h3.title a', 0);
|
||||
$content = trim($element->innertext);
|
||||
$figCaption = strpos($content, $category);
|
||||
|
||||
if($figCaption !== false) {
|
||||
$content = str_replace('src="/', 'src="' . static::URI, $content);
|
||||
$content = str_replace('href="/', 'href="' . static::URI, $content);
|
||||
$content = str_replace('src=\'/', 'src=\'' . static::URI, $content);
|
||||
$content = str_replace('href=\'/', 'href=\'' . static::URI, $content);
|
||||
$item['content'] = $content;
|
||||
$item['title'] = trim($title->innertext);
|
||||
$item['uri'] = static::URI . $title->href;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] = $content;
|
||||
$item['title'] = trim($title->innertext);
|
||||
$item['uri'] = static::URI . '/' . substr($title->href, 1);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AllocineFRSortiesBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'Simounet';
|
||||
const NAME = 'AlloCiné Sorties Bridge';
|
||||
const CACHE_TIMEOUT = 25200; // 7h
|
||||
const BASE_URI = 'https://www.allocine.fr';
|
||||
const URI = self::BASE_URI . '/film/sorties-semaine/';
|
||||
const DESCRIPTION = 'Bridge for AlloCiné - Sorties cinéma cette semaine';
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
foreach ($html->find('section.section.section-wrap', 0)->find('li.mdl') as $element) {
|
||||
$item = [];
|
||||
|
||||
$thumb = $element->find('figure.thumbnail', 0);
|
||||
$meta = $element->find('div.meta-body', 0);
|
||||
$synopsis = $element->find('div.synopsis', 0);
|
||||
$date = $element->find('span.date', 0);
|
||||
|
||||
$title = $element->find('a[class*=meta-title-link]', 0);
|
||||
$content = trim(defaultLinkTo($thumb->outertext . $meta->outertext . $synopsis->outertext, static::URI));
|
||||
|
||||
// Replace image 'src' with the one in 'data-src'
|
||||
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9=+\/]*"@', '', $content);
|
||||
$content = preg_replace('@data-src=@', 'src=', $content);
|
||||
|
||||
$item['content'] = $content;
|
||||
$item['title'] = trim($title->innertext);
|
||||
$item['timestamp'] = $this->frenchPubDateToTimestamp($date->plaintext);
|
||||
$item['uri'] = static::BASE_URI . '/' . substr($title->href, 1);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function frenchPubDateToTimestamp($date)
|
||||
{
|
||||
return strtotime(
|
||||
strtr(
|
||||
strtolower($date),
|
||||
[
|
||||
'janvier' => 'jan',
|
||||
'février' => 'feb',
|
||||
'mars' => 'march',
|
||||
'avril' => 'apr',
|
||||
'mai' => 'may',
|
||||
'juin' => 'jun',
|
||||
'juillet' => 'jul',
|
||||
'août' => 'aug',
|
||||
'septembre' => 'sep',
|
||||
'octobre' => 'oct',
|
||||
'novembre' => 'nov',
|
||||
'décembre' => 'dec'
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,105 +1,94 @@
|
||||
<?php
|
||||
|
||||
class AmazonBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'Alexis CHEMEL';
|
||||
const NAME = 'Amazon';
|
||||
const URI = 'https://www.amazon.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Returns products from Amazon search';
|
||||
class AmazonBridge extends BridgeAbstract {
|
||||
|
||||
const PARAMETERS = [[
|
||||
'q' => [
|
||||
'name' => 'Keyword',
|
||||
'required' => true,
|
||||
'exampleValue' => 'watch',
|
||||
],
|
||||
'sort' => [
|
||||
'name' => 'Sort by',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Relevance' => 'relevanceblender',
|
||||
'Price: Low to High' => 'price-asc-rank',
|
||||
'Price: High to Low' => 'price-desc-rank',
|
||||
'Average Customer Review' => 'review-rank',
|
||||
'Newest Arrivals' => 'date-desc-rank',
|
||||
],
|
||||
'defaultValue' => 'relevanceblender',
|
||||
],
|
||||
'tld' => [
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Australia' => 'com.au',
|
||||
'Brazil' => 'com.br',
|
||||
'Canada' => 'ca',
|
||||
'China' => 'cn',
|
||||
'France' => 'fr',
|
||||
'Germany' => 'de',
|
||||
'India' => 'in',
|
||||
'Italy' => 'it',
|
||||
'Japan' => 'co.jp',
|
||||
'Mexico' => 'com.mx',
|
||||
'Netherlands' => 'nl',
|
||||
'Poland' => 'pl',
|
||||
'Spain' => 'es',
|
||||
'Sweden' => 'se',
|
||||
'Turkey' => 'com.tr',
|
||||
'United Kingdom' => 'co.uk',
|
||||
'United States' => 'com',
|
||||
],
|
||||
'defaultValue' => 'com',
|
||||
],
|
||||
]];
|
||||
const MAINTAINER = 'Alexis CHEMEL';
|
||||
const NAME = 'Amazon';
|
||||
const URI = 'https://www.amazon.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Returns products from Amazon search';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld'));
|
||||
const PARAMETERS = array(array(
|
||||
'q' => array(
|
||||
'name' => 'Keyword',
|
||||
'required' => true,
|
||||
),
|
||||
'sort' => array(
|
||||
'name' => 'Sort by',
|
||||
'type' => 'list',
|
||||
'required' => false,
|
||||
'values' => array(
|
||||
'Relevance' => 'relevanceblender',
|
||||
'Price: Low to High' => 'price-asc-rank',
|
||||
'Price: High to Low' => 'price-desc-rank',
|
||||
'Average Customer Review' => 'review-rank',
|
||||
'Newest Arrivals' => 'date-desc-rank',
|
||||
),
|
||||
'defaultValue' => 'relevanceblender',
|
||||
),
|
||||
'tld' => array(
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'Australia' => 'com.au',
|
||||
'Brazil' => 'com.br',
|
||||
'Canada' => 'ca',
|
||||
'China' => 'cn',
|
||||
'France' => 'fr',
|
||||
'Germany' => 'de',
|
||||
'India' => 'in',
|
||||
'Italy' => 'it',
|
||||
'Japan' => 'co.jp',
|
||||
'Mexico' => 'com.mx',
|
||||
'Netherlands' => 'nl',
|
||||
'Spain' => 'es',
|
||||
'United Kingdom' => 'co.uk',
|
||||
'United States' => 'com',
|
||||
),
|
||||
'defaultValue' => 'com',
|
||||
),
|
||||
));
|
||||
|
||||
$url = sprintf(
|
||||
'%s/s/?field-keywords=%s&sort=%s',
|
||||
$baseUrl,
|
||||
urlencode($this->getInput('q')),
|
||||
$this->getInput('sort')
|
||||
);
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
|
||||
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
|
||||
}
|
||||
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
$elements = $dom->find('div.s-result-item');
|
||||
public function collectData() {
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$item = [];
|
||||
$uri = 'https://www.amazon.' . $this->getInput('tld') . '/';
|
||||
$uri .= 's/?field-keywords=' . urlencode($this->getInput('q')) . '&sort=' . $this->getInput('sort');
|
||||
|
||||
$title = $element->find('h2', 0);
|
||||
if (!$title) {
|
||||
continue;
|
||||
}
|
||||
$html = getSimpleHTMLDOM($uri)
|
||||
or returnServerError('Could not request Amazon.');
|
||||
|
||||
$item['title'] = $title->innertext;
|
||||
foreach($html->find('li.s-result-item') as $element) {
|
||||
|
||||
$itemUrl = $element->find('a', 0)->href;
|
||||
$item['uri'] = urljoin($baseUrl, $itemUrl);
|
||||
$item = array();
|
||||
|
||||
$image = $element->find('img', 0);
|
||||
if ($image) {
|
||||
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />';
|
||||
}
|
||||
// Title
|
||||
$title = $element->find('h2', 0);
|
||||
|
||||
$price = $element->find('span.a-price > .a-offscreen', 0);
|
||||
if ($price) {
|
||||
$item['content'] .= $price->innertext;
|
||||
}
|
||||
$item['title'] = html_entity_decode($title->innertext, ENT_QUOTES);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
// Url
|
||||
$uri = $title->parent()->getAttribute('href');
|
||||
$uri = substr($uri, 0, strrpos($uri, '/'));
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
|
||||
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
|
||||
}
|
||||
$item['uri'] = substr($uri, 0, strrpos($uri, '/'));
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
// Content
|
||||
$image = $element->find('img', 0);
|
||||
$price = $element->find('span.s-price', 0);
|
||||
$price = ($price) ? $price->innertext : '';
|
||||
|
||||
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />' . $price;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,253 +1,187 @@
|
||||
<?php
|
||||
|
||||
class AmazonPriceTrackerBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'captn3m0, sal0max';
|
||||
const NAME = 'Amazon Price Tracker';
|
||||
const URI = 'https://www.amazon.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Tracks price for a single product on Amazon';
|
||||
class AmazonPriceTrackerBridge extends BridgeAbstract {
|
||||
const MAINTAINER = 'captn3m0';
|
||||
const NAME = 'Amazon Price Tracker';
|
||||
const URI = 'https://www.amazon.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Tracks price for a single product on Amazon';
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'asin' => [
|
||||
'name' => 'ASIN',
|
||||
'required' => true,
|
||||
'exampleValue' => 'B071GB1VMQ',
|
||||
// https://stackoverflow.com/a/12827734
|
||||
'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
|
||||
],
|
||||
'tld' => [
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Australia' => 'com.au',
|
||||
'Brazil' => 'com.br',
|
||||
'Canada' => 'ca',
|
||||
'China' => 'cn',
|
||||
'France' => 'fr',
|
||||
'Germany' => 'de',
|
||||
'India' => 'in',
|
||||
'Italy' => 'it',
|
||||
'Japan' => 'co.jp',
|
||||
'Mexico' => 'com.mx',
|
||||
'Netherlands' => 'nl',
|
||||
'Poland' => 'pl',
|
||||
'Spain' => 'es',
|
||||
'Sweden' => 'se',
|
||||
'Turkey' => 'com.tr',
|
||||
'United Kingdom' => 'co.uk',
|
||||
'United States' => 'com',
|
||||
],
|
||||
'defaultValue' => 'com',
|
||||
],
|
||||
]];
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
'asin' => array(
|
||||
'name' => 'ASIN',
|
||||
'required' => true,
|
||||
'exampleValue' => 'B071GB1VMQ',
|
||||
// https://stackoverflow.com/a/12827734
|
||||
'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
|
||||
),
|
||||
'tld' => array(
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'values' => array(
|
||||
'Australia' => 'com.au',
|
||||
'Brazil' => 'com.br',
|
||||
'Canada' => 'ca',
|
||||
'China' => 'cn',
|
||||
'France' => 'fr',
|
||||
'Germany' => 'de',
|
||||
'India' => 'in',
|
||||
'Italy' => 'it',
|
||||
'Japan' => 'co.jp',
|
||||
'Mexico' => 'com.mx',
|
||||
'Netherlands' => 'nl',
|
||||
'Spain' => 'es',
|
||||
'United Kingdom' => 'co.uk',
|
||||
'United States' => 'com',
|
||||
),
|
||||
'defaultValue' => 'com',
|
||||
),
|
||||
));
|
||||
|
||||
const PRICE_SELECTORS = [
|
||||
'#priceblock_ourprice',
|
||||
'.priceBlockBuyingPriceString',
|
||||
'#newBuyBoxPrice',
|
||||
'#tp_price_block_total_price_ww',
|
||||
'span.offer-price',
|
||||
'.a-color-price',
|
||||
];
|
||||
protected $title;
|
||||
|
||||
const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0";
|
||||
/**
|
||||
* Generates domain name given a amazon TLD
|
||||
*/
|
||||
private function getDomainName() {
|
||||
return 'https://www.amazon.' . $this->getInput('tld');
|
||||
}
|
||||
|
||||
protected $title;
|
||||
/**
|
||||
* Generates URI for a Amazon product page
|
||||
*/
|
||||
public function getURI() {
|
||||
if (!is_null($this->getInput('asin'))) {
|
||||
return $this->getDomainName() . '/dp/' . $this->getInput('asin') . '/';
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates domain name given a amazon TLD
|
||||
*/
|
||||
private function getDomainName()
|
||||
{
|
||||
return 'https://www.amazon.' . $this->getInput('tld');
|
||||
}
|
||||
/**
|
||||
* Scrapes the product title from the html page
|
||||
* returns the default title if scraping fails
|
||||
*/
|
||||
private function getTitle($html) {
|
||||
$titleTag = $html->find('#productTitle', 0);
|
||||
|
||||
/**
|
||||
* Generates URI for a Amazon product page
|
||||
*/
|
||||
public function getURI()
|
||||
{
|
||||
if (!is_null($this->getInput('asin'))) {
|
||||
return $this->getDomainName() . '/dp/' . $this->getInput('asin');
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
if (!$titleTag) {
|
||||
return $this->getDefaultTitle();
|
||||
} else {
|
||||
return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrapes the product title from the html page
|
||||
* returns the default title if scraping fails
|
||||
*/
|
||||
private function getTitle($html)
|
||||
{
|
||||
$titleTag = $html->find('#productTitle', 0);
|
||||
/**
|
||||
* Title used by the feed if none could be found
|
||||
*/
|
||||
private function getDefaultTitle() {
|
||||
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
|
||||
}
|
||||
|
||||
if (!$titleTag) {
|
||||
return $this->getDefaultTitle();
|
||||
} else {
|
||||
return trim(html_entity_decode($titleTag->innertext, ENT_QUOTES));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns name for the feed
|
||||
* Uses title (already scraped) if it has one
|
||||
*/
|
||||
public function getName() {
|
||||
if (isset($this->title)) {
|
||||
return $this->title;
|
||||
} else {
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Title used by the feed if none could be found
|
||||
*/
|
||||
private function getDefaultTitle()
|
||||
{
|
||||
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
|
||||
}
|
||||
private function parseDynamicImage($attribute) {
|
||||
$json = json_decode(html_entity_decode($attribute), true);
|
||||
|
||||
/**
|
||||
* Returns name for the feed
|
||||
* Uses title (already scraped) if it has one
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
if (isset($this->title)) {
|
||||
return $this->title;
|
||||
} else {
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
if ($json and count($json) > 0) {
|
||||
return array_keys($json)[0];
|
||||
}
|
||||
}
|
||||
|
||||
private function parseDynamicImage($attribute)
|
||||
{
|
||||
$json = json_decode(html_entity_decode($attribute), true);
|
||||
/**
|
||||
* Returns a generated image tag for the product
|
||||
*/
|
||||
private function getImage($html) {
|
||||
$imageSrc = $html->find('#main-image-container img', 0);
|
||||
|
||||
if ($json and count($json) > 0) {
|
||||
return array_keys($json)[0];
|
||||
}
|
||||
}
|
||||
if ($imageSrc) {
|
||||
$hiresImage = $imageSrc->getAttribute('data-old-hires');
|
||||
$dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image');
|
||||
$image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute);
|
||||
}
|
||||
$image = $image ?: 'https://placekitten.com/200/300';
|
||||
|
||||
/**
|
||||
* Returns a generated image tag for the product
|
||||
*/
|
||||
private function getImage($html)
|
||||
{
|
||||
$image = 'https://placekitten.com/200/300';
|
||||
$imageSrc = $html->find('#main-image-container img', 0);
|
||||
if ($imageSrc) {
|
||||
$hiresImage = $imageSrc->getAttribute('data-old-hires');
|
||||
$dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image');
|
||||
$image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute);
|
||||
}
|
||||
|
||||
return <<<EOT
|
||||
return <<<EOT
|
||||
<img width="300" style="max-width:300;max-height:300" src="$image" alt="{$this->title}" />
|
||||
EOT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return \simple_html_dom object
|
||||
* for the entire html of the product page
|
||||
*/
|
||||
private function getHtml()
|
||||
{
|
||||
$uri = $this->getURI();
|
||||
/**
|
||||
* Return \simple_html_dom object
|
||||
* for the entire html of the product page
|
||||
*/
|
||||
private function getHtml() {
|
||||
$uri = $this->getURI();
|
||||
|
||||
return getSimpleHTMLDOM($uri);
|
||||
}
|
||||
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
|
||||
}
|
||||
|
||||
private function scrapePriceFromMetrics($html)
|
||||
{
|
||||
$asinData = $html->find('#cerberus-data-metrics', 0);
|
||||
private function scrapePriceFromMetrics($html) {
|
||||
$asinData = $html->find('#cerberus-data-metrics', 0);
|
||||
|
||||
// <div id="cerberus-data-metrics" style="display: none;"
|
||||
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
|
||||
// data-asin-currency-code="USD" data-substitute-count="-1" ... />
|
||||
if ($asinData) {
|
||||
return [
|
||||
'price' => $asinData->getAttribute('data-asin-price'),
|
||||
'currency' => $asinData->getAttribute('data-asin-currency-code'),
|
||||
'shipping' => $asinData->getAttribute('data-asin-shipping')
|
||||
];
|
||||
}
|
||||
// <div id="cerberus-data-metrics" style="display: none;"
|
||||
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
|
||||
// data-asin-currency-code="USD" data-substitute-count="-1" ... />
|
||||
if ($asinData) {
|
||||
return [
|
||||
'price' => $asinData->getAttribute('data-asin-price'),
|
||||
'currency' => $asinData->getAttribute('data-asin-currency-code'),
|
||||
'shipping' => $asinData->getAttribute('data-asin-shipping')
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function scrapePriceTwister($html)
|
||||
{
|
||||
$str = $html->find('.twister-plus-buying-options-price-data', 0);
|
||||
private function scrapePriceGeneric($html) {
|
||||
$priceDiv = $html->find('span.offer-price', 0) ?: $html->find('.a-color-price', 0);
|
||||
|
||||
$data = json_decode($str->innertext, true);
|
||||
if (count($data) === 1) {
|
||||
$data = $data[0];
|
||||
return [
|
||||
'displayPrice' => $data['displayPrice'],
|
||||
'currency' => $data['currency'],
|
||||
'shipping' => '0',
|
||||
];
|
||||
}
|
||||
preg_match('/^\s*([A-Z]{3}|£|\$)\s?([\d.,]+)\s*$/', $priceDiv->plaintext, $matches);
|
||||
|
||||
return false;
|
||||
}
|
||||
if (count($matches) === 3) {
|
||||
return [
|
||||
'price' => $matches[2],
|
||||
'currency' => $matches[1],
|
||||
'shipping' => '0'
|
||||
];
|
||||
}
|
||||
|
||||
private function scrapePriceGeneric($html)
|
||||
{
|
||||
$default = [
|
||||
'price' => null,
|
||||
'displayPrice' => null,
|
||||
'currency' => null,
|
||||
'shipping' => null,
|
||||
];
|
||||
$priceDiv = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (self::PRICE_SELECTORS as $sel) {
|
||||
$priceDiv = $html->find($sel, 0);
|
||||
if ($priceDiv) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Scrape method for Amazon product page
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function collectData() {
|
||||
$html = $this->getHtml();
|
||||
$this->title = $this->getTitle($html);
|
||||
$imageTag = $this->getImage($html);
|
||||
|
||||
if (!$priceDiv) {
|
||||
return $default;
|
||||
}
|
||||
$data = $this->scrapePriceFromMetrics($html) ?: $this->scrapePriceGeneric($html);
|
||||
|
||||
$priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext);
|
||||
preg_match('/(\d+\.\d{0,2})/', $priceString, $matches);
|
||||
$item = array(
|
||||
'title' => $this->title,
|
||||
'uri' => $this->getURI(),
|
||||
'content' => "$imageTag<br/>Price: {$data['price']} {$data['currency']}",
|
||||
);
|
||||
|
||||
$price = $matches[0] ?? null;
|
||||
$currency = str_replace($price, '', $priceString);
|
||||
if ($data['shipping'] !== '0') {
|
||||
$item['content'] .= "<br>Shipping: {$data['shipping']} {$data['currency']}</br>";
|
||||
}
|
||||
|
||||
if ($price != null && $currency != null) {
|
||||
return [
|
||||
'price' => $price,
|
||||
'displayPrice' => null,
|
||||
'currency' => $currency,
|
||||
'shipping' => '0'
|
||||
];
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = $this->getHtml();
|
||||
$this->title = $this->getTitle($html);
|
||||
$image = $this->getImage($html);
|
||||
$data = $this->scrapePriceGeneric($html);
|
||||
|
||||
// render
|
||||
$content = '';
|
||||
$price = $data['displayPrice'];
|
||||
if (!$price) {
|
||||
$price = sprintf('%s %s', $data['price'], $data['currency']);
|
||||
}
|
||||
$content .= sprintf('%s<br>Price: %s', $image, $price);
|
||||
if ($data['shipping'] !== '0') {
|
||||
$content .= sprintf('<br>Shipping: %s %s</br>', $data['shipping'], $data['currency']);
|
||||
}
|
||||
|
||||
$item = [
|
||||
'title' => $this->title,
|
||||
'uri' => $this->getURI(),
|
||||
'content' => $content,
|
||||
// This is to ensure that feed readers notice the price change
|
||||
'uid' => md5($data['price'])
|
||||
];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
@ -1,278 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AnfrBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ANFR';
|
||||
const URI = 'https://data.anfr.fr/';
|
||||
const DESCRIPTION = 'Fetches data from the French administration "Agence Nationale des Fréquences".';
|
||||
const CACHE_TIMEOUT = 604800; // 7d
|
||||
const MAINTAINER = 'quent1';
|
||||
const PARAMETERS = [
|
||||
'Données sur les réseaux mobiles' => [
|
||||
'departement' => [
|
||||
'name' => 'Département',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tous' => null,
|
||||
'Ain' => '001',
|
||||
'Aisne' => '002',
|
||||
'Allier' => '003',
|
||||
'Alpes-de-Haute-Provence' => '004',
|
||||
'Hautes-Alpes' => '005',
|
||||
'Alpes-Maritimes' => '006',
|
||||
'Ardèche' => '007',
|
||||
'Ardennes' => '008',
|
||||
'Ariège' => '009',
|
||||
'Aube' => '010',
|
||||
'Aude' => '011',
|
||||
'Aveyron' => '012',
|
||||
'Bouches-du-Rhône' => '013',
|
||||
'Calvados' => '014',
|
||||
'Cantal' => '015',
|
||||
'Charente' => '016',
|
||||
'Charente-Maritime' => '017',
|
||||
'Cher' => '018',
|
||||
'Corrèze' => '019',
|
||||
'Corse-du-Sud' => '02A',
|
||||
'Haute-Corse' => '02B',
|
||||
'Côte-d\'Or' => '021',
|
||||
'Côtes-d\'Armor' => '022',
|
||||
'Creuse' => '023',
|
||||
'Dordogne' => '024',
|
||||
'Doubs' => '025',
|
||||
'Drôme' => '026',
|
||||
'Eure' => '027',
|
||||
'Eure-et-Loir' => '028',
|
||||
'Finistère' => '029',
|
||||
'Gard' => '030',
|
||||
'Haute-Garonne' => '031',
|
||||
'Gers' => '032',
|
||||
'Gironde' => '033',
|
||||
'Hérault' => '034',
|
||||
'Ille-et-Vilaine' => '035',
|
||||
'Indre' => '036',
|
||||
'Indre-et-Loire' => '037',
|
||||
'Isère' => '038',
|
||||
'Jura' => '039',
|
||||
'Landes' => '040',
|
||||
'Loir-et-Cher' => '041',
|
||||
'Loire' => '042',
|
||||
'Haute-Loire' => '043',
|
||||
'Loire-Atlantique' => '044',
|
||||
'Loiret' => '045',
|
||||
'Lot' => '046',
|
||||
'Lot-et-Garonne' => '047',
|
||||
'Lozère' => '048',
|
||||
'Maine-et-Loire' => '049',
|
||||
'Manche' => '050',
|
||||
'Marne' => '051',
|
||||
'Haute-Marne' => '052',
|
||||
'Mayenne' => '053',
|
||||
'Meurthe-et-Moselle' => '054',
|
||||
'Meuse' => '055',
|
||||
'Morbihan' => '056',
|
||||
'Moselle' => '057',
|
||||
'Nièvre' => '058',
|
||||
'Nord' => '059',
|
||||
'Oise' => '060',
|
||||
'Orne' => '061',
|
||||
'Pas-de-Calais' => '062',
|
||||
'Puy-de-Dôme' => '063',
|
||||
'Pyrénées-Atlantiques' => '064',
|
||||
'Hautes-Pyrénées' => '065',
|
||||
'Pyrénées-Orientales' => '066',
|
||||
'Bas-Rhin' => '067',
|
||||
'Haut-Rhin' => '068',
|
||||
'Rhône' => '069',
|
||||
'Haute-Saône' => '070',
|
||||
'Saône-et-Loire' => '071',
|
||||
'Sarthe' => '072',
|
||||
'Savoie' => '073',
|
||||
'Haute-Savoie' => '074',
|
||||
'Paris' => '075',
|
||||
'Seine-Maritime' => '076',
|
||||
'Seine-et-Marne' => '077',
|
||||
'Yvelines' => '078',
|
||||
'Deux-Sèvres' => '079',
|
||||
'Somme' => '080',
|
||||
'Tarn' => '081',
|
||||
'Tarn-et-Garonne' => '082',
|
||||
'Var' => '083',
|
||||
'Vaucluse' => '084',
|
||||
'Vendée' => '085',
|
||||
'Vienne' => '086',
|
||||
'Haute-Vienne' => '087',
|
||||
'Vosges' => '088',
|
||||
'Yonne' => '089',
|
||||
'Territoire de Belfort' => '090',
|
||||
'Essonne' => '091',
|
||||
'Hauts-de-Seine' => '092',
|
||||
'Seine-Saint-Denis' => '093',
|
||||
'Val-de-Marne' => '094',
|
||||
'Val-d\'Oise' => '095',
|
||||
'Guadeloupe' => '971',
|
||||
'Martinique' => '972',
|
||||
'Guyane' => '973',
|
||||
'La Réunion' => '974',
|
||||
'Saint-Pierre-et-Miquelon' => '975',
|
||||
'Mayotte' => '976',
|
||||
'Saint-Barthélemy' => '977',
|
||||
'Saint-Martin' => '978',
|
||||
'Terres australes et antarctiques françaises' => '984',
|
||||
'Wallis-et-Futuna' => '986',
|
||||
'Polynésie française' => '987',
|
||||
'Nouvelle-Calédonie' => '988',
|
||||
'Île de Clipperton' => '989'
|
||||
]
|
||||
],
|
||||
'generation' => [
|
||||
'name' => 'Génération',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tous' => null,
|
||||
'2G' => '2G',
|
||||
'3G' => '3G',
|
||||
'4G' => '4G',
|
||||
'5G' => '5G',
|
||||
]
|
||||
],
|
||||
'operateur' => [
|
||||
'name' => 'Opérateur',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tous' => null,
|
||||
'Bouygues Télécom' => 'BOUYGUES TELECOM',
|
||||
'Dauphin Télécom' => 'DAUPHIN TELECOM',
|
||||
'Digiciel' => 'DIGICEL',
|
||||
'Free Caraïbes' => 'FREE CARAIBES',
|
||||
'Free Mobile' => 'FREE MOBILE',
|
||||
'GLOBALTEL' => 'GLOBALTEL',
|
||||
'Office des postes et télécommunications de Nouvelle Calédonie' => 'Gouv Nelle Calédonie (OPT)',
|
||||
'Maore Mobile' => 'MAORE MOBILE',
|
||||
'ONATi' => 'ONATI',
|
||||
'Orange' => 'ORANGE',
|
||||
'Outremer Telecom' => 'OUTREMER TELECOM',
|
||||
'Vodafone polynésie' => 'PMT/VODAPHONE',
|
||||
'SFR' => 'SFR',
|
||||
'SPM Télécom' => 'SPM TELECOM',
|
||||
'Service des Postes et Télécommunications de Polynésie Française' => 'Gouv Nelle Calédonie (OPT)',
|
||||
'SRR' => 'SRR',
|
||||
'Station étrangère' => 'Station étrangère',
|
||||
'Telco OI' => 'TELCO IO',
|
||||
'United Telecommunication Services Caraïbes' => 'UTS Caraibes',
|
||||
'Ora Mobile' => 'VITI SAS',
|
||||
'Zeop' => 'ZEOP'
|
||||
]
|
||||
],
|
||||
'statut' => [
|
||||
'name' => 'Statut',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tous' => null,
|
||||
'En service' => 'En service',
|
||||
'Projet approuvé' => 'Projet approuvé',
|
||||
'Techniquement opérationnel' => 'Techniquement opérationnel',
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$urlParts = [
|
||||
'id' => 'observatoire_2g_3g_4g',
|
||||
'resource_id' => '88ef0887-6b0f-4d3f-8545-6d64c8f597da',
|
||||
'fields' => 'id,adm_lb_nom,sta_nm_dpt,emr_lb_systeme,generation,date_maj,sta_nm_anfr,adr_lb_lieu,adr_lb_add1,adr_lb_add2,adr_lb_add3,adr_nm_cp,statut',
|
||||
'rows' => 10000
|
||||
];
|
||||
|
||||
if (!empty($this->getInput('departement'))) {
|
||||
$urlParts['refine.sta_nm_dpt'] = urlencode($this->getInput('departement'));
|
||||
}
|
||||
|
||||
if (!empty($this->getInput('generation'))) {
|
||||
$urlParts['refine.generation'] = $this->getInput('generation');
|
||||
}
|
||||
|
||||
if (!empty($this->getInput('operateur'))) {
|
||||
// http_build_query() already does urlencoding so this call is redundant
|
||||
$urlParts['refine.adm_lb_nom'] = urlencode($this->getInput('operateur'));
|
||||
}
|
||||
|
||||
if (!empty($this->getInput('statut'))) {
|
||||
$urlParts['refine.statut'] = urlencode($this->getInput('statut'));
|
||||
}
|
||||
|
||||
// API seems to not play well with urlencoded data
|
||||
$url = urljoin(static::URI, '/d4c/api/records/1.0/download/?' . urldecode(http_build_query($urlParts)));
|
||||
|
||||
$json = getContents($url);
|
||||
$data = Json::decode($json, false);
|
||||
$records = $data->records;
|
||||
$frequenciesByStation = [];
|
||||
foreach ($records as $record) {
|
||||
if (!isset($frequenciesByStation[$record->fields->sta_nm_anfr])) {
|
||||
$street = sprintf(
|
||||
'%s %s %s',
|
||||
$record->fields->adr_lb_add1 ?? '',
|
||||
$record->fields->adr_lb_add2 ?? '',
|
||||
$record->fields->adr_lb_add3 ?? ''
|
||||
);
|
||||
$frequenciesByStation[$record->fields->sta_nm_anfr] = [
|
||||
'id' => $record->fields->sta_nm_anfr,
|
||||
'operator' => $record->fields->adm_lb_nom,
|
||||
'frequencies' => [],
|
||||
'lastUpdate' => 0,
|
||||
'address' => [
|
||||
'street' => trim($street),
|
||||
'postCode' => $record->fields->adr_nm_cp,
|
||||
'city' => $record->fields->adr_lb_lieu
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$frequenciesByStation[$record->fields->sta_nm_anfr]['frequencies'][] = [
|
||||
'generation' => $record->fields->generation,
|
||||
'frequency' => $record->fields->emr_lb_systeme,
|
||||
'status' => $record->fields->statut,
|
||||
'updatedAt' => strtotime($record->fields->date_maj),
|
||||
];
|
||||
|
||||
$frequenciesByStation[$record->fields->sta_nm_anfr]['lastUpdate'] = max(
|
||||
$frequenciesByStation[$record->fields->sta_nm_anfr]['lastUpdate'],
|
||||
strtotime($record->fields->date_maj)
|
||||
);
|
||||
}
|
||||
|
||||
usort($frequenciesByStation, static fn ($a, $b) => $b['lastUpdate'] <=> $a['lastUpdate']);
|
||||
|
||||
foreach ($frequenciesByStation as $station) {
|
||||
$title = sprintf(
|
||||
'[%s] Mise à jour de la station n°%s à %s (%s)',
|
||||
$station['operator'],
|
||||
$station['id'],
|
||||
$station['address']['city'],
|
||||
$station['address']['postCode']
|
||||
);
|
||||
|
||||
$array_reduce = array_reduce($station['frequencies'], static function ($carry, $frequency) {
|
||||
return sprintf('%s<li>%s : %s</li>', $carry, $frequency['frequency'], $frequency['status']);
|
||||
}, '');
|
||||
|
||||
$content = sprintf(
|
||||
'<h1>Adresse complète</h1><p>%s<br>%s<br>%s</p><h1>Fréquences</h1><p><ul>%s</ul></p>',
|
||||
$station['address']['street'],
|
||||
$station['address']['postCode'],
|
||||
$station['address']['city'],
|
||||
$array_reduce
|
||||
);
|
||||
|
||||
$this->items[] = [
|
||||
'uid' => $station['id'],
|
||||
'timestamp' => $station['lastUpdate'],
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,218 +1,207 @@
|
||||
<?php
|
||||
class AnidexBridge extends BridgeAbstract {
|
||||
|
||||
class AnidexBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = 'Anidex';
|
||||
const URI = 'http://anidex.info/'; // anidex.info has ddos-guard so we need to use anidex.moe
|
||||
const ALTERNATE_URI = 'https://anidex.moe/'; // anidex.moe returns 301 unless Host is set to anidex.info
|
||||
const ALTERNATE_HOST = 'anidex.info'; // Correct host for requesting anidex.moe without 301 redirect
|
||||
const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'id' => [
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All categories' => '0',
|
||||
'Anime' => '1,2,3',
|
||||
'Anime - Sub' => '1',
|
||||
'Anime - Raw' => '2',
|
||||
'Anime - Dub' => '3',
|
||||
'Live Action' => '4,5',
|
||||
'Live Action - Sub' => '4',
|
||||
'Live Action - Raw' => '5',
|
||||
'Light Novel' => '6',
|
||||
'Manga' => '7,8',
|
||||
'Manga - Translated' => '7',
|
||||
'Manga - Raw' => '8',
|
||||
'Music' => '9,10,11',
|
||||
'Music - Lossy' => '9',
|
||||
'Music - Lossless' => '10',
|
||||
'Music - Video' => '11',
|
||||
'Games' => '12',
|
||||
'Applications' => '13',
|
||||
'Pictures' => '14',
|
||||
'Adult Video' => '15',
|
||||
'Other' => '16'
|
||||
]
|
||||
],
|
||||
'lang_id' => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All languages' => '0',
|
||||
'English' => '1',
|
||||
'Japanese' => '2',
|
||||
'Polish' => '3',
|
||||
'Serbo-Croatian' => '4',
|
||||
'Dutch' => '5',
|
||||
'Italian' => '6',
|
||||
'Russian' => '7',
|
||||
'German' => '8',
|
||||
'Hungarian' => '9',
|
||||
'French' => '10',
|
||||
'Finnish' => '11',
|
||||
'Vietnamese' => '12',
|
||||
'Greek' => '13',
|
||||
'Bulgarian' => '14',
|
||||
'Spanish (Spain)' => '15',
|
||||
'Portuguese (Brazil)' => '16',
|
||||
'Portuguese (Portugal)' => '17',
|
||||
'Swedish' => '18',
|
||||
'Arabic' => '19',
|
||||
'Danish' => '20',
|
||||
'Chinese (Simplified)' => '21',
|
||||
'Bengali' => '22',
|
||||
'Romanian' => '23',
|
||||
'Czech' => '24',
|
||||
'Mongolian' => '25',
|
||||
'Turkish' => '26',
|
||||
'Indonesian' => '27',
|
||||
'Korean' => '28',
|
||||
'Spanish (LATAM)' => '29',
|
||||
'Persian' => '30',
|
||||
'Malaysian' => '31'
|
||||
]
|
||||
],
|
||||
'group_id' => [
|
||||
'name' => 'Group ID',
|
||||
'type' => 'number'
|
||||
],
|
||||
'r' => [
|
||||
'name' => 'Hide Remakes',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'b' => [
|
||||
'name' => 'Only Batches',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'a' => [
|
||||
'name' => 'Only Authorized',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'q' => [
|
||||
'name' => 'Keyword',
|
||||
'description' => 'Keyword(s)',
|
||||
'type' => 'text'
|
||||
],
|
||||
'h' => [
|
||||
'name' => 'Adult content',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'No filter' => '0',
|
||||
'Hide +18' => '1',
|
||||
'Only +18' => '2'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = 'Anidex';
|
||||
const URI = 'https://anidex.info/';
|
||||
const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
'id' => array(
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'All categories' => '0',
|
||||
'Anime' => '1,2,3',
|
||||
'Anime - Sub' => '1',
|
||||
'Anime - Raw' => '2',
|
||||
'Anime - Dub' => '3',
|
||||
'Live Action' => '4,5',
|
||||
'Live Action - Sub' => '4',
|
||||
'Live Action - Raw' => '5',
|
||||
'Light Novel' => '6',
|
||||
'Manga' => '7,8',
|
||||
'Manga - Translated' => '7',
|
||||
'Manga - Raw' => '8',
|
||||
'Music' => '9,10,11',
|
||||
'Music - Lossy' => '9',
|
||||
'Music - Lossless' => '10',
|
||||
'Music - Video' => '11',
|
||||
'Games' => '12',
|
||||
'Applications' => '13',
|
||||
'Pictures' => '14',
|
||||
'Adult Video' => '15',
|
||||
'Other' => '16'
|
||||
)
|
||||
),
|
||||
'lang_id' => array(
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'All languages' => '0',
|
||||
'English' => '1',
|
||||
'Japanese' => '2',
|
||||
'Polish' => '3',
|
||||
'Serbo-Croatian' => '4',
|
||||
'Dutch' => '5',
|
||||
'Italian' => '6',
|
||||
'Russian' => '7',
|
||||
'German' => '8',
|
||||
'Hungarian' => '9',
|
||||
'French' => '10',
|
||||
'Finnish' => '11',
|
||||
'Vietnamese' => '12',
|
||||
'Greek' => '13',
|
||||
'Bulgarian' => '14',
|
||||
'Spanish (Spain)' => '15',
|
||||
'Portuguese (Brazil)' => '16',
|
||||
'Portuguese (Portugal)' => '17',
|
||||
'Swedish' => '18',
|
||||
'Arabic' => '19',
|
||||
'Danish' => '20',
|
||||
'Chinese (Simplified)' => '21',
|
||||
'Bengali' => '22',
|
||||
'Romanian' => '23',
|
||||
'Czech' => '24',
|
||||
'Mongolian' => '25',
|
||||
'Turkish' => '26',
|
||||
'Indonesian' => '27',
|
||||
'Korean' => '28',
|
||||
'Spanish (LATAM)' => '29',
|
||||
'Persian' => '30',
|
||||
'Malaysian' => '31'
|
||||
)
|
||||
),
|
||||
'group_id' => array(
|
||||
'name' => 'Group ID',
|
||||
'type' => 'number'
|
||||
),
|
||||
'r' => array(
|
||||
'name' => 'Hide Remakes',
|
||||
'type' => 'checkbox'
|
||||
),
|
||||
'b' => array(
|
||||
'name' => 'Only Batches',
|
||||
'type' => 'checkbox'
|
||||
),
|
||||
'a' => array(
|
||||
'name' => 'Only Authorized',
|
||||
'type' => 'checkbox'
|
||||
),
|
||||
'q' => array(
|
||||
'name' => 'Keyword',
|
||||
'description' => 'Keyword(s)',
|
||||
'type' => 'text'
|
||||
),
|
||||
'h' => array(
|
||||
'name' => 'Adult content',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'No filter' => '0',
|
||||
'Hide +18' => '1',
|
||||
'Only +18' => '2'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
// Build Search URL from user-provided parameters
|
||||
$search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc';
|
||||
foreach (['id', 'lang_id', 'group_id'] as $param_name) {
|
||||
$param = $this->getInput($param_name);
|
||||
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) {
|
||||
$search_url .= '&' . $param_name . '=' . $param;
|
||||
}
|
||||
}
|
||||
foreach (['r', 'b', 'a'] as $param_name) {
|
||||
$param = $this->getInput($param_name);
|
||||
if (!empty($param) && boolval($param)) {
|
||||
$search_url .= '&' . $param_name . '=1';
|
||||
}
|
||||
}
|
||||
$query = $this->getInput('q');
|
||||
if (!empty($query)) {
|
||||
$search_url .= '&q=' . urlencode($query);
|
||||
}
|
||||
$opt = [];
|
||||
$h = $this->getInput('h');
|
||||
if (!empty($h) && intval($h) != 0 && ctype_digit($h)) {
|
||||
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
|
||||
}
|
||||
public function collectData() {
|
||||
|
||||
// We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI
|
||||
$headers = ['Host: ' . self::ALTERNATE_HOST];
|
||||
// Build Search URL from user-provided parameters
|
||||
$search_url = self::URI . '?s=upload_timestamp&o=desc';
|
||||
foreach (array('id', 'lang_id', 'group_id') as $param_name) {
|
||||
$param = $this->getInput($param_name);
|
||||
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) {
|
||||
$search_url .= '&' . $param_name . '=' . $param;
|
||||
}
|
||||
}
|
||||
foreach (array('r', 'b', 'a') as $param_name) {
|
||||
$param = $this->getInput($param_name);
|
||||
if (!empty($param) && boolval($param)) {
|
||||
$search_url .= '&' . $param_name . '=1';
|
||||
}
|
||||
}
|
||||
$query = $this->getInput('q');
|
||||
if (!empty($query)) {
|
||||
$search_url .= '&q=' . urlencode($query);
|
||||
}
|
||||
$opt = array();
|
||||
$h = $this->getInput('h');
|
||||
if (!empty($h) && intval($h) != 0 && ctype_digit($h)) {
|
||||
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
|
||||
}
|
||||
|
||||
// The HTTPS certificate presented by anidex.moe is for anidex.info. We need to ignore this.
|
||||
// As a consequence, the bridge is intentionally marked as insecure by setting self::URI to http://
|
||||
$opt[CURLOPT_SSL_VERIFYHOST] = 0;
|
||||
$opt[CURLOPT_SSL_VERIFYPEER] = 0;
|
||||
// Retrieve torrent listing from search results, which does not contain torrent description
|
||||
$html = getSimpleHTMLDOM($search_url, array(), $opt)
|
||||
or returnServerError('Could not request Anidex: ' . $search_url);
|
||||
$links = $html->find('a');
|
||||
$results = array();
|
||||
foreach ($links as $link)
|
||||
if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results))
|
||||
$results[] = $link->href;
|
||||
if (empty($results) && empty($this->getInput('q')))
|
||||
returnServerError('No results from Anidex: ' . $search_url);
|
||||
|
||||
// Retrieve torrent listing from search results, which does not contain torrent description
|
||||
$html = getSimpleHTMLDOM($search_url, $headers, $opt);
|
||||
$links = $html->find('a');
|
||||
$results = [];
|
||||
foreach ($links as $link) {
|
||||
if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results)) {
|
||||
$results[] = $link->href;
|
||||
}
|
||||
}
|
||||
if (empty($results) && empty($this->getInput('q'))) {
|
||||
returnServerError('No results from Anidex: ' . $search_url);
|
||||
}
|
||||
//Process each item individually
|
||||
foreach ($results as $element) {
|
||||
|
||||
//Process each item individually
|
||||
foreach ($results as $element) {
|
||||
//Limit total amount of requests
|
||||
if (count($this->items) >= 20) {
|
||||
break;
|
||||
}
|
||||
//Limit total amount of requests
|
||||
if(count($this->items) >= 20) {
|
||||
break;
|
||||
}
|
||||
|
||||
$torrent_id = str_replace('/torrent/', '', $element);
|
||||
$torrent_id = str_replace('/torrent/', '', $element);
|
||||
|
||||
//Ignore entries without valid torrent ID
|
||||
if ($torrent_id != 0 && ctype_digit($torrent_id)) {
|
||||
//Retrieve data for this torrent ID
|
||||
$item_browse_uri = self::URI . 'torrent/' . $torrent_id;
|
||||
$item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id;
|
||||
//Ignore entries without valid torrent ID
|
||||
if ($torrent_id != 0 && ctype_digit($torrent_id)) {
|
||||
|
||||
//Retrieve full description from torrent page (cached for 24 hours: 86400 seconds)
|
||||
if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) {
|
||||
//Retrieve data from page contents
|
||||
$item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext);
|
||||
$item_desc = $item_html->find('div.panel-body', 0);
|
||||
$item_author = trim($item_html->find('span.fa-user', 0)->parent()->plaintext);
|
||||
$item_date = strtotime(trim($item_html->find('span.fa-clock', 0)->parent()->plaintext));
|
||||
$item_image = $this->getURI() . 'images/user_logos/default.png';
|
||||
//Retrieve data for this torrent ID
|
||||
$item_uri = self::URI . 'torrent/' . $torrent_id;
|
||||
|
||||
//Check for description-less torrent andn optionally extract image
|
||||
$desc_title_found = false;
|
||||
foreach ($item_html->find('h3.panel-title') as $h3) {
|
||||
if (strpos($h3, 'Description') !== false) {
|
||||
$desc_title_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($desc_title_found) {
|
||||
//Retrieve image for thumbnail or generic logo fallback
|
||||
foreach ($item_desc->find('img') as $img) {
|
||||
if (strpos($img->src, 'prez') === false) {
|
||||
$item_image = $img->src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$item_desc = trim($item_desc->innertext);
|
||||
} else {
|
||||
$item_desc = '<em>No description.</em>';
|
||||
}
|
||||
//Retrieve full description from torrent page
|
||||
if ($item_html = getSimpleHTMLDOMCached($item_uri)) {
|
||||
|
||||
//Build and add final item
|
||||
$item = [];
|
||||
$item['uri'] = $item_browse_uri;
|
||||
$item['title'] = $item_title;
|
||||
$item['author'] = $item_author;
|
||||
$item['timestamp'] = $item_date;
|
||||
$item['enclosures'] = [$item_image];
|
||||
$item['content'] = $item_desc;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
$element = null;
|
||||
}
|
||||
$results = null;
|
||||
}
|
||||
//Retrieve data from page contents
|
||||
$item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext);
|
||||
$item_desc = $item_html->find('div.panel-body', 0);
|
||||
$item_author = trim($item_html->find('span.fa-user', 0)->parent()->plaintext);
|
||||
$item_date = strtotime(trim($item_html->find('span.fa-clock', 0)->parent()->plaintext));
|
||||
$item_image = $this->getURI() . 'images/user_logos/default.png';
|
||||
|
||||
//Check for description-less torrent andn optionally extract image
|
||||
$desc_title_found = false;
|
||||
foreach ($item_html->find('h3.panel-title') as $h3) {
|
||||
if (strpos($h3, 'Description') !== false) {
|
||||
$desc_title_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($desc_title_found) {
|
||||
//Retrieve image for thumbnail or generic logo fallback
|
||||
foreach ($item_desc->find('img') as $img) {
|
||||
if (strpos($img->src, 'prez') === false) {
|
||||
$item_image = $img->src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$item_desc = trim($item_desc->innertext);
|
||||
} else {
|
||||
$item_desc = '<em>No description.</em>';
|
||||
}
|
||||
|
||||
//Build and add final item
|
||||
$item = array();
|
||||
$item['uri'] = $item_uri;
|
||||
$item['title'] = $item_title;
|
||||
$item['author'] = $item_author;
|
||||
$item['timestamp'] = $item_date;
|
||||
$item['enclosures'] = array($item_image);
|
||||
$item['content'] = $item_desc;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
$element = null;
|
||||
}
|
||||
$results = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,133 +1,141 @@
|
||||
<?php
|
||||
class AnimeUltimeBridge extends BridgeAbstract {
|
||||
|
||||
class AnimeUltimeBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = 'Anime-Ultime';
|
||||
const URI = 'http://www.anime-ultime.net/';
|
||||
const CACHE_TIMEOUT = 10800; // 3h
|
||||
const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.';
|
||||
const PARAMETERS = [ [
|
||||
'type' => [
|
||||
'name' => 'Type',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Everything' => '',
|
||||
'Anime' => 'A',
|
||||
'Drama' => 'D',
|
||||
'Tokusatsu' => 'T'
|
||||
]
|
||||
]
|
||||
]];
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = 'Anime-Ultime';
|
||||
const URI = 'http://www.anime-ultime.net/';
|
||||
const CACHE_TIMEOUT = 10800; // 3h
|
||||
const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.';
|
||||
const PARAMETERS = array( array(
|
||||
'type' => array(
|
||||
'name' => 'Type',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Everything' => '',
|
||||
'Anime' => 'A',
|
||||
'Drama' => 'D',
|
||||
'Tokusatsu' => 'T'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
private $filter = 'Releases';
|
||||
private $filter = 'Releases';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
//Add type filter if provided
|
||||
$typeFilter = $this->getKey('type');
|
||||
public function collectData(){
|
||||
|
||||
//Build date and filters for making requests
|
||||
$thismonth = date('mY') . $typeFilter;
|
||||
$lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter;
|
||||
//Add type filter if provided
|
||||
$typeFilter = array_search(
|
||||
$this->getInput('type'),
|
||||
self::PARAMETERS[$this->queriedContext]['type']['values']
|
||||
);
|
||||
|
||||
//Process each HTML page until having 10 releases
|
||||
$processedOK = 0;
|
||||
foreach ([$thismonth, $lastmonth] as $requestFilter) {
|
||||
$url = self::URI . 'history-0-1/' . $requestFilter;
|
||||
$html = getContents($url);
|
||||
// Convert html from iso-8859-1 => utf8
|
||||
$html = utf8_encode($html);
|
||||
$html = str_get_html($html);
|
||||
//Build date and filters for making requests
|
||||
$thismonth = date('mY') . $typeFilter;
|
||||
$lastmonth = date('mY', mktime(0, 0, 0, date('n') - 1, 1, date('Y'))) . $typeFilter;
|
||||
|
||||
//Relases are sorted by day : process each day individually
|
||||
foreach ($html->find('div.history', 0)->find('h3') as $daySection) {
|
||||
//Retrieve day and build date information
|
||||
$dateString = $daySection->plaintext;
|
||||
$day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2));
|
||||
$item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT)
|
||||
. '-'
|
||||
. substr($requestFilter, 0, 2)
|
||||
. '-'
|
||||
. substr($requestFilter, 2, 4));
|
||||
//Process each HTML page until having 10 releases
|
||||
$processedOK = 0;
|
||||
foreach (array($thismonth, $lastmonth) as $requestFilter) {
|
||||
|
||||
//<h3>day</h3><br /><table><tr> <-- useful data in table rows
|
||||
$release = $daySection->next_sibling()->next_sibling()->first_child();
|
||||
//Retrive page contents
|
||||
$url = self::URI . 'history-0-1/' . $requestFilter;
|
||||
$html = getSimpleHTMLDOM($url)
|
||||
or returnServerError('Could not request Anime-Ultime: ' . $url);
|
||||
|
||||
//Process each release of that day, ignoring first table row: contains table headers
|
||||
while (!is_null($release = $release->next_sibling())) {
|
||||
if (count($release->find('td')) > 0) {
|
||||
//Retrieve metadata from table columns
|
||||
$item_link_element = $release->find('td', 0)->find('a', 0);
|
||||
$item_uri = self::URI . $item_link_element->href;
|
||||
$item_name = html_entity_decode($item_link_element->plaintext);
|
||||
//Relases are sorted by day : process each day individually
|
||||
foreach($html->find('div.history', 0)->find('h3') as $daySection) {
|
||||
|
||||
$item_image = self::URI . substr(
|
||||
$item_link_element->onmouseover,
|
||||
37,
|
||||
strpos($item_link_element->onmouseover, ' ', 37) - 37
|
||||
);
|
||||
//Retrieve day and build date information
|
||||
$dateString = $daySection->plaintext;
|
||||
$day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2));
|
||||
$item_date = strtotime(str_pad($day, 2, '0', STR_PAD_LEFT)
|
||||
. '-'
|
||||
. substr($requestFilter, 0, 2)
|
||||
. '-'
|
||||
. substr($requestFilter, 2, 4));
|
||||
|
||||
$item_episode = html_entity_decode(
|
||||
str_pad(
|
||||
$release->find('td', 1)->plaintext,
|
||||
2,
|
||||
'0',
|
||||
STR_PAD_LEFT
|
||||
)
|
||||
);
|
||||
//<h3>day</h3><br /><table><tr> <-- useful data in table rows
|
||||
$release = $daySection->next_sibling()->next_sibling()->first_child();
|
||||
|
||||
$item_fansub = $release->find('td', 2)->plaintext;
|
||||
$item_type = $release->find('td', 4)->plaintext;
|
||||
//Process each release of that day, ignoring first table row: contains table headers
|
||||
while(!is_null($release = $release->next_sibling())) {
|
||||
if(count($release->find('td')) > 0) {
|
||||
|
||||
if (!empty($item_uri)) {
|
||||
// Retrieve description from description page
|
||||
$html_item = getContents($item_uri);
|
||||
// Convert html from iso-8859-1 => utf8
|
||||
$html_item = utf8_encode($html_item);
|
||||
$item_description = substr(
|
||||
$html_item,
|
||||
strpos($html_item, 'class="principal_contain" align="center">') + 41
|
||||
);
|
||||
$item_description = substr(
|
||||
$item_description,
|
||||
0,
|
||||
strpos($item_description, '<div id="table">')
|
||||
);
|
||||
//Retrieve metadata from table columns
|
||||
$item_link_element = $release->find('td', 0)->find('a', 0);
|
||||
$item_uri = self::URI . $item_link_element->href;
|
||||
$item_name = html_entity_decode($item_link_element->plaintext);
|
||||
|
||||
// Convert relative image src into absolute image src, remove line breaks
|
||||
$item_description = defaultLinkTo($item_description, self::URI);
|
||||
$item_description = str_replace("\r", '', $item_description);
|
||||
$item_description = str_replace("\n", '', $item_description);
|
||||
$item_image = self::URI . substr(
|
||||
$item_link_element->onmouseover,
|
||||
37,
|
||||
strpos($item_link_element->onmouseover, ' ', 37) - 37
|
||||
);
|
||||
|
||||
//Build and add final item
|
||||
$item = [];
|
||||
$item['uri'] = $item_uri;
|
||||
$item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode;
|
||||
$item['author'] = $item_fansub;
|
||||
$item['timestamp'] = $item_date;
|
||||
$item['enclosures'] = [$item_image];
|
||||
$item['content'] = $item_description;
|
||||
$this->items[] = $item;
|
||||
$processedOK++;
|
||||
$item_episode = html_entity_decode(
|
||||
str_pad(
|
||||
$release->find('td', 1)->plaintext,
|
||||
2,
|
||||
'0',
|
||||
STR_PAD_LEFT
|
||||
)
|
||||
);
|
||||
|
||||
//Stop processing once limit is reached
|
||||
if ($processedOK >= 10) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$item_fansub = $release->find('td', 2)->plaintext;
|
||||
$item_type = $release->find('td', 4)->plaintext;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('type'))) {
|
||||
return 'Latest ' . $this->getKey('type') . ' - Anime-Ultime Bridge';
|
||||
}
|
||||
if(!empty($item_uri)) {
|
||||
|
||||
// Retrieve description from description page
|
||||
$html_item = getContents($item_uri)
|
||||
or returnServerError('Could not request Anime-Ultime: ' . $item_uri);
|
||||
$item_description = substr(
|
||||
$html_item,
|
||||
strpos($html_item, 'class="principal_contain" align="center">') + 41
|
||||
);
|
||||
$item_description = substr($item_description,
|
||||
0,
|
||||
strpos($item_description, '<div id="table">')
|
||||
);
|
||||
|
||||
// Convert relative image src into absolute image src, remove line breaks
|
||||
$item_description = defaultLinkTo($item_description, self::URI);
|
||||
$item_description = str_replace("\r", '', $item_description);
|
||||
$item_description = str_replace("\n", '', $item_description);
|
||||
$item_description = utf8_encode($item_description);
|
||||
|
||||
//Build and add final item
|
||||
$item = array();
|
||||
$item['uri'] = $item_uri;
|
||||
$item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode;
|
||||
$item['author'] = $item_fansub;
|
||||
$item['timestamp'] = $item_date;
|
||||
$item['enclosures'] = array($item_image);
|
||||
$item['content'] = $item_description;
|
||||
$this->items[] = $item;
|
||||
$processedOK++;
|
||||
|
||||
//Stop processing once limit is reached
|
||||
if ($processedOK >= 10)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
if(!is_null($this->getInput('type'))) {
|
||||
$typeFilter = array_search(
|
||||
$this->getInput('type'),
|
||||
self::PARAMETERS[$this->queriedContext]['type']['values']
|
||||
);
|
||||
|
||||
return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge';
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AnisearchBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'Tone866';
|
||||
const NAME = 'Anisearch';
|
||||
const URI = 'https://www.anisearch.de';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Feed for Anisearch';
|
||||
const PARAMETERS = [[
|
||||
'category' => [
|
||||
'name' => 'Dub',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'DE'
|
||||
=> 'https://www.anisearch.de/anime/index/page-1?char=all&synchro=de&sort=date&order=desc&view=4',
|
||||
'EN'
|
||||
=> 'https://www.anisearch.de/anime/index/page-1?char=all&synchro=en&sort=date&order=desc&view=4',
|
||||
'JP'
|
||||
=> 'https://www.anisearch.de/anime/index/page-1?char=all&synchro=ja&sort=date&order=desc&view=4'
|
||||
]
|
||||
],
|
||||
'trailers' => [
|
||||
'name' => 'Trailers',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Will include trailes',
|
||||
'defaultValue' => false
|
||||
]
|
||||
]];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$baseurl = 'https://www.anisearch.de/';
|
||||
$trailers = false;
|
||||
$trailers = $this->getInput('trailers');
|
||||
$limit = 10;
|
||||
if ($trailers) {
|
||||
$limit = 5;
|
||||
}
|
||||
|
||||
$dom = getSimpleHTMLDOM($this->getInput('category'));
|
||||
|
||||
foreach ($dom->find('li.btype0') as $key => $li) {
|
||||
if ($key >= $limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
$a = $li->find('a', 0);
|
||||
$title = $a->find('span.title', 0);
|
||||
$url = $baseurl . $a->href;
|
||||
|
||||
//get article
|
||||
$domarticle = getSimpleHTMLDOM($url);
|
||||
$content = $domarticle->find('div.details-text', 0);
|
||||
|
||||
//get header-image and set absolute src
|
||||
$headerimage = $domarticle->find('img#details-cover', 0);
|
||||
$src = $headerimage->src;
|
||||
|
||||
foreach ($content->find('.hidden') as $element) {
|
||||
$element->remove();
|
||||
}
|
||||
|
||||
//get trailer
|
||||
$ytlink = '';
|
||||
if ($trailers) {
|
||||
$trailerlink = $domarticle->find('section#trailers > div > div.swiper > ul.swiper-wrapper > li.swiper-slide > a', 0);
|
||||
if (isset($trailerlink)) {
|
||||
$trailersite = getSimpleHTMLDOM($baseurl . $trailerlink->href);
|
||||
$trailer = $trailersite->find('div#video > iframe', 0);
|
||||
$trailer = $trailer->{'data-xsrc'};
|
||||
$ytlink = <<<EOT
|
||||
<br /><iframe width="560" height="315" src="$trailer" title="YouTube video player"
|
||||
frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
EOT;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items[] = [
|
||||
'title' => $title->plaintext,
|
||||
'uri' => $url,
|
||||
'content' => $headerimage . '<br />' . $content . $ytlink
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AnnasArchiveBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Anna\'s Archive';
|
||||
const MAINTAINER = 'phantop';
|
||||
const URI = 'https://annas-archive.org/';
|
||||
const DESCRIPTION = 'Returns books from Anna\'s Archive';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'q' => [
|
||||
'name' => 'Query',
|
||||
'exampleValue' => 'apothecary diaries',
|
||||
'required' => true,
|
||||
],
|
||||
'ext' => [
|
||||
'name' => 'Extension',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Any' => null,
|
||||
'azw3' => 'azw3',
|
||||
'cbr' => 'cbr',
|
||||
'cbz' => 'cbz',
|
||||
'djvu' => 'djvu',
|
||||
'epub' => 'epub',
|
||||
'fb2' => 'fb2',
|
||||
'fb2.zip' => 'fb2.zip',
|
||||
'mobi' => 'mobi',
|
||||
'pdf' => 'pdf',
|
||||
]
|
||||
],
|
||||
'lang' => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Any' => null,
|
||||
'Afrikaans [af]' => 'af',
|
||||
'Arabic [ar]' => 'ar',
|
||||
'Bangla [bn]' => 'bn',
|
||||
'Belarusian [be]' => 'be',
|
||||
'Bulgarian [bg]' => 'bg',
|
||||
'Catalan [ca]' => 'ca',
|
||||
'Chinese [zh]' => 'zh',
|
||||
'Church Slavic [cu]' => 'cu',
|
||||
'Croatian [hr]' => 'hr',
|
||||
'Czech [cs]' => 'cs',
|
||||
'Danish [da]' => 'da',
|
||||
'Dongxiang [sce]' => 'sce',
|
||||
'Dutch [nl]' => 'nl',
|
||||
'English [en]' => 'en',
|
||||
'French [fr]' => 'fr',
|
||||
'German [de]' => 'de',
|
||||
'Greek [el]' => 'el',
|
||||
'Hebrew [he]' => 'he',
|
||||
'Hindi [hi]' => 'hi',
|
||||
'Hungarian [hu]' => 'hu',
|
||||
'Indonesian [id]' => 'id',
|
||||
'Irish [ga]' => 'ga',
|
||||
'Italian [it]' => 'it',
|
||||
'Japanese [ja]' => 'ja',
|
||||
'Kazakh [kk]' => 'kk',
|
||||
'Korean [ko]' => 'ko',
|
||||
'Latin [la]' => 'la',
|
||||
'Latvian [lv]' => 'lv',
|
||||
'Lithuanian [lt]' => 'lt',
|
||||
'Luxembourgish [lb]' => 'lb',
|
||||
'Ndolo [ndl]' => 'ndl',
|
||||
'Norwegian [no]' => 'no',
|
||||
'Persian [fa]' => 'fa',
|
||||
'Polish [pl]' => 'pl',
|
||||
'Portuguese [pt]' => 'pt',
|
||||
'Romanian [ro]' => 'ro',
|
||||
'Russian [ru]' => 'ru',
|
||||
'Serbian [sr]' => 'sr',
|
||||
'Spanish [es]' => 'es',
|
||||
'Swedish [sv]' => 'sv',
|
||||
'Tamil [ta]' => 'ta',
|
||||
'Traditional Chinese [zh‑Hant]' => 'zh‑Hant',
|
||||
'Turkish [tr]' => 'tr',
|
||||
'Ukrainian [uk]' => 'uk',
|
||||
'Unknown language' => '_empty',
|
||||
'Unknown language [und]' => 'und',
|
||||
'Unknown language [urdu]' => 'urdu',
|
||||
'Urdu [ur]' => 'ur',
|
||||
'Vietnamese [vi]' => 'vi',
|
||||
'Welsh [cy]' => 'cy',
|
||||
]
|
||||
],
|
||||
'content' => [
|
||||
'name' => 'Type',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Any' => null,
|
||||
'Book (fiction)' => 'book_fiction',
|
||||
'Book (non‑fiction)' => 'book_nonfiction',
|
||||
'Book (unknown)' => 'book_unknown',
|
||||
'Comic book' => 'book_comic',
|
||||
'Journal article' => 'journal_article',
|
||||
'Magazine' => 'magazine',
|
||||
'Standards document' => 'standards_document',
|
||||
]
|
||||
],
|
||||
'src' => [
|
||||
'name' => 'Source',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Any' => null,
|
||||
'Internet Archive' => 'ia',
|
||||
'Libgen.li' => 'lgli',
|
||||
'Libgen.rs' => 'lgrs',
|
||||
'Sci‑Hub' => 'scihub',
|
||||
'Z‑Library' => 'zlib',
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getURI();
|
||||
$list = getSimpleHTMLDOMCached($url);
|
||||
$list = defaultLinkTo($list, self::URI);
|
||||
|
||||
// Don't attempt to do anything if not found message is given
|
||||
if ($list->find('.js-not-found-additional')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$elements = $list->find('.w-full > .mb-4 > div');
|
||||
foreach ($elements as $element) {
|
||||
// stop added entries once partial match list starts
|
||||
if (str_contains($element->innertext, 'partial match')) {
|
||||
break;
|
||||
}
|
||||
if ($element = $element->find('a', 0)) {
|
||||
$item = [];
|
||||
$item['title'] = $element->find('h3', 0)->plaintext;
|
||||
$item['author'] = $element->find('div.italic', 0)->plaintext;
|
||||
$item['uri'] = $element->href;
|
||||
$item['content'] = $element->plaintext;
|
||||
$item['uid'] = $item['uri'];
|
||||
|
||||
$item_html = getSimpleHTMLDOMCached($item['uri'], 86400 * 20);
|
||||
if ($item_html) {
|
||||
$item_html = defaultLinkTo($item_html, self::URI);
|
||||
$item['content'] .= $item_html->find('main img', 0);
|
||||
$item['content'] .= $item_html->find('main .mt-4', 0); // Summary
|
||||
foreach ($item_html->find('main ul.mb-4 > li > a.js-download-link') as $file) {
|
||||
if (!str_contains($file->href, 'fast_download')) {
|
||||
$item['enclosures'][] = $file->href;
|
||||
}
|
||||
}
|
||||
// Remove bulk torrents from enclosures list
|
||||
$item['enclosures'] = array_diff($item['enclosures'], [self::URI . 'datasets']);
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
$name = parent::getName();
|
||||
if ($this->getInput('q') != null) {
|
||||
$name .= ' - ' . $this->getInput('q');
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
$params = array_filter([ // Filter to remove non-provided parameters
|
||||
'q' => $this->getInput('q'),
|
||||
'ext' => $this->getInput('ext'),
|
||||
'lang' => $this->getInput('lang'),
|
||||
'src' => $this->getInput('src'),
|
||||
'content' => $this->getInput('content'),
|
||||
]);
|
||||
$url = parent::getURI() . 'search?sort=newest&' . http_build_query($params);
|
||||
return $url;
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AnthropicBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'sqrtminusone';
|
||||
const NAME = 'Anthropic Research Bridge';
|
||||
const URI = 'https://www.anthropic.com';
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
const DESCRIPTION = 'Returns research publications from Anthropic';
|
||||
|
||||
const PARAMETERS = [
|
||||
'' => [
|
||||
'limit' => [
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'defaultValue' => 10
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
// Anthropic sometimes returns 500 for no reason. The contents are still there.
|
||||
$html = $this->getHTMLIgnoreError(self::URI . '/research');
|
||||
$limit = $this->getInput('limit');
|
||||
|
||||
$page_data = $this->extractPageData($html);
|
||||
$pages = $this->parsePageData($page_data);
|
||||
for ($i = 0; $i < min(count($pages), $limit); $i++) {
|
||||
$page = $pages[$i];
|
||||
$page['content'] = $this->parsePage($page['uri']);
|
||||
$this->items[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
private function getHTMLIgnoreError($url, $ttl = null)
|
||||
{
|
||||
if ($ttl != null) {
|
||||
$cacheKey = 'pages_' . $url;
|
||||
$content = $this->cache->get($cacheKey);
|
||||
if ($content) {
|
||||
return str_get_html($content);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$content = getContents($url);
|
||||
} catch (HttpException $e) {
|
||||
$content = $e->response->getBody();
|
||||
}
|
||||
if ($ttl != null) {
|
||||
$this->cache->set($cacheKey, $content, $ttl);
|
||||
}
|
||||
return str_get_html($content);
|
||||
}
|
||||
|
||||
private function extractPageData($html)
|
||||
{
|
||||
foreach ($html->find('script') as $script) {
|
||||
$js_code = $script->innertext;
|
||||
if (!str_starts_with($js_code, 'self.__next_f.push(')) {
|
||||
continue;
|
||||
}
|
||||
$push_data = (string)json_decode(mb_substr($js_code, 22, mb_strlen($js_code) - 2 - 22));
|
||||
$square_bracket = mb_strpos($push_data, '[');
|
||||
$push_array = json_decode(mb_substr($push_data, $square_bracket), true);
|
||||
if ($push_array == null || count($push_array) < 4) {
|
||||
continue;
|
||||
}
|
||||
$page_data = $push_array[3];
|
||||
if ($page_data != null && array_key_exists('page', $page_data)) {
|
||||
return $page_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parsePageData($page_data)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($page_data['page']['sections'] as $section) {
|
||||
if (
|
||||
!array_key_exists('internalName', $section) ||
|
||||
$section['internalName'] != 'Research Teams'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
foreach ($section['tabPages'] as $tabPage) {
|
||||
if ($tabPage['label'] != 'Overview') {
|
||||
continue;
|
||||
}
|
||||
foreach ($tabPage['sections'] as $section1) {
|
||||
if (
|
||||
!array_key_exists('title', $section1)
|
||||
|| $section1['title'] != 'Publications'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
foreach ($section1['posts'] as $post) {
|
||||
$enc = [];
|
||||
if ($post['cta'] != null && array_key_exists('url', $post['cta'])) {
|
||||
$enc = [$post['cta']['url']];
|
||||
}
|
||||
$result[] = [
|
||||
'title' => $post['title'],
|
||||
'timestamp' => $post['publishedOn'],
|
||||
'uri' => self::URI . '/research/' . $post['slug']['current'],
|
||||
'categories' => array_map(
|
||||
fn($s) => $s['label'],
|
||||
$post['subjects'],
|
||||
),
|
||||
'enclosures' => $enc,
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parsePage($url)
|
||||
{
|
||||
// Again, 500 for no reason.
|
||||
$html = $this->getHTMLIgnoreError($url, 7 * 24 * 60 * 60);
|
||||
|
||||
$content = '';
|
||||
|
||||
// Main content
|
||||
$main = $html->find('div[class*="PostDetail_post-detail"] > article', 0);
|
||||
|
||||
// Mostly YouTube videos
|
||||
$iframes = $main->find('iframe');
|
||||
foreach ($iframes as $iframe) {
|
||||
$iframe->parent->removeAttribute('style');
|
||||
$iframe->outertext = '<a href="' . $iframe->src . '">' . $iframe->src . '</a>';
|
||||
}
|
||||
|
||||
$main = convertLazyLoading($main);
|
||||
$main = defaultLinkTo($main, self::URI);
|
||||
$content .= $main;
|
||||
return $content;
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AppleAppStoreBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'captn3m0';
|
||||
const NAME = 'Apple App Store';
|
||||
const URI = 'https://apps.apple.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Returns version updates for a specific application';
|
||||
|
||||
const PARAMETERS = [[
|
||||
'id' => [
|
||||
'name' => 'Application ID',
|
||||
'required' => true,
|
||||
'exampleValue' => '310633997'
|
||||
],
|
||||
'p' => [
|
||||
'name' => 'Platform',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'iPad' => 'ipad',
|
||||
'iPhone' => 'iphone',
|
||||
'Mac' => 'mac',
|
||||
|
||||
// The following 2 are present in responses
|
||||
// but not yet tested
|
||||
'Web' => 'web',
|
||||
'Apple TV' => 'appletv',
|
||||
],
|
||||
'defaultValue' => 'iphone',
|
||||
],
|
||||
'country' => [
|
||||
'name' => 'Store Country',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'US' => 'US',
|
||||
'India' => 'IN',
|
||||
'Canada' => 'CA',
|
||||
'Germany' => 'DE',
|
||||
'Netherlands' => 'NL',
|
||||
'Belgium (NL)' => 'BENL',
|
||||
'Belgium (FR)' => 'BEFR',
|
||||
'France' => 'FR',
|
||||
'Italy' => 'IT',
|
||||
'United Kingdom' => 'UK',
|
||||
'Spain' => 'ES',
|
||||
'Portugal' => 'PT',
|
||||
'Australia' => 'AU',
|
||||
'New Zealand' => 'NZ',
|
||||
'Indonesia' => 'ID',
|
||||
'Brazil' => 'BR',
|
||||
],
|
||||
'defaultValue' => 'US',
|
||||
],
|
||||
]];
|
||||
|
||||
const PLATFORM_MAPPING = [
|
||||
'iphone' => 'ios',
|
||||
'ipad' => 'ios',
|
||||
];
|
||||
|
||||
private function makeHtmlUrl($id, $country)
|
||||
{
|
||||
return 'https://apps.apple.com/' . $country . '/app/id' . $id;
|
||||
}
|
||||
|
||||
private function makeJsonUrl($id, $platform, $country)
|
||||
{
|
||||
return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (isset($this->name)) {
|
||||
return $this->name . ' - AppStore Updates';
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* In case of some platforms, the data is present in the initial response
|
||||
*/
|
||||
private function getDataFromShoebox($id, $platform, $country)
|
||||
{
|
||||
$uri = $this->makeHtmlUrl($id, $country);
|
||||
$html = getSimpleHTMLDOMCached($uri, 3600);
|
||||
$script = $html->find('script[id="shoebox-ember-data-store"]', 0);
|
||||
|
||||
$json = json_decode($script->innertext, true);
|
||||
return $json['data'];
|
||||
}
|
||||
|
||||
private function getJWTToken($id, $platform, $country)
|
||||
{
|
||||
$uri = $this->makeHtmlUrl($id, $country);
|
||||
|
||||
$html = getSimpleHTMLDOMCached($uri, 3600);
|
||||
|
||||
$meta = $html->find('meta[name="web-experience-app/config/environment"]', 0);
|
||||
|
||||
$json = urldecode($meta->content);
|
||||
|
||||
$json = json_decode($json);
|
||||
|
||||
return $json->MEDIA_API->token;
|
||||
}
|
||||
|
||||
private function getAppData($id, $platform, $country, $token)
|
||||
{
|
||||
$uri = $this->makeJsonUrl($id, $platform, $country);
|
||||
|
||||
$headers = [
|
||||
"Authorization: Bearer $token",
|
||||
'Origin: https://apps.apple.com',
|
||||
];
|
||||
|
||||
$json = json_decode(getContents($uri, $headers), true);
|
||||
|
||||
return $json['data'][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the version history from the data received
|
||||
* @return array list of versions with details on each element
|
||||
*/
|
||||
private function getVersionHistory($data, $platform)
|
||||
{
|
||||
switch ($platform) {
|
||||
case 'mac':
|
||||
return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
|
||||
default:
|
||||
$os = self::PLATFORM_MAPPING[$platform];
|
||||
return $data['attributes']['platformAttributes'][$os]['versionHistory'];
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$id = $this->getInput('id');
|
||||
$country = $this->getInput('country');
|
||||
$platform = $this->getInput('p');
|
||||
|
||||
switch ($platform) {
|
||||
case 'mac':
|
||||
$data = $this->getDataFromShoebox($id, $platform, $country);
|
||||
break;
|
||||
|
||||
default:
|
||||
$token = $this->getJWTToken($id, $platform, $country);
|
||||
$data = $this->getAppData($id, $platform, $country, $token);
|
||||
}
|
||||
|
||||
$versionHistory = $this->getVersionHistory($data, $platform);
|
||||
$name = $this->name = $data['attributes']['name'];
|
||||
$author = $data['attributes']['artistName'];
|
||||
|
||||
foreach ($versionHistory as $row) {
|
||||
$item = [];
|
||||
|
||||
$item['content'] = nl2br($row['releaseNotes']);
|
||||
$item['title'] = $name . ' - ' . $row['versionDisplay'];
|
||||
$item['timestamp'] = $row['releaseDate'];
|
||||
$item['author'] = $author;
|
||||
|
||||
$item['uri'] = $this->makeHtmlUrl($id, $country);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AppleMusicBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Apple Music';
|
||||
const URI = 'https://www.apple.com';
|
||||
const DESCRIPTION = 'Fetches the latest releases from an artist';
|
||||
const MAINTAINER = 'bockiii';
|
||||
const PARAMETERS = [[
|
||||
'artist' => [
|
||||
'name' => 'Artist ID',
|
||||
'exampleValue' => '909253',
|
||||
'required' => true,
|
||||
],
|
||||
'limit' => [
|
||||
'name' => 'Latest X Releases (max 50)',
|
||||
'defaultValue' => '10',
|
||||
'required' => true,
|
||||
],
|
||||
]];
|
||||
const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours
|
||||
|
||||
private $title;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$items = $this->getJson();
|
||||
$artist = $this->getArtist($items);
|
||||
|
||||
$this->title = $artist->artistName;
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item->wrapperType === 'collection') {
|
||||
$copyright = $item->copyright ?? '';
|
||||
$artworkUrl500 = str_replace('/100x100', '/500x500', $item->artworkUrl100);
|
||||
$artworkUrl2000 = str_replace('/100x100', '/2000x2000', $item->artworkUrl100);
|
||||
$escapedCollectionName = htmlspecialchars($item->collectionName);
|
||||
|
||||
$this->items[] = [
|
||||
'title' => $item->collectionName,
|
||||
'uri' => $item->collectionViewUrl,
|
||||
'timestamp' => $item->releaseDate,
|
||||
'enclosures' => $artworkUrl500,
|
||||
'author' => $item->artistName,
|
||||
'content' => "<figure>
|
||||
<img srcset=\"$item->artworkUrl60 60w, $item->artworkUrl100 100w, $artworkUrl500 500w, $artworkUrl2000 2000w\"
|
||||
sizes=\"100%\" src=\"$artworkUrl2000\"
|
||||
alt=\"Cover of $escapedCollectionName\"
|
||||
style=\"display: block; margin: 0 auto;\" />
|
||||
<figcaption>
|
||||
from <a href=\"$artist->artistLinkUrl\">$item->artistName</a><br />$copyright
|
||||
</figcaption>
|
||||
</figure>",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getJson()
|
||||
{
|
||||
# Limit the amount of releases to 50
|
||||
if ($this->getInput('limit') > 50) {
|
||||
$limit = 50;
|
||||
} else {
|
||||
$limit = $this->getInput('limit');
|
||||
}
|
||||
|
||||
$url = 'https://itunes.apple.com/lookup?id=' . $this->getInput('artist') . '&entity=album&limit=' . $limit . '&sort=recent';
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
$json = json_decode($html);
|
||||
$result = $json->results;
|
||||
|
||||
if (!is_array($result) || count($result) == 0) {
|
||||
returnServerError('There is no artist with id "' . $this->getInput('artist') . '".');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getArtist($json)
|
||||
{
|
||||
$nameArray = array_filter($json, function ($obj) {
|
||||
return $obj->wrapperType == 'artist';
|
||||
});
|
||||
|
||||
if (count($nameArray) === 1) {
|
||||
return $nameArray[0];
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (isset($this->title)) {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
if (empty($this->getInput('artist'))) {
|
||||
return parent::getIcon();
|
||||
}
|
||||
|
||||
// it isn't necessary to set the correct artist name into the url
|
||||
$url = 'https://music.apple.com/us/artist/jon-bellion/' . $this->getInput('artist');
|
||||
$html = getSimpleHTMLDOMCached($url);
|
||||
$image = $html->find('meta[property="og:image"]', 0)->content;
|
||||
|
||||
$imageUpdatedSize = preg_replace('/\/\d*x\d*cw/i', '/144x144-999', $image);
|
||||
|
||||
return $imageUpdatedSize;
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ArsTechnicaBridge extends FeedExpander
|
||||
{
|
||||
const MAINTAINER = 'phantop';
|
||||
const NAME = 'Ars Technica';
|
||||
const URI = 'https://arstechnica.com/';
|
||||
const DESCRIPTION = 'Returns the latest articles from Ars Technica';
|
||||
const PARAMETERS = [[
|
||||
'section' => [
|
||||
'name' => 'Site section',
|
||||
'type' => 'list',
|
||||
'defaultValue' => 'index',
|
||||
'values' => [
|
||||
'All' => 'index',
|
||||
'Apple' => 'apple',
|
||||
'Board Games' => 'cardboard',
|
||||
'Cars' => 'cars',
|
||||
'Features' => 'features',
|
||||
'Gaming' => 'gaming',
|
||||
'Information Technology' => 'technology-lab',
|
||||
'Science' => 'science',
|
||||
'Staff Blogs' => 'staff-blogs',
|
||||
'Tech Policy' => 'tech-policy',
|
||||
'Tech' => 'gadgets',
|
||||
]
|
||||
]
|
||||
]];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = 'https://feeds.arstechnica.com/arstechnica/' . $this->getInput('section');
|
||||
$this->collectExpandableDatas($url, 10);
|
||||
}
|
||||
|
||||
protected function parseItem(array $item)
|
||||
{
|
||||
$item_html = getSimpleHTMLDOMCached($item['uri']);
|
||||
$item_html = defaultLinkTo($item_html, self::URI);
|
||||
|
||||
$content = '';
|
||||
$header = $item_html->find('article header', 0);
|
||||
$leading = $header->find('p[class*=leading]', 0);
|
||||
if ($leading != null) {
|
||||
$content .= '<p>' . $leading->innertext . '</p>';
|
||||
}
|
||||
$intro_image = $header->find('img.intro-image', 0);
|
||||
if ($intro_image != null) {
|
||||
$content .= '<figure>' . $intro_image;
|
||||
|
||||
$image_caption = $header->find('.caption .caption-content', 0);
|
||||
if ($image_caption != null) {
|
||||
$content .= '<figcaption>' . $image_caption->innertext . '</figcaption>';
|
||||
}
|
||||
$content .= '</figure>';
|
||||
}
|
||||
|
||||
foreach ($item_html->find('.post-content') as $content_tag) {
|
||||
$content .= $content_tag->innertext;
|
||||
}
|
||||
|
||||
$item['content'] = str_get_html($content);
|
||||
|
||||
$parsely = $item_html->find('[name="parsely-page"]', 0);
|
||||
$parsely_json = json_decode(html_entity_decode($parsely->content), true);
|
||||
$item['categories'] = $parsely_json['tags'];
|
||||
|
||||
// Some lightboxes are nested in figures. I'd guess that's a
|
||||
// bug in the website
|
||||
foreach ($item['content']->find('figure div div.ars-lightbox') as $weird_lightbox) {
|
||||
$weird_lightbox->parent->parent->outertext = $weird_lightbox;
|
||||
}
|
||||
|
||||
// It's easier to reconstruct the whole thing than remove
|
||||
// duplicate reactive tags
|
||||
foreach ($item['content']->find('.ars-lightbox') as $lightbox) {
|
||||
$lightbox_content = '';
|
||||
foreach ($lightbox->find('.ars-lightbox-item') as $lightbox_item) {
|
||||
$img = $lightbox_item->find('img', 0);
|
||||
if ($img != null) {
|
||||
$lightbox_content .= '<figure>' . $img;
|
||||
$caption = $lightbox_item->find('div.pswp-caption-content', 0);
|
||||
if ($caption != null) {
|
||||
$credit = $lightbox_item->find('div.ars-gallery-caption-credit', 0);
|
||||
if ($credit != null) {
|
||||
$credit->innertext = 'Credit: ' . $credit->innertext;
|
||||
}
|
||||
$lightbox_content .= '<figcaption>' . $caption->innertext . '</figcaption>';
|
||||
}
|
||||
$lightbox_content .= '</figure>';
|
||||
}
|
||||
}
|
||||
$lightbox->innertext = $lightbox_content;
|
||||
}
|
||||
|
||||
// remove various ars advertising
|
||||
foreach ($item['content']->find('.ars-interlude-container') as $ad) {
|
||||
$ad->remove();
|
||||
}
|
||||
foreach ($item['content']->find('.toc-container') as $toc) {
|
||||
$toc->remove();
|
||||
}
|
||||
|
||||
// Mostly YouTube videos
|
||||
$iframes = $item['content']->find('iframe');
|
||||
foreach ($iframes as $iframe) {
|
||||
$iframe->outertext = '<a href="' . $iframe->src . '">' . $iframe->src . '</a>';
|
||||
}
|
||||
// This fixed padding around the former iframes and actual inline videos
|
||||
foreach ($item['content']->find('div[style*=aspect-ratio]') as $styled) {
|
||||
$styled->removeAttribute('style');
|
||||
}
|
||||
|
||||
$item['content'] = backgroundToImg($item['content']);
|
||||
$item['uid'] = strval($parsely_json['post_id']);
|
||||
return $item;
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
<?php
|
||||
|
||||
class ArtStationBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ArtStation';
|
||||
const URI = 'https://www.artstation.com';
|
||||
const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.';
|
||||
const MAINTAINER = 'thefranke';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
|
||||
const PARAMETERS = [
|
||||
'Search Query' => [
|
||||
'q' => [
|
||||
'name' => 'Search term',
|
||||
'required' => true,
|
||||
'exampleValue' => 'bird'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return self::NAME . ': ' . $this->getInput('q');
|
||||
}
|
||||
|
||||
private function fetchSearch($searchQuery)
|
||||
{
|
||||
$data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
|
||||
$data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
|
||||
|
||||
$header = [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json'
|
||||
];
|
||||
|
||||
$opts = [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $data,
|
||||
CURLOPT_RETURNTRANSFER => true
|
||||
];
|
||||
|
||||
$jsonSearchURL = self::URI . '/api/v2/search/projects.json';
|
||||
$jsonSearchStr = getContents($jsonSearchURL, $header, $opts);
|
||||
return json_decode($jsonSearchStr);
|
||||
}
|
||||
|
||||
private function fetchProject($hashID)
|
||||
{
|
||||
$jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
|
||||
$jsonProjectStr = getContents($jsonProjectURL);
|
||||
return json_decode($jsonProjectStr);
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$searchTerm = $this->getInput('q');
|
||||
$jsonQuery = $this->fetchSearch($searchTerm);
|
||||
|
||||
foreach ($jsonQuery->data as $media) {
|
||||
// get detailed info about media item
|
||||
$jsonProject = $this->fetchProject($media->hash_id);
|
||||
|
||||
// create item
|
||||
$item = [];
|
||||
$item['title'] = $media->title;
|
||||
$item['uri'] = $media->url;
|
||||
$item['timestamp'] = strtotime($jsonProject->published_at);
|
||||
$item['author'] = $media->user->full_name;
|
||||
$item['categories'] = implode(',', $jsonProject->tags);
|
||||
|
||||
$item['content'] = '<a href="'
|
||||
. $media->url
|
||||
. '"><img style="max-width: 100%" src="'
|
||||
. $jsonProject->cover_url
|
||||
. '"></a><p>'
|
||||
. $jsonProject->description
|
||||
. '</p>';
|
||||
|
||||
$numAssets = count($jsonProject->assets);
|
||||
|
||||
if ($numAssets > 1) {
|
||||
$item['content'] .= '<p><a href="'
|
||||
. $media->url
|
||||
. '">Project contains '
|
||||
. ($numAssets - 1)
|
||||
. ' more item(s).</a></p>';
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
if (count($this->items) >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,166 +1,123 @@
|
||||
<?php
|
||||
class Arte7Bridge extends BridgeAbstract {
|
||||
|
||||
class Arte7Bridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Arte +7';
|
||||
const URI = 'https://www.arte.tv/';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Returns newest videos from ARTE +7';
|
||||
const MAINTAINER = 'mitsukarenai';
|
||||
const NAME = 'Arte +7';
|
||||
const URI = 'https://www.arte.tv/';
|
||||
const CACHE_TIMEOUT = 1800; // 30min
|
||||
const DESCRIPTION = 'Returns newest videos from ARTE +7';
|
||||
|
||||
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
|
||||
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
|
||||
|
||||
const PARAMETERS = [
|
||||
'global' => [
|
||||
'sort_by' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Sort by',
|
||||
'required' => false,
|
||||
'defaultValue' => null,
|
||||
'values' => [
|
||||
'Default' => null,
|
||||
'Video rights start date' => 'videoRightsBegin',
|
||||
'Video rights end date' => 'videoRightsEnd',
|
||||
'Brodcast date' => 'broadcastBegin',
|
||||
'Creation date' => 'creationDate',
|
||||
'Last modified' => 'lastModified',
|
||||
'Number of views' => 'views',
|
||||
'Number of views per period' => 'viewsPeriod',
|
||||
'Available screens' => 'availableScreens',
|
||||
'Episode' => 'episode'
|
||||
],
|
||||
],
|
||||
'sort_direction' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Sort direction',
|
||||
'required' => false,
|
||||
'defaultValue' => 'DESC',
|
||||
'values' => [
|
||||
'Ascending' => 'ASC',
|
||||
'Descending' => 'DESC'
|
||||
],
|
||||
],
|
||||
'exclude_trailers' => [
|
||||
'name' => 'Exclude trailers',
|
||||
'type' => 'checkbox',
|
||||
'required' => false,
|
||||
'defaultValue' => false
|
||||
],
|
||||
],
|
||||
'Category' => [
|
||||
'lang' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Language',
|
||||
'values' => [
|
||||
'Français' => 'fr',
|
||||
'Deutsch' => 'de',
|
||||
'English' => 'en',
|
||||
'Español' => 'es',
|
||||
'Polski' => 'pl',
|
||||
'Italiano' => 'it'
|
||||
],
|
||||
],
|
||||
'cat' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Category',
|
||||
'values' => [
|
||||
'All videos' => null,
|
||||
'News & society' => 'ACT',
|
||||
'Series & fiction' => 'SER',
|
||||
'Cinema' => 'CIN',
|
||||
'Culture' => 'ARS',
|
||||
'Culture pop' => 'CPO',
|
||||
'Discovery' => 'DEC',
|
||||
'History' => 'HIST',
|
||||
'Science' => 'SCI',
|
||||
'Other' => 'AUT'
|
||||
]
|
||||
],
|
||||
],
|
||||
'Collection' => [
|
||||
'lang' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Language',
|
||||
'values' => [
|
||||
'Français' => 'fr',
|
||||
'Deutsch' => 'de',
|
||||
'English' => 'en',
|
||||
'Español' => 'es',
|
||||
'Polski' => 'pl',
|
||||
'Italiano' => 'it'
|
||||
]
|
||||
],
|
||||
'col' => [
|
||||
'name' => 'Collection id',
|
||||
'required' => true,
|
||||
'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/',
|
||||
'exampleValue' => 'RC-014095'
|
||||
]
|
||||
]
|
||||
];
|
||||
const PARAMETERS = array(
|
||||
'Catégorie (Français)' => array(
|
||||
'catfr' => array(
|
||||
'type' => 'list',
|
||||
'name' => 'Catégorie',
|
||||
'values' => array(
|
||||
'Toutes les vidéos (français)' => null,
|
||||
'Actu & société' => 'ACT',
|
||||
'Séries & fiction' => 'SER',
|
||||
'Cinéma' => 'CIN',
|
||||
'Arts & spectacles classiques' => 'ARS',
|
||||
'Culture pop' => 'CPO',
|
||||
'Découverte' => 'DEC',
|
||||
'Histoire' => 'HIST',
|
||||
'Science' => 'SCI',
|
||||
'Autre' => 'AUT'
|
||||
)
|
||||
)
|
||||
),
|
||||
'Collection (Français)' => array(
|
||||
'colfr' => array(
|
||||
'name' => 'Collection id',
|
||||
'required' => true,
|
||||
'title' => 'ex. RC-014095 pour https://www.arte.tv/fr/videos/RC-014095/blow-up/'
|
||||
)
|
||||
),
|
||||
'Catégorie (Allemand)' => array(
|
||||
'catde' => array(
|
||||
'type' => 'list',
|
||||
'name' => 'Catégorie',
|
||||
'values' => array(
|
||||
'Alle Videos (deutsch)' => null,
|
||||
'Aktuelles & Gesellschaft' => 'ACT',
|
||||
'Fernsehfilme & Serien' => 'SER',
|
||||
'Kino' => 'CIN',
|
||||
'Kunst & Kultur' => 'ARS',
|
||||
'Popkultur & Alternativ' => 'CPO',
|
||||
'Entdeckung' => 'DEC',
|
||||
'Geschichte' => 'HIST',
|
||||
'Wissenschaft' => 'SCI',
|
||||
'Sonstiges' => 'AUT'
|
||||
)
|
||||
)
|
||||
),
|
||||
'Collection (Allemand)' => array(
|
||||
'colde' => array(
|
||||
'name' => 'Collection id',
|
||||
'required' => true,
|
||||
'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Category':
|
||||
$category = $this->getInput('cat');
|
||||
$collectionId = null;
|
||||
break;
|
||||
case 'Collection':
|
||||
$collectionId = $this->getInput('col');
|
||||
$category = null;
|
||||
break;
|
||||
}
|
||||
public function collectData(){
|
||||
switch($this->queriedContext) {
|
||||
case 'Catégorie (Français)':
|
||||
$category = $this->getInput('catfr');
|
||||
$lang = 'fr';
|
||||
break;
|
||||
case 'Collection (Français)':
|
||||
$lang = 'fr';
|
||||
$collectionId = $this->getInput('colfr');
|
||||
break;
|
||||
case 'Catégorie (Allemand)':
|
||||
$category = $this->getInput('catde');
|
||||
$lang = 'de';
|
||||
break;
|
||||
case 'Collection (Allemand)':
|
||||
$lang = 'de';
|
||||
$collectionId = $this->getInput('colde');
|
||||
break;
|
||||
}
|
||||
|
||||
$lang = $this->getInput('lang');
|
||||
$sort_by = $this->getInput('sort_by');
|
||||
$sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-';
|
||||
$url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=10&language='
|
||||
. $lang
|
||||
. ($category != null ? '&category.code=' . $category : '')
|
||||
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
|
||||
|
||||
$url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language='
|
||||
. $lang
|
||||
. ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '')
|
||||
. ($category != null ? '&category.code=' . $category : '')
|
||||
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
|
||||
$header = array(
|
||||
'Authorization: Bearer ' . self::API_TOKEN
|
||||
);
|
||||
|
||||
$header = [
|
||||
'Authorization: Bearer ' . self::API_TOKEN
|
||||
];
|
||||
$input = getContents($url, $header) or die('Could not request ARTE.');
|
||||
$input_json = json_decode($input, true);
|
||||
|
||||
$input = getContents($url, $header);
|
||||
$input_json = json_decode($input, true);
|
||||
foreach($input_json['videos'] as $element) {
|
||||
|
||||
foreach ($input_json['videos'] as $element) {
|
||||
if ($this->getInput('exclude_trailers') && $element['platform'] == 'EXTRAIT') {
|
||||
continue;
|
||||
}
|
||||
$item = array();
|
||||
$item['uri'] = $element['url'];
|
||||
$item['id'] = $element['id'];
|
||||
|
||||
$durationSeconds = $element['durationSeconds'];
|
||||
$item['timestamp'] = strtotime($element['videoRightsBegin']);
|
||||
$item['title'] = $element['title'];
|
||||
|
||||
$item = [];
|
||||
$item['uri'] = $element['url'];
|
||||
$item['id'] = $element['id'];
|
||||
if(!empty($element['subtitle']))
|
||||
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
|
||||
|
||||
$item['timestamp'] = strtotime($element['videoRightsBegin']);
|
||||
$item['title'] = $element['title'];
|
||||
$item['duration'] = round((int)$element['durationSeconds'] / 60);
|
||||
$item['content'] = $element['teaserText']
|
||||
. '<br><br>'
|
||||
. $item['duration']
|
||||
. 'min<br><a href="'
|
||||
. $item['uri']
|
||||
. '"><img src="'
|
||||
. $element['mainImage']['url']
|
||||
. '" /></a>';
|
||||
|
||||
if (!empty($element['subtitle'])) {
|
||||
$item['title'] = $element['title'] . ' | ' . $element['subtitle'];
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$durationMinutes = round((int)$durationSeconds / 60);
|
||||
$item['content'] = $element['teaserText']
|
||||
. '<br><br>'
|
||||
. $durationMinutes
|
||||
. 'min<br><a href="'
|
||||
. $item['uri']
|
||||
. '"><img src="'
|
||||
. $element['mainImage']['url']
|
||||
. '" /></a>';
|
||||
|
||||
$item['itunes'] = [
|
||||
'duration' => $durationSeconds,
|
||||
];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AsahiShimbunAJWBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Asahi Shimbun AJW';
|
||||
const BASE_URI = 'http://www.asahi.com';
|
||||
const URI = self::BASE_URI . '/ajw/';
|
||||
const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
|
||||
const MAINTAINER = 'somini';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'section' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Section',
|
||||
'values' => [
|
||||
'Japan » Social Affairs' => 'japan/social',
|
||||
'Japan » People' => 'japan/people',
|
||||
'Japan » 3/11 Disaster' => 'japan/0311disaster',
|
||||
'Japan » Sci & Tech' => 'japan/sci_tech',
|
||||
'Politics' => 'politics',
|
||||
'Business' => 'business',
|
||||
'Culture » Style' => 'culture/style',
|
||||
'Culture » Movies' => 'culture/movies',
|
||||
'Culture » Manga & Anime' => 'culture/manga_anime',
|
||||
'Asia » China' => 'asia_world/china',
|
||||
'Asia » Korean Peninsula' => 'asia_world/korean_peninsula',
|
||||
'Asia » Around Asia' => 'asia_world/around_asia',
|
||||
'Asia » World' => 'asia_world/world',
|
||||
'Opinion » Editorial' => 'opinion/editorial',
|
||||
'Opinion » Vox Populi' => 'opinion/vox',
|
||||
],
|
||||
'defaultValue' => 'politics',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
private function getSectionURI($section)
|
||||
{
|
||||
return $this->getURI() . $section . '/';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')));
|
||||
|
||||
foreach ($html->find('#MainInner li a') as $element) {
|
||||
if ($element->parent()->class == 'HeadlineTopImage-S') {
|
||||
continue;
|
||||
}
|
||||
$item = [];
|
||||
|
||||
$item['uri'] = self::BASE_URI . $element->href;
|
||||
$e_lead = $element->find('span.Lead', 0);
|
||||
if ($e_lead) {
|
||||
$item['content'] = $e_lead->innertext;
|
||||
$e_lead->outertext = '';
|
||||
} else {
|
||||
$item['content'] = $element->innertext;
|
||||
}
|
||||
$e_date = $element->find('span.EnDate', 0);
|
||||
if ($e_date) {
|
||||
$item['timestamp'] = strtotime($e_date->innertext);
|
||||
$e_date->outertext = '';
|
||||
}
|
||||
$e_video = $element->find('span.EnVideo', 0);
|
||||
if ($e_video) {
|
||||
$e_video->outertext = '';
|
||||
$element->innertext = "VIDEO: $element->innertext";
|
||||
}
|
||||
$e_title = $element->find('.title', 0);
|
||||
if ($e_title) {
|
||||
$item['title'] = $e_title->innertext;
|
||||
} else {
|
||||
$item['title'] = $element->innertext;
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +1,74 @@
|
||||
<?php
|
||||
class AskfmBridge extends BridgeAbstract {
|
||||
|
||||
class AskfmBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'az5he6ch, logmanoriginal';
|
||||
const NAME = 'Ask.fm Answers';
|
||||
const URI = 'https://ask.fm/';
|
||||
const CACHE_TIMEOUT = 300; //5 min
|
||||
const DESCRIPTION = 'Returns answers from an Ask.fm user';
|
||||
const PARAMETERS = [
|
||||
'Ask.fm username' => [
|
||||
'u' => [
|
||||
'name' => 'Username',
|
||||
'required' => true,
|
||||
'exampleValue' => 'ApprovedAndReal'
|
||||
]
|
||||
]
|
||||
];
|
||||
const MAINTAINER = 'az5he6ch, logmanoriginal';
|
||||
const NAME = 'Ask.fm Answers';
|
||||
const URI = 'https://ask.fm/';
|
||||
const CACHE_TIMEOUT = 300; //5 min
|
||||
const DESCRIPTION = 'Returns answers from an Ask.fm user';
|
||||
const PARAMETERS = array(
|
||||
'Ask.fm username' => array(
|
||||
'u' => array(
|
||||
'name' => 'Username',
|
||||
'required' => true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
public function collectData(){
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('Requested username can\'t be found.');
|
||||
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
foreach ($html->find('article.streamItem-answer') as $element) {
|
||||
$item = [];
|
||||
$item['uri'] = $element->find('a.streamItem_meta', 0)->href;
|
||||
$question = trim($element->find('header.streamItem_header', 0)->innertext);
|
||||
foreach($html->find('article.streamItem-answer') as $element) {
|
||||
$item = array();
|
||||
$item['uri'] = $element->find('a.streamItem_meta', 0)->href;
|
||||
$question = trim($element->find('header.streamItem_header', 0)->innertext);
|
||||
|
||||
$item['title'] = trim(
|
||||
htmlspecialchars_decode(
|
||||
$element->find('header.streamItem_header', 0)->plaintext,
|
||||
ENT_QUOTES
|
||||
)
|
||||
);
|
||||
$item['title'] = trim(
|
||||
htmlspecialchars_decode($element->find('header.streamItem_header', 0)->plaintext,
|
||||
ENT_QUOTES
|
||||
)
|
||||
);
|
||||
|
||||
$item['timestamp'] = strtotime($element->find('time', 0)->datetime);
|
||||
$item['timestamp'] = strtotime($element->find('time', 0)->datetime);
|
||||
|
||||
$var = $element->find('div.streamItem_content', 0);
|
||||
$answer = trim($var->innertext ?? '');
|
||||
$answer = trim($element->find('div.streamItem_content', 0)->innertext);
|
||||
|
||||
// This probably should be cleaned up, especially for YouTube embeds
|
||||
if ($visual = $element->find('div.streamItem_visual', 0)) {
|
||||
$visual = $visual->innertext;
|
||||
}
|
||||
// This probably should be cleaned up, especially for YouTube embeds
|
||||
if($visual = $element->find('div.streamItem_visual', 0)) {
|
||||
$visual = $visual->innertext;
|
||||
}
|
||||
|
||||
// Fix tracking links, also doesn't work
|
||||
foreach ($element->find('a') as $link) {
|
||||
if (strpos($link->href, 'l.ask.fm') !== false) {
|
||||
$link->href = $link->plaintext;
|
||||
}
|
||||
}
|
||||
// Fix tracking links, also doesn't work
|
||||
foreach($element->find('a') as $link) {
|
||||
if(strpos($link->href, 'l.ask.fm') !== false) {
|
||||
$link->href = $link->plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] = '<p>' . $question
|
||||
. '</p><p>' . $answer
|
||||
. '</p><p>' . $visual . '</p>';
|
||||
$item['content'] = '<p>' . $question
|
||||
. '</p><p>' . $answer
|
||||
. '</p><p>' . $visual . '</p>';
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('u'))) {
|
||||
return self::NAME . ' : ' . $this->getInput('u');
|
||||
}
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('u'))) {
|
||||
return self::NAME . ' : ' . $this->getInput('u');
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if (!is_null($this->getInput('u'))) {
|
||||
return self::URI . urlencode($this->getInput('u'));
|
||||
}
|
||||
public function getURI(){
|
||||
if(!is_null($this->getInput('u'))) {
|
||||
return self::URI . urlencode($this->getInput('u'));
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
}
|
||||
|
@ -1,280 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AssociatedPressNewsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Associated Press News Bridge';
|
||||
const URI = 'https://apnews.com/';
|
||||
const DESCRIPTION = 'Returns newest articles by topic';
|
||||
const MAINTAINER = 'VerifiedJoseph';
|
||||
const PARAMETERS = [
|
||||
'Standard Topics' => [
|
||||
'topic' => [
|
||||
'name' => 'Topic',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'AP Top News' => 'apf-topnews',
|
||||
'Sports' => 'apf-sports',
|
||||
'Entertainment' => 'apf-entertainment',
|
||||
'Oddities' => 'apf-oddities',
|
||||
'Travel' => 'apf-Travel',
|
||||
'Technology' => 'apf-technology',
|
||||
'Lifestyle' => 'apf-lifestyle',
|
||||
'Business' => 'apf-business',
|
||||
'U.S. News' => 'apf-usnews',
|
||||
'Health' => 'apf-Health',
|
||||
'Science' => 'apf-science',
|
||||
'World News' => 'apf-WorldNews',
|
||||
'Politics' => 'apf-politics',
|
||||
'Religion' => 'apf-religion',
|
||||
'Photo Galleries' => 'PhotoGalleries',
|
||||
'Fact Checks' => 'APFactCheck',
|
||||
'Videos' => 'apf-videos',
|
||||
],
|
||||
'defaultValue' => 'apf-topnews',
|
||||
],
|
||||
],
|
||||
'Custom Topic' => [
|
||||
'topic' => [
|
||||
'name' => 'Topic',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'europe'
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
const CACHE_TIMEOUT = 900; // 15 mins
|
||||
|
||||
private $detectParamRegex = '/^https?:\/\/(?:www\.)?apnews\.com\/(?:[tag|hub]+\/)?([\w-]+)$/';
|
||||
private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
|
||||
private $feedName = '';
|
||||
|
||||
public function detectParameters($url)
|
||||
{
|
||||
$params = [];
|
||||
|
||||
if (preg_match($this->detectParamRegex, $url, $matches) > 0) {
|
||||
$params['topic'] = $matches[1];
|
||||
$params['context'] = 'Custom Topic';
|
||||
return $params;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch ($this->getInput('topic')) {
|
||||
case 'Podcasts':
|
||||
returnClientError('Podcasts topic feed is not supported');
|
||||
break;
|
||||
case 'PressReleases':
|
||||
returnClientError('PressReleases topic feed is not supported');
|
||||
break;
|
||||
default:
|
||||
$this->collectCardData();
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if (!is_null($this->getInput('topic'))) {
|
||||
return self::URI . $this->getInput('topic');
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!empty($this->feedName)) {
|
||||
return $this->feedName . ' - Associated Press';
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
private function getTagURI()
|
||||
{
|
||||
if (!is_null($this->getInput('topic'))) {
|
||||
return $this->tagEndpoint . $this->getInput('topic');
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
private function collectCardData()
|
||||
{
|
||||
$json = getContents($this->getTagURI());
|
||||
|
||||
$tagContents = json_decode($json, true);
|
||||
|
||||
if (empty($tagContents['tagObjs'])) {
|
||||
returnClientError('Topic not found: ' . $this->getInput('topic'));
|
||||
}
|
||||
|
||||
$this->feedName = $tagContents['tagObjs'][0]['name'];
|
||||
|
||||
foreach ($tagContents['cards'] as $card) {
|
||||
$item = [];
|
||||
|
||||
// skip hub peeks & Notifications
|
||||
if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$storyContent = $card['contents'][0];
|
||||
|
||||
switch ($storyContent['contentType']) {
|
||||
case 'web': // Skip link only content
|
||||
continue 2;
|
||||
|
||||
case 'video':
|
||||
$html = $this->processVideo($storyContent);
|
||||
|
||||
$item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
|
||||
. $storyContent['media'][0]['id'] . '/800.jpeg';
|
||||
break;
|
||||
default:
|
||||
if (empty($storyContent['storyHTML'])) { // Skip if no storyHTML
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$html = defaultLinkTo($storyContent['storyHTML'], self::URI);
|
||||
$html = str_get_html($html);
|
||||
|
||||
$this->processMediaPlaceholders($html, $storyContent['id']);
|
||||
$this->processHubLinks($html, $storyContent);
|
||||
$this->processIframes($html);
|
||||
|
||||
if (!is_null($storyContent['leadPhotoId'])) {
|
||||
$leadPhotoUrl = sprintf('https://storage.googleapis.com/afs-prod/media/%s/800.jpeg', $storyContent['leadPhotoId']);
|
||||
$leadPhotoImageTag = sprintf('<img src="%s">', $leadPhotoUrl);
|
||||
// Move the image to the beginning of the content
|
||||
$html = $leadPhotoImageTag . $html;
|
||||
// Explicitly not adding it to the item's enclosures!
|
||||
}
|
||||
}
|
||||
|
||||
$item['title'] = $card['contents'][0]['headline'];
|
||||
$item['uri'] = self::URI . $card['shortId'];
|
||||
|
||||
if ($card['contents'][0]['localLinkUrl']) {
|
||||
$item['uri'] = $card['contents'][0]['localLinkUrl'];
|
||||
}
|
||||
|
||||
$item['timestamp'] = $storyContent['published'];
|
||||
|
||||
if (is_null($storyContent['bylines']) === false) {
|
||||
// Remove 'By' from the bylines
|
||||
if (substr($storyContent['bylines'], 0, 2) == 'By') {
|
||||
$item['author'] = ltrim($storyContent['bylines'], 'By ');
|
||||
} else {
|
||||
$item['author'] = $storyContent['bylines'];
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] = $html;
|
||||
|
||||
foreach ($storyContent['tagObjs'] as $tag) {
|
||||
$item['categories'][] = $tag['name'];
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
if (count($this->items) >= 15) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processMediaPlaceholders($html, $id)
|
||||
{
|
||||
if ($html->find('div.media-placeholder', 0)) {
|
||||
// Fetch page content
|
||||
$json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id);
|
||||
$storyContent = json_decode($json, true);
|
||||
|
||||
foreach ($html->find('div.media-placeholder') as $div) {
|
||||
$key = array_search($div->id, $storyContent['mediumIds']);
|
||||
|
||||
if (!isset($storyContent['media'][$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$media = $storyContent['media'][$key];
|
||||
|
||||
if ($media['type'] === 'Photo') {
|
||||
$mediaUrl = $media['gcsBaseUrl'] . $media['imageRenderedSizes'][0] . $media['imageFileExtension'];
|
||||
$mediaCaption = $media['caption'];
|
||||
|
||||
$div->outertext = <<<EOD
|
||||
<figure><img loading="lazy" src="{$mediaUrl}"/><figcaption>{$mediaCaption}</figcaption></figure>
|
||||
EOD;
|
||||
}
|
||||
|
||||
if ($media['type'] === 'YouTube') {
|
||||
$div->outertext = <<<EOD
|
||||
<iframe src="https://www.youtube.com/embed/{$media['externalId']}" width="560" height="315">
|
||||
</iframe>
|
||||
EOD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Create full coverage links (HubLinks)
|
||||
*/
|
||||
private function processHubLinks($html, $storyContent)
|
||||
{
|
||||
if (!empty($storyContent['richEmbeds'])) {
|
||||
foreach ($storyContent['richEmbeds'] as $embed) {
|
||||
if ($embed['type'] === 'Hub Link') {
|
||||
$url = self::URI . $embed['tag']['id'];
|
||||
$div = $html->find('div[id=' . $embed['id'] . ']', 0);
|
||||
|
||||
if ($div) {
|
||||
$div->outertext = <<<EOD
|
||||
<p><a href="{$url}">{$embed['calloutText']} {$embed['displayName']}</a></p>
|
||||
EOD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processVideo($storyContent)
|
||||
{
|
||||
$video = $storyContent['media'][0];
|
||||
|
||||
if ($video['type'] === 'YouTube') {
|
||||
$url = 'https://www.youtube.com/embed/' . $video['externalId'];
|
||||
$html = <<<EOD
|
||||
<iframe width="560" height="315" src="{$url}" frameborder="0" allowfullscreen></iframe>
|
||||
EOD;
|
||||
} else {
|
||||
$html = <<<EOD
|
||||
<video controls poster="https://storage.googleapis.com/afs-prod/media/{$video['id']}/800.jpeg" preload="none">
|
||||
<source src="{$video['gcsBaseUrl']} {$video['videoRenderedSizes'][0]} {$video['videoFileExtension']}" type="video/mp4">
|
||||
</video>
|
||||
EOD;
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Remove datawrapper.dwcdn.net iframes and related javaScript
|
||||
private function processIframes($html)
|
||||
{
|
||||
foreach ($html->find('iframe') as $index => $iframe) {
|
||||
if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) {
|
||||
$iframe->outertext = '';
|
||||
|
||||
if ($html->find('script', $index)) {
|
||||
$html->find('script', $index)->outertext = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AstrophysicsDataSystemBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'SAO/NASA Astrophysics Data System';
|
||||
const DESCRIPTION = 'Returns the latest publications from a query';
|
||||
const URI = 'https://ui.adsabs.harvard.edu';
|
||||
const PARAMETERS = [
|
||||
'Publications' => [
|
||||
'query' => [
|
||||
'name' => 'query',
|
||||
'title' => 'Same format as the search bar on the website',
|
||||
'exampleValue' => 'author:"huchra, john"',
|
||||
'required' => true
|
||||
]
|
||||
]];
|
||||
|
||||
private $feedTitle;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if ($this->queriedContext === 'Publications') {
|
||||
return $this->feedTitle;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if ($this->queriedContext === 'Publications') {
|
||||
return self::URI . '/search/?q=' . urlencode($this->getInput('query'));
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$headers = [
|
||||
'Cookie: core=always;'
|
||||
];
|
||||
$html = str_get_html(defaultLinkTo(getContents($this->getURI(), $headers), self::URI));
|
||||
$this->feedTitle = html_entity_decode($html->find('title', 0)->plaintext);
|
||||
foreach ($html->find('div.row > ul > li') as $pub) {
|
||||
$item = [];
|
||||
$item['title'] = $pub->find('h3.s-results-title', 0)->plaintext;
|
||||
$item['content'] = $pub->find('div.s-results-links', 0);
|
||||
$item['uri'] = $pub->find('a.abs-redirect-link', 0)->href;
|
||||
$item['author'] = rtrim($pub->find('li.article-author', 0)->plaintext, ' ;');
|
||||
$item['timestamp'] = $pub->find('div[aria-label="date published"]', 0)->plaintext;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AtmoOccitanieBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Atmo Occitanie';
|
||||
const URI = 'https://www.atmo-occitanie.org/';
|
||||
const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo';
|
||||
const MAINTAINER = 'floviolleau';
|
||||
const PARAMETERS = [[
|
||||
'city' => [
|
||||
'name' => 'Ville',
|
||||
'required' => true,
|
||||
'exampleValue' => 'cahors'
|
||||
]
|
||||
]];
|
||||
const CACHE_TIMEOUT = 7200;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$uri = self::URI . $this->getInput('city');
|
||||
|
||||
$html = getSimpleHTMLDOM($uri);
|
||||
|
||||
$generalMessage = $html->find('.landing-ville .city-banner .iqa-avertissement', 0)->innertext;
|
||||
$recommendationsDom = $html->find('.landing-ville .recommandations', 0);
|
||||
$recommendationsItemDom = $recommendationsDom->find('.recommandation-item .label');
|
||||
|
||||
$recommendationsMessage = '';
|
||||
|
||||
$i = 0;
|
||||
$len = count($recommendationsItemDom);
|
||||
foreach ($recommendationsItemDom as $key => $value) {
|
||||
if ($i == 0) {
|
||||
$recommendationsMessage .= trim($value->innertext) . '.';
|
||||
} else {
|
||||
$recommendationsMessage .= ' ' . trim($value->innertext) . '.';
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
$lastRecommendationsDom = $recommendationsDom->find('.col-md-6', -1);
|
||||
$informationHeaderMessage = $lastRecommendationsDom->find('.heading', 0)->innertext;
|
||||
$indice = $lastRecommendationsDom->find('.current-indice .indice div', 0)->innertext;
|
||||
$informationDescriptionMessage = $lastRecommendationsDom->find('.current-indice .description p', 0)->innertext;
|
||||
|
||||
$message = "$generalMessage L'indice est de $indice/10. $informationDescriptionMessage. $recommendationsMessage";
|
||||
$city = $this->getInput('city');
|
||||
|
||||
$item['uri'] = $uri;
|
||||
$today = date('d/m/Y');
|
||||
$item['title'] = "Bulletin de l'air du $today pour la ville : $city.";
|
||||
//$item['title'] .= ' Retrouvez plus d\'informations en allant sur atmo-occitanie.org #QualiteAir. ' . $message;
|
||||
$item['title'] .= ' #QualiteAir. ' . $message;
|
||||
$item['author'] = 'floviolleau';
|
||||
$item['content'] = $message;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
@ -1,344 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AuctionetBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Auctionet';
|
||||
const URI = 'https://www.auctionet.com';
|
||||
const DESCRIPTION = 'Fetches info about auction objects from Auctionet (an auction platform for many European auction houses)';
|
||||
const MAINTAINER = 'Qluxzz';
|
||||
const PARAMETERS = [[
|
||||
'category' => [
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All categories' => '',
|
||||
'Art' => [
|
||||
'All' => '25-art',
|
||||
'Drawings' => '119-drawings',
|
||||
'Engravings & Prints' => '27-engravings-prints',
|
||||
'Other' => '30-other',
|
||||
'Paintings' => '28-paintings',
|
||||
'Photography' => '26-photography',
|
||||
'Sculptures & Bronzes' => '29-sculptures-bronzes',
|
||||
],
|
||||
'Asiatica' => [
|
||||
'All' => '117-asiatica',
|
||||
],
|
||||
'Books, Maps & Manuscripts' => [
|
||||
'All' => '50-books-maps-manuscripts',
|
||||
'Autographs & Manuscripts' => '206-autographs-manuscripts',
|
||||
'Books' => '204-books',
|
||||
'Maps' => '205-maps',
|
||||
'Other' => '207-other',
|
||||
],
|
||||
'Carpets & Textiles' => [
|
||||
'All' => '35-carpets-textiles',
|
||||
'Carpets' => '36-carpets',
|
||||
'Textiles' => '37-textiles',
|
||||
],
|
||||
'Ceramics & Porcelain' => [
|
||||
'All' => '9-ceramics-porcelain',
|
||||
'European' => '10-european',
|
||||
'Oriental' => '11-oriental',
|
||||
'Rest of the world' => '12-rest-of-the-world',
|
||||
'Tableware' => '210-tableware',
|
||||
],
|
||||
'Clocks & Watches' => [
|
||||
'All' => '31-clocks-watches',
|
||||
'Carriage & Miniature Clocks' => '258-carriage-miniature-clocks',
|
||||
'Longcase clocks' => '32-longcase-clocks',
|
||||
'Mantel clocks' => '33-mantel-clocks',
|
||||
'Other clocks' => '34-other-clocks',
|
||||
'Pocket & Stop Watches' => '110-pocket-stop-watches',
|
||||
'Wall Clocks' => '127-wall-clocks',
|
||||
'Wristwatches' => '15-wristwatches',
|
||||
],
|
||||
'Coins, Medals & Stamps' => [
|
||||
'All' => '46-coins-medals-stamps',
|
||||
'Coins' => '128-coins',
|
||||
'Orders & Medals' => '135-orders-medals',
|
||||
'Other' => '131-other',
|
||||
'Stamps' => '136-stamps',
|
||||
],
|
||||
'Folk art' => [
|
||||
'All' => '58-folk-art',
|
||||
'Bowls & Boxes' => '121-bowls-boxes',
|
||||
'Furniture' => '122-furniture',
|
||||
'Other' => '123-other',
|
||||
'Tools & Gears' => '120-tools-gears',
|
||||
],
|
||||
'Furniture' => [
|
||||
'All' => '16-furniture',
|
||||
'Armchairs & Chairs' => '18-armchairs-chairs',
|
||||
'Chests of drawers' => '24-chests-of-drawers',
|
||||
'Cupboards, Cabinets & Shelves' => '23-cupboards-cabinets-shelves',
|
||||
'Dining room furniture' => '22-dining-room-furniture',
|
||||
'Garden' => '21-garden',
|
||||
'Other' => '17-other',
|
||||
'Sofas & seatings' => '20-sofas-seatings',
|
||||
'Tables' => '19-tables',
|
||||
],
|
||||
'Glass' => [
|
||||
'All' => '6-glass',
|
||||
'Art glass' => '208-art-glass',
|
||||
'Other' => '8-other',
|
||||
'Tableware' => '7-tableware',
|
||||
'Utility glass' => '209-utility-glass',
|
||||
],
|
||||
'Jewellery & Gemstones' => [
|
||||
'All' => '13-jewellery-gemstones',
|
||||
'Alliance rings' => '113-alliance-rings',
|
||||
'Bracelets' => '106-bracelets',
|
||||
'Brooches & Pendants' => '107-brooches-pendants',
|
||||
'Costume Jewellery' => '259-costume-jewellery',
|
||||
'Cufflinks & Tie Pins' => '111-cufflinks-tie-pins',
|
||||
'Ear studs' => '116-ear-studs',
|
||||
'Earrings' => '115-earrings',
|
||||
'Gemstones' => '48-gemstones',
|
||||
'Jewellery' => '14-jewellery',
|
||||
'Jewellery Suites' => '109-jewellery-suites',
|
||||
'Necklace' => '104-necklace',
|
||||
'Other' => '118-other',
|
||||
'Rings' => '112-rings',
|
||||
'Signet rings' => '105-signet-rings',
|
||||
'Solitaire rings' => '114-solitaire-rings',
|
||||
],
|
||||
'Licence weapons' => [
|
||||
'All' => '59-licence-weapons',
|
||||
'Combi/Combo' => '63-combi-combo',
|
||||
'Double express rifles' => '60-double-express-rifles',
|
||||
'Rifles' => '61-rifles',
|
||||
'Shotguns' => '62-shotguns',
|
||||
],
|
||||
'Lighting & Lamps' => [
|
||||
'All' => '1-lighting-lamps',
|
||||
'Candlesticks' => '4-candlesticks',
|
||||
'Ceiling lights' => '3-ceiling-lights',
|
||||
'Chandeliers' => '203-chandeliers',
|
||||
'Floor lights' => '2-floor-lights',
|
||||
'Other lighting' => '5-other-lighting',
|
||||
'Table Lamps' => '125-table-lamps',
|
||||
'Wall Lights' => '124-wall-lights',
|
||||
],
|
||||
'Mirrors' => [
|
||||
'All' => '42-mirrors',
|
||||
],
|
||||
'Miscellaneous' => [
|
||||
'All' => '43-miscellaneous',
|
||||
'Fishing equipment' => '54-fishing-equipment',
|
||||
'Miscellaneous' => '47-miscellaneous',
|
||||
'Modern Tools' => '133-modern-tools',
|
||||
'Modern consumer electronics' => '52-modern-consumer-electronics',
|
||||
'Musical instruments' => '51-musical-instruments',
|
||||
'Technica & Nautica' => '45-technica-nautica',
|
||||
],
|
||||
'Photo, Cameras & Lenses' => [
|
||||
'All' => '57-photo-cameras-lenses',
|
||||
'Cameras & accessories' => '71-cameras-accessories',
|
||||
'Optics' => '66-optics',
|
||||
'Other' => '72-other',
|
||||
],
|
||||
'Silver & Metals' => [
|
||||
'All' => '38-silver-metals',
|
||||
'Other metals' => '40-other-metals',
|
||||
'Pewter, Brass & Copper' => '41-pewter-brass-copper',
|
||||
'Silver' => '39-silver',
|
||||
'Silver plated' => '213-silver-plated',
|
||||
],
|
||||
'Toys' => [
|
||||
'All' => '44-toys',
|
||||
'Comics' => '211-comics',
|
||||
'Toys' => '212-toys',
|
||||
],
|
||||
'Tribal art' => [
|
||||
'All' => '134-tribal-art',
|
||||
],
|
||||
'Vehicles, Boats & Parts' => [
|
||||
'All' => '249-vehicles-boats-parts',
|
||||
'Automobilia & Transport' => '255-automobilia-transport',
|
||||
'Bicycles' => '132-bicycles',
|
||||
'Boats & Accessories' => '250-boats-accessories',
|
||||
'Car parts' => '253-car-parts',
|
||||
'Cars' => '215-cars',
|
||||
'Moped parts' => '254-moped-parts',
|
||||
'Mopeds' => '216-mopeds',
|
||||
'Motorcycle parts' => '252-motorcycle-parts',
|
||||
'Motorcycles' => '251-motorcycles',
|
||||
'Other' => '256-other',
|
||||
],
|
||||
'Vintage & Designer Fashion' => [
|
||||
'All' => '49-vintage-designer-fashion',
|
||||
],
|
||||
'Weapons & Militaria' => [
|
||||
'All' => '137-weapons-militaria',
|
||||
'Airguns' => '257-airguns',
|
||||
'Armour & Uniform' => '138-armour-uniform',
|
||||
'Edged weapons' => '130-edged-weapons',
|
||||
'Guns & Rifles' => '129-guns-rifles',
|
||||
'Other' => '214-other',
|
||||
],
|
||||
'Wine, Port & Spirits' => [
|
||||
'All' => '170-wine-port-spirits',
|
||||
],
|
||||
]
|
||||
],
|
||||
'sort_order' => [
|
||||
'name' => 'Sort order',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Most bids' => 'bids_count_desc',
|
||||
'Lowest bid' => 'bid_asc',
|
||||
'Highest bid' => 'bid_desc',
|
||||
'Last bid on' => 'bid_on',
|
||||
'Ending soonest' => 'end_asc_active',
|
||||
'Lowest estimate' => 'estimate_asc',
|
||||
'Highest estimate' => 'estimate_desc',
|
||||
'Recently added' => 'recent'
|
||||
],
|
||||
],
|
||||
'country' => [
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All' => '',
|
||||
'Denmark' => 'DK',
|
||||
'Finland' => 'FI',
|
||||
'Germany' => 'DE',
|
||||
'Spain' => 'ES',
|
||||
'Sweden' => 'SE',
|
||||
'United Kingdom' => 'GB'
|
||||
]
|
||||
],
|
||||
'language' => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'English' => 'en',
|
||||
'Español' => 'es',
|
||||
'Deutsch' => 'de',
|
||||
'Svenska' => 'sv',
|
||||
'Dansk' => 'da',
|
||||
'Suomi' => 'fi',
|
||||
],
|
||||
],
|
||||
]];
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
private $title;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
// Each page contains 48 auctions
|
||||
// So we fetch 10 pages so we decrease the likelihood
|
||||
// of missing auctions between feed refreshes
|
||||
|
||||
// Fetch first page and use that to get title
|
||||
{
|
||||
$url = $this->getUrl(1);
|
||||
$data = getContents($url);
|
||||
|
||||
$title = $this->getDocumentTitle($data);
|
||||
|
||||
$this->items = array_merge($this->items, $this->parsePageData($data));
|
||||
}
|
||||
|
||||
// Fetch remaining pages
|
||||
for ($page = 2; $page <= 10; $page++) {
|
||||
$url = $this->getUrl($page);
|
||||
|
||||
$data = getContents($url);
|
||||
|
||||
$this->items = array_merge($this->items, $this->parsePageData($data));
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->title ?: parent::getName();
|
||||
}
|
||||
|
||||
|
||||
/* HELPERS */
|
||||
|
||||
private function getUrl($page)
|
||||
{
|
||||
$category = $this->getInput('category');
|
||||
$language = $this->getInput('language');
|
||||
$sort_order = $this->getInput('sort_order');
|
||||
$country = $this->getInput('country');
|
||||
|
||||
$url = self::URI . '/' . $language . '/search';
|
||||
|
||||
if ($category) {
|
||||
$url = $url . '/' . $category;
|
||||
}
|
||||
|
||||
$query = [];
|
||||
$query['page'] = $page;
|
||||
|
||||
if ($sort_order) {
|
||||
$query['order'] = $sort_order;
|
||||
}
|
||||
|
||||
if ($country) {
|
||||
$query['country_code'] = $country;
|
||||
}
|
||||
|
||||
if (count($query) > 0) {
|
||||
$url = $url . '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function getDocumentTitle($data)
|
||||
{
|
||||
$title_elem = '<title>';
|
||||
$title_elem_length = strlen($title_elem);
|
||||
$title_start = strpos($data, $title_elem);
|
||||
$title_end = strpos($data, '</title>', $title_start);
|
||||
$title_length = $title_end - $title_start + strlen($title_elem);
|
||||
$title = substr($data, $title_start + strlen($title_elem), $title_length);
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* The auction items data is included in the HTML document
|
||||
* as a HTML entities encoded JSON structure
|
||||
* which is used to hydrate the React component for the list of auctions
|
||||
*/
|
||||
private function parsePageData($data)
|
||||
{
|
||||
$key = 'data-react-props="';
|
||||
$keyLength = strlen($key);
|
||||
|
||||
$start = strpos($data, $key);
|
||||
$end = strpos($data, '"', $start + strlen($key));
|
||||
$length = $end - ($start + $keyLength);
|
||||
|
||||
$jsonString = substr($data, $start + $keyLength, $length);
|
||||
|
||||
$jsonData = json_decode(htmlspecialchars_decode($jsonString), false);
|
||||
|
||||
$items = [];
|
||||
|
||||
foreach ($jsonData->{'items'} as $item) {
|
||||
$title = $item->{'longTitle'};
|
||||
$relative_url = $item->{'url'};
|
||||
$images = $item->{'imageUrls'};
|
||||
$id = $item->{'auctionId'};
|
||||
|
||||
$items[] = [
|
||||
'title' => $title,
|
||||
'uri' => self::URI . $relative_url,
|
||||
'uid' => $id,
|
||||
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
|
||||
'enclosures' => array_slice($images, 1),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
@ -1,168 +1,65 @@
|
||||
<?php
|
||||
|
||||
class AutoJMBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'AutoJM';
|
||||
const URI = 'https://www.autojm.fr/';
|
||||
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
|
||||
const MAINTAINER = 'sysadminstory';
|
||||
const PARAMETERS = [
|
||||
'Afficher les offres de véhicules disponible sur la recheche AutoJM' => [
|
||||
'url' => [
|
||||
'name' => 'URL de la page de recherche',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
|
||||
'exampleValue' => 'recherche?brands[]=PEUGEOT&ranges[]=PEUGEOT 308'
|
||||
],
|
||||
]
|
||||
];
|
||||
class AutoJMBridge extends BridgeAbstract {
|
||||
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
const NAME = 'AutoJM';
|
||||
const URI = 'http://www.autojm.fr/';
|
||||
const DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
|
||||
const MAINTAINER = 'sysadminstory';
|
||||
const PARAMETERS = array(
|
||||
'Afficher les offres de véhicules disponible en fonction des critères du site AutoJM' => array(
|
||||
'url' => array(
|
||||
'name' => 'URL de la recherche',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
|
||||
'exampleValue' => 'gammes/index/398?order_by=finition_asc&energie[]=3&transmission[]=2&dispo=all'
|
||||
)
|
||||
)
|
||||
);
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
const TEST_DETECT_PARAMETERS = [
|
||||
'https://www.autojm.fr/recherche?brands%5B%5D=PEUGEOT&ranges%5B%5D=PEUGEOT%20308'
|
||||
=> ['url' => 'recherche?brands%5B%5D=PEUGEOT&ranges%5B%5D=PEUGEOT%20308',
|
||||
'context' => 'Afficher les offres de véhicules disponible sur la recheche AutoJM'
|
||||
]
|
||||
];
|
||||
public function getIcon() {
|
||||
return self::URI . 'assets/images/favicon.ico';
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return self::URI . 'favicon.ico';
|
||||
}
|
||||
public function collectData() {
|
||||
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'))
|
||||
or returnServerError('Could not request AutoJM.');
|
||||
$list = $html->find('div[class*=ligne_modele]');
|
||||
foreach($list as $element) {
|
||||
$image = $element->find('img[class=width-100]', 0)->src;
|
||||
$serie = $element->find('div[class=serie]', 0)->find('span', 0)->plaintext;
|
||||
$url = $element->find('div[class=serie]', 0)->find('a[class=btn_ligne color-black]', 0)->href;
|
||||
if($element->find('div[class*=hasStock-info]', 0) != null) {
|
||||
$dispo = 'Disponible';
|
||||
} else {
|
||||
$dispo = 'Sur commande';
|
||||
}
|
||||
$carburant = str_replace('dispo |', '', $element->find('div[class=carburant]', 0)->plaintext);
|
||||
$transmission = $element->find('div[class*=bv]', 0)->plaintext;
|
||||
$places = $element->find('div[class*=places]', 0)->plaintext;
|
||||
$portes = $element->find('div[class*=nb_portes]', 0)->plaintext;
|
||||
$carosserie = $element->find('div[class*=coloris]', 0)->plaintext;
|
||||
$remise = $element->find('div[class*=remise]', 0)->plaintext;
|
||||
$prix = $element->find('div[class*=prixjm]', 0)->plaintext;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Afficher les offres de véhicules disponible sur la recheche AutoJM':
|
||||
return 'AutoJM | Recherche de véhicules';
|
||||
break;
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
$item = array();
|
||||
$item['uri'] = $url;
|
||||
$item['title'] = $serie;
|
||||
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' . $serie . '</p>';
|
||||
$item['content'] .= '<ul><li>Disponibilité : ' . $dispo . '</li>';
|
||||
$item['content'] .= '<li>Carburant : ' . $carburant . '</li>';
|
||||
$item['content'] .= '<li>Transmission : ' . $transmission . '</li>';
|
||||
$item['content'] .= '<li>Nombre de places : ' . $places . '</li>';
|
||||
$item['content'] .= '<li>Nombre de portes : ' . $portes . '</li>';
|
||||
$item['content'] .= '<li>Série : ' . $serie . '</li>';
|
||||
$item['content'] .= '<li>Carosserie : ' . $carosserie . '</li>';
|
||||
$item['content'] .= '<li>Remise : ' . $remise . '</li>';
|
||||
$item['content'] .= '<li>Prix : ' . $prix . '</li></ul>';
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Afficher les offres de véhicules disponible sur la recheche AutoJM':
|
||||
return self::URI . $this->getInput('url');
|
||||
break;
|
||||
default:
|
||||
return self::URI;
|
||||
}
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
// Get the number of result for this search
|
||||
$search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false';
|
||||
|
||||
// Set the header 'X-Requested-With' like the website does it
|
||||
$header = [
|
||||
'X-Requested-With: XMLHttpRequest'
|
||||
];
|
||||
|
||||
// Get the JSON content of the form
|
||||
$json = getContents($search_url, $header);
|
||||
|
||||
// Extract the HTML content from the JSON result
|
||||
$data = json_decode($json);
|
||||
|
||||
$nb_results = $data->nbResults;
|
||||
$total_pages = ceil($nb_results / 14);
|
||||
|
||||
// Limit the number of page to analyse to 10
|
||||
for ($page = 1; $page <= $total_pages && $page <= 10; $page++) {
|
||||
// Get the result the next page
|
||||
$html = $this->getResults($page);
|
||||
|
||||
// Go through every car of the search
|
||||
$list = $html->find('div[class*=card-car card-car--listing]');
|
||||
foreach ($list as $car) {
|
||||
// Get the info about the car offer
|
||||
$image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src;
|
||||
// Decode HTML attribute JSON data
|
||||
$car_data = json_decode(html_entity_decode($car->{'data-layer'}));
|
||||
$car_model = $car_data->title;
|
||||
$availability = $car->find('div[class*=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext;
|
||||
$warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext;
|
||||
$discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0);
|
||||
// Check if there is any discount info displayed
|
||||
if ($discount_html != null) {
|
||||
$reference_price_value = $discount_html->find('span[data-cfg=vehicle__reference_price]', 0)->plaintext;
|
||||
$discount_percent_value = $discount_html->find('span[data-cfg=vehicle__discount_percent]', 0)->plaintext;
|
||||
$reference_price = '<li>Prix de référence : <s>' . $reference_price_value . '</s></li>';
|
||||
$discount_percent = '<li>Réduction : ' . $discount_percent_value . ' %</li>';
|
||||
} else {
|
||||
$reference_price = '';
|
||||
$discount_percent = '';
|
||||
}
|
||||
$price = $car_data->price;
|
||||
$kilometer = $car->find('span[data-cfg=vehicle__kilometer]', 0)->plaintext;
|
||||
$energy = $car->find('span[data-cfg=vehicle__energy__label]', 0)->plaintext;
|
||||
$power = $car->find('span[data-cfg=vehicle__tax_horse_power]', 0)->plaintext;
|
||||
$seats = $car->find('span[data-cfg=vehicle__seats]', 0)->plaintext;
|
||||
$doors = $car->find('span[data-cfg=vehicle__door__label]', 0)->plaintext;
|
||||
$transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext;
|
||||
$loa_html = $car->find('span[data-cfg=vehicle__loa]', 0);
|
||||
// Check if any LOA price is displayed
|
||||
if ($loa_html != null) {
|
||||
$loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext;
|
||||
$loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>';
|
||||
} else {
|
||||
$loa = '';
|
||||
}
|
||||
|
||||
// Construct the new item
|
||||
$item = [];
|
||||
$item['title'] = $car_model;
|
||||
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
|
||||
. $car_model . '</p>';
|
||||
$item['content'] .= '<ul><li>Disponibilité : ' . $availability . '</li>';
|
||||
$item['content'] .= '<li>Prix : ' . $price . ' €</li>';
|
||||
$item['content'] .= $reference_price;
|
||||
$item['content'] .= $loa;
|
||||
$item['content'] .= $discount_percent;
|
||||
$item['content'] .= '<li>Garantie : ' . $warranty . '</li>';
|
||||
$item['content'] .= '<li>Kilométrage : ' . $kilometer . ' km</li>';
|
||||
$item['content'] .= '<li>Energie : ' . $energy . '</li>';
|
||||
$item['content'] .= '<li>Puissance: ' . $power . ' CV Fiscaux</li>';
|
||||
$item['content'] .= '<li>Nombre de Places : ' . $seats . ' place(s)</li>';
|
||||
$item['content'] .= '<li>Nombre de portes : ' . $doors . '</li>';
|
||||
$item['content'] .= '<li>Boite de vitesse : ' . $transmission . '</li></ul>';
|
||||
$item['uri'] = $car_data->{'uri'};
|
||||
$item['uid'] = hash('md5', $item['content']);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getResults(int $page)
|
||||
{
|
||||
$user_input = $this->getInput('url');
|
||||
$search_data = preg_replace('#(recherche|recherche/[0-9]{1,10})\?#', 'recherche/' . $page . '?', $user_input);
|
||||
|
||||
$search_url = self::URI . $search_data . '&open=energy&onlyFilters=false';
|
||||
|
||||
// Get the HTML content of the page
|
||||
$html = getSimpleHTMLDOMCached($search_url);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function detectParameters($url)
|
||||
{
|
||||
$params = [];
|
||||
$regex = '/^(https?:\/\/)?(www\.|)autojm.fr\/(recherche\?.*|recherche\/[0-9]{1,10}\?.*)$/m';
|
||||
if (preg_match($regex, $url, $matches) > 0) {
|
||||
$url = preg_replace('#(recherche|recherche/[0-9]{1,10})#', 'recherche', $matches[3]);
|
||||
|
||||
$params['url'] = $url;
|
||||
$params['context'] = 'Afficher les offres de véhicules disponible sur la recheche AutoJM';
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
class AwwwardsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Awwwards';
|
||||
const URI = 'https://www.awwwards.com/';
|
||||
const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards';
|
||||
const MAINTAINER = 'Paroleen';
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
const SITESURI = 'https://www.awwwards.com/websites/sites_of_the_day/';
|
||||
const SITEURI = 'https://www.awwwards.com/sites/';
|
||||
const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/';
|
||||
|
||||
private $sites = [];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$this->fetchSites();
|
||||
|
||||
foreach ($this->sites as $site) {
|
||||
$item = [];
|
||||
$item['title'] = $site['title'];
|
||||
$item['timestamp'] = $site['createdAt'];
|
||||
$item['categories'] = $site['tags'];
|
||||
|
||||
$item['content'] = '<img src="'
|
||||
. self::ASSETSURI
|
||||
. $site['images']['thumbnail']
|
||||
. '">';
|
||||
$item['uri'] = self::SITEURI . $site['slug'];
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
if (count($this->items) >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://www.awwwards.com/favicon.ico';
|
||||
}
|
||||
|
||||
private function fetchSites()
|
||||
{
|
||||
$sites = getSimpleHTMLDOM(self::SITESURI);
|
||||
foreach ($sites->find('.grid-sites li') as $li) {
|
||||
$encodedJson = $li->attr['data-collectable-model-value'] ?? null;
|
||||
if (!$encodedJson) {
|
||||
continue;
|
||||
}
|
||||
$json = html_entity_decode($encodedJson, ENT_QUOTES, 'utf-8');
|
||||
$site = Json::decode($json);
|
||||
$this->sites[] = $site;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,269 +1,265 @@
|
||||
<?php
|
||||
class BAEBridge extends BridgeAbstract {
|
||||
const MAINTAINER = 'couraudt';
|
||||
const NAME = 'Bourse Aux Equipiers Bridge';
|
||||
const URI = 'https://www.bourse-aux-equipiers.com';
|
||||
const DESCRIPTION = 'Returns the newest sailing offers.';
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
'keyword' => array(
|
||||
'name' => 'Filtrer par mots clés',
|
||||
'title' => 'Entrez le mot clé à filtrer ici'
|
||||
),
|
||||
'type' => array(
|
||||
'name' => 'Type de recherche',
|
||||
'title' => 'Afficher seuleument un certain type d\'annonce',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'Toutes les annonces' => false,
|
||||
'Les embarquements' => 'boat',
|
||||
'Les skippers' => 'skipper',
|
||||
'Les équipiers' => 'crew'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
class BAEBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'couraudt';
|
||||
const NAME = 'Bourse Aux Equipiers Bridge';
|
||||
const URI = 'https://www.bourse-aux-equipiers.com';
|
||||
const DESCRIPTION = 'Returns the newest sailing offers.';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'keyword' => [
|
||||
'name' => 'Filtrer par mots clés',
|
||||
'title' => 'Entrez le mot clé à filtrer ici'
|
||||
],
|
||||
'type' => [
|
||||
'name' => 'Type de recherche',
|
||||
'title' => 'Afficher seuleument un certain type d\'annonce',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Toutes les annonces' => false,
|
||||
'Les embarquements' => 'boat',
|
||||
'Les skippers' => 'skipper',
|
||||
'Les équipiers' => 'crew'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
public function collectData() {
|
||||
$url = $this->getURI();
|
||||
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getURI();
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
$annonces = $html->find('main article');
|
||||
foreach ($annonces as $annonce) {
|
||||
$detail = $annonce->find('footer a', 0);
|
||||
|
||||
$annonces = $html->find('main article');
|
||||
foreach ($annonces as $annonce) {
|
||||
$detail = $annonce->find('footer a', 0);
|
||||
$htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href);
|
||||
if (!$htmlDetail)
|
||||
continue;
|
||||
|
||||
$htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href);
|
||||
if (!$htmlDetail) {
|
||||
continue;
|
||||
}
|
||||
$item = array();
|
||||
|
||||
$item = [];
|
||||
$item['title'] = $annonce->find('header h2', 0)->plaintext;
|
||||
$item['uri'] = parent::getURI() . $detail->href;
|
||||
|
||||
$item['title'] = $annonce->find('header h2', 0)->plaintext;
|
||||
$item['uri'] = parent::getURI() . $detail->href;
|
||||
$content = $htmlDetail->find('article p', 0)->innertext;
|
||||
if (!empty($this->getInput('keyword'))) {
|
||||
$keyword = $this->remove_accents(strtolower($this->getInput('keyword')));
|
||||
$cleanTitle = $this->remove_accents(strtolower($item['title']));
|
||||
if (strpos($cleanTitle, $keyword) === false) {
|
||||
$cleanContent = $this->remove_accents(strtolower($content));
|
||||
if (strpos($cleanContent, $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$content = $htmlDetail->find('article p', 0)->innertext;
|
||||
if (!empty($this->getInput('keyword'))) {
|
||||
$keyword = $this->removeAccents(strtolower($this->getInput('keyword')));
|
||||
$cleanTitle = $this->removeAccents(strtolower($item['title']));
|
||||
if (strpos($cleanTitle, $keyword) === false) {
|
||||
$cleanContent = $this->removeAccents(strtolower($content));
|
||||
if (strpos($cleanContent, $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
$content .= '<hr>';
|
||||
$content .= $htmlDetail->find('section', 0)->innertext;
|
||||
$content = str_replace('src="/', 'src="' . parent::getURI() . '/', $content);
|
||||
$content = str_replace('href="/', 'href="' . parent::getURI() . '/', $content);
|
||||
$item['content'] = $content;
|
||||
$image = $htmlDetail->find('#zoom', 0);
|
||||
if ($image) {
|
||||
$item['enclosures'] = array(parent::getURI() . $image->getAttribute('src'));
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$content .= '<hr>';
|
||||
$content .= $htmlDetail->find('section', 0)->innertext;
|
||||
$item['content'] = defaultLinkTo($content, parent::getURI());
|
||||
$image = $htmlDetail->find('#zoom', 0);
|
||||
if ($image) {
|
||||
$item['enclosures'] = [parent::getURI() . $image->getAttribute('src')];
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
public function getURI() {
|
||||
$uri = parent::getURI();
|
||||
if (!empty($this->getInput('type'))) {
|
||||
if ($this->getInput('type') == 'boat') {
|
||||
$uri .= '/embarquements.html';
|
||||
} elseif ($this->getInput('type') == 'skipper') {
|
||||
$uri .= '/skippers.html';
|
||||
} else {
|
||||
$uri .= '/equipiers.html';
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
$uri = parent::getURI();
|
||||
if (!empty($this->getInput('type'))) {
|
||||
if ($this->getInput('type') == 'boat') {
|
||||
$uri .= '/embarquements.html';
|
||||
} elseif ($this->getInput('type') == 'skipper') {
|
||||
$uri .= '/skippers.html';
|
||||
} else {
|
||||
$uri .= '/equipiers.html';
|
||||
}
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
private function remove_accents($string) {
|
||||
$chars = array(
|
||||
// Decompositions for Latin-1 Supplement
|
||||
'ª' => 'a', 'º' => 'o',
|
||||
'À' => 'A', 'Á' => 'A',
|
||||
'Â' => 'A', 'Ã' => 'A',
|
||||
'Ä' => 'A', 'Å' => 'A',
|
||||
'Æ' => 'AE', 'Ç' => 'C',
|
||||
'È' => 'E', 'É' => 'E',
|
||||
'Ê' => 'E', 'Ë' => 'E',
|
||||
'Ì' => 'I', 'Í' => 'I',
|
||||
'Î' => 'I', 'Ï' => 'I',
|
||||
'Ð' => 'D', 'Ñ' => 'N',
|
||||
'Ò' => 'O', 'Ó' => 'O',
|
||||
'Ô' => 'O', 'Õ' => 'O',
|
||||
'Ö' => 'O', 'Ù' => 'U',
|
||||
'Ú' => 'U', 'Û' => 'U',
|
||||
'Ü' => 'U', 'Ý' => 'Y',
|
||||
'Þ' => 'TH', 'ß' => 's',
|
||||
'à' => 'a', 'á' => 'a',
|
||||
'â' => 'a', 'ã' => 'a',
|
||||
'ä' => 'a', 'å' => 'a',
|
||||
'æ' => 'ae', 'ç' => 'c',
|
||||
'è' => 'e', 'é' => 'e',
|
||||
'ê' => 'e', 'ë' => 'e',
|
||||
'ì' => 'i', 'í' => 'i',
|
||||
'î' => 'i', 'ï' => 'i',
|
||||
'ð' => 'd', 'ñ' => 'n',
|
||||
'ò' => 'o', 'ó' => 'o',
|
||||
'ô' => 'o', 'õ' => 'o',
|
||||
'ö' => 'o', 'ø' => 'o',
|
||||
'ù' => 'u', 'ú' => 'u',
|
||||
'û' => 'u', 'ü' => 'u',
|
||||
'ý' => 'y', 'þ' => 'th',
|
||||
'ÿ' => 'y', 'Ø' => 'O',
|
||||
// Decompositions for Latin Extended-A
|
||||
'Ā' => 'A', 'ā' => 'a',
|
||||
'Ă' => 'A', 'ă' => 'a',
|
||||
'Ą' => 'A', 'ą' => 'a',
|
||||
'Ć' => 'C', 'ć' => 'c',
|
||||
'Ĉ' => 'C', 'ĉ' => 'c',
|
||||
'Ċ' => 'C', 'ċ' => 'c',
|
||||
'Č' => 'C', 'č' => 'c',
|
||||
'Ď' => 'D', 'ď' => 'd',
|
||||
'Đ' => 'D', 'đ' => 'd',
|
||||
'Ē' => 'E', 'ē' => 'e',
|
||||
'Ĕ' => 'E', 'ĕ' => 'e',
|
||||
'Ė' => 'E', 'ė' => 'e',
|
||||
'Ę' => 'E', 'ę' => 'e',
|
||||
'Ě' => 'E', 'ě' => 'e',
|
||||
'Ĝ' => 'G', 'ĝ' => 'g',
|
||||
'Ğ' => 'G', 'ğ' => 'g',
|
||||
'Ġ' => 'G', 'ġ' => 'g',
|
||||
'Ģ' => 'G', 'ģ' => 'g',
|
||||
'Ĥ' => 'H', 'ĥ' => 'h',
|
||||
'Ħ' => 'H', 'ħ' => 'h',
|
||||
'Ĩ' => 'I', 'ĩ' => 'i',
|
||||
'Ī' => 'I', 'ī' => 'i',
|
||||
'Ĭ' => 'I', 'ĭ' => 'i',
|
||||
'Į' => 'I', 'į' => 'i',
|
||||
'İ' => 'I', 'ı' => 'i',
|
||||
'IJ' => 'IJ', 'ij' => 'ij',
|
||||
'Ĵ' => 'J', 'ĵ' => 'j',
|
||||
'Ķ' => 'K', 'ķ' => 'k',
|
||||
'ĸ' => 'k', 'Ĺ' => 'L',
|
||||
'ĺ' => 'l', 'Ļ' => 'L',
|
||||
'ļ' => 'l', 'Ľ' => 'L',
|
||||
'ľ' => 'l', 'Ŀ' => 'L',
|
||||
'ŀ' => 'l', 'Ł' => 'L',
|
||||
'ł' => 'l', 'Ń' => 'N',
|
||||
'ń' => 'n', 'Ņ' => 'N',
|
||||
'ņ' => 'n', 'Ň' => 'N',
|
||||
'ň' => 'n', 'ʼn' => 'n',
|
||||
'Ŋ' => 'N', 'ŋ' => 'n',
|
||||
'Ō' => 'O', 'ō' => 'o',
|
||||
'Ŏ' => 'O', 'ŏ' => 'o',
|
||||
'Ő' => 'O', 'ő' => 'o',
|
||||
'Œ' => 'OE', 'œ' => 'oe',
|
||||
'Ŕ' => 'R', 'ŕ' => 'r',
|
||||
'Ŗ' => 'R', 'ŗ' => 'r',
|
||||
'Ř' => 'R', 'ř' => 'r',
|
||||
'Ś' => 'S', 'ś' => 's',
|
||||
'Ŝ' => 'S', 'ŝ' => 's',
|
||||
'Ş' => 'S', 'ş' => 's',
|
||||
'Š' => 'S', 'š' => 's',
|
||||
'Ţ' => 'T', 'ţ' => 't',
|
||||
'Ť' => 'T', 'ť' => 't',
|
||||
'Ŧ' => 'T', 'ŧ' => 't',
|
||||
'Ũ' => 'U', 'ũ' => 'u',
|
||||
'Ū' => 'U', 'ū' => 'u',
|
||||
'Ŭ' => 'U', 'ŭ' => 'u',
|
||||
'Ů' => 'U', 'ů' => 'u',
|
||||
'Ű' => 'U', 'ű' => 'u',
|
||||
'Ų' => 'U', 'ų' => 'u',
|
||||
'Ŵ' => 'W', 'ŵ' => 'w',
|
||||
'Ŷ' => 'Y', 'ŷ' => 'y',
|
||||
'Ÿ' => 'Y', 'Ź' => 'Z',
|
||||
'ź' => 'z', 'Ż' => 'Z',
|
||||
'ż' => 'z', 'Ž' => 'Z',
|
||||
'ž' => 'z', 'ſ' => 's',
|
||||
// Decompositions for Latin Extended-B
|
||||
'Ș' => 'S', 'ș' => 's',
|
||||
'Ț' => 'T', 'ț' => 't',
|
||||
// Euro Sign
|
||||
'€' => 'E',
|
||||
// GBP (Pound) Sign
|
||||
'£' => '',
|
||||
// Vowels with diacritic (Vietnamese)
|
||||
// unmarked
|
||||
'Ơ' => 'O', 'ơ' => 'o',
|
||||
'Ư' => 'U', 'ư' => 'u',
|
||||
// grave accent
|
||||
'Ầ' => 'A', 'ầ' => 'a',
|
||||
'Ằ' => 'A', 'ằ' => 'a',
|
||||
'Ề' => 'E', 'ề' => 'e',
|
||||
'Ồ' => 'O', 'ồ' => 'o',
|
||||
'Ờ' => 'O', 'ờ' => 'o',
|
||||
'Ừ' => 'U', 'ừ' => 'u',
|
||||
'Ỳ' => 'Y', 'ỳ' => 'y',
|
||||
// hook
|
||||
'Ả' => 'A', 'ả' => 'a',
|
||||
'Ẩ' => 'A', 'ẩ' => 'a',
|
||||
'Ẳ' => 'A', 'ẳ' => 'a',
|
||||
'Ẻ' => 'E', 'ẻ' => 'e',
|
||||
'Ể' => 'E', 'ể' => 'e',
|
||||
'Ỉ' => 'I', 'ỉ' => 'i',
|
||||
'Ỏ' => 'O', 'ỏ' => 'o',
|
||||
'Ổ' => 'O', 'ổ' => 'o',
|
||||
'Ở' => 'O', 'ở' => 'o',
|
||||
'Ủ' => 'U', 'ủ' => 'u',
|
||||
'Ử' => 'U', 'ử' => 'u',
|
||||
'Ỷ' => 'Y', 'ỷ' => 'y',
|
||||
// tilde
|
||||
'Ẫ' => 'A', 'ẫ' => 'a',
|
||||
'Ẵ' => 'A', 'ẵ' => 'a',
|
||||
'Ẽ' => 'E', 'ẽ' => 'e',
|
||||
'Ễ' => 'E', 'ễ' => 'e',
|
||||
'Ỗ' => 'O', 'ỗ' => 'o',
|
||||
'Ỡ' => 'O', 'ỡ' => 'o',
|
||||
'Ữ' => 'U', 'ữ' => 'u',
|
||||
'Ỹ' => 'Y', 'ỹ' => 'y',
|
||||
// acute accent
|
||||
'Ấ' => 'A', 'ấ' => 'a',
|
||||
'Ắ' => 'A', 'ắ' => 'a',
|
||||
'Ế' => 'E', 'ế' => 'e',
|
||||
'Ố' => 'O', 'ố' => 'o',
|
||||
'Ớ' => 'O', 'ớ' => 'o',
|
||||
'Ứ' => 'U', 'ứ' => 'u',
|
||||
// dot below
|
||||
'Ạ' => 'A', 'ạ' => 'a',
|
||||
'Ậ' => 'A', 'ậ' => 'a',
|
||||
'Ặ' => 'A', 'ặ' => 'a',
|
||||
'Ẹ' => 'E', 'ẹ' => 'e',
|
||||
'Ệ' => 'E', 'ệ' => 'e',
|
||||
'Ị' => 'I', 'ị' => 'i',
|
||||
'Ọ' => 'O', 'ọ' => 'o',
|
||||
'Ộ' => 'O', 'ộ' => 'o',
|
||||
'Ợ' => 'O', 'ợ' => 'o',
|
||||
'Ụ' => 'U', 'ụ' => 'u',
|
||||
'Ự' => 'U', 'ự' => 'u',
|
||||
'Ỵ' => 'Y', 'ỵ' => 'y',
|
||||
// Vowels with diacritic (Chinese, Hanyu Pinyin)
|
||||
'ɑ' => 'a',
|
||||
// macron
|
||||
'Ǖ' => 'U', 'ǖ' => 'u',
|
||||
// acute accent
|
||||
'Ǘ' => 'U', 'ǘ' => 'u',
|
||||
// caron
|
||||
'Ǎ' => 'A', 'ǎ' => 'a',
|
||||
'Ǐ' => 'I', 'ǐ' => 'i',
|
||||
'Ǒ' => 'O', 'ǒ' => 'o',
|
||||
'Ǔ' => 'U', 'ǔ' => 'u',
|
||||
'Ǚ' => 'U', 'ǚ' => 'u',
|
||||
// grave accent
|
||||
'Ǜ' => 'U', 'ǜ' => 'u',
|
||||
);
|
||||
|
||||
private function removeAccents($string)
|
||||
{
|
||||
$chars = [
|
||||
// Decompositions for Latin-1 Supplement
|
||||
'ª' => 'a', 'º' => 'o',
|
||||
'À' => 'A', 'Á' => 'A',
|
||||
'Â' => 'A', 'Ã' => 'A',
|
||||
'Ä' => 'A', 'Å' => 'A',
|
||||
'Æ' => 'AE', 'Ç' => 'C',
|
||||
'È' => 'E', 'É' => 'E',
|
||||
'Ê' => 'E', 'Ë' => 'E',
|
||||
'Ì' => 'I', 'Í' => 'I',
|
||||
'Î' => 'I', 'Ï' => 'I',
|
||||
'Ð' => 'D', 'Ñ' => 'N',
|
||||
'Ò' => 'O', 'Ó' => 'O',
|
||||
'Ô' => 'O', 'Õ' => 'O',
|
||||
'Ö' => 'O', 'Ù' => 'U',
|
||||
'Ú' => 'U', 'Û' => 'U',
|
||||
'Ü' => 'U', 'Ý' => 'Y',
|
||||
'Þ' => 'TH', 'ß' => 's',
|
||||
'à' => 'a', 'á' => 'a',
|
||||
'â' => 'a', 'ã' => 'a',
|
||||
'ä' => 'a', 'å' => 'a',
|
||||
'æ' => 'ae', 'ç' => 'c',
|
||||
'è' => 'e', 'é' => 'e',
|
||||
'ê' => 'e', 'ë' => 'e',
|
||||
'ì' => 'i', 'í' => 'i',
|
||||
'î' => 'i', 'ï' => 'i',
|
||||
'ð' => 'd', 'ñ' => 'n',
|
||||
'ò' => 'o', 'ó' => 'o',
|
||||
'ô' => 'o', 'õ' => 'o',
|
||||
'ö' => 'o', 'ø' => 'o',
|
||||
'ù' => 'u', 'ú' => 'u',
|
||||
'û' => 'u', 'ü' => 'u',
|
||||
'ý' => 'y', 'þ' => 'th',
|
||||
'ÿ' => 'y', 'Ø' => 'O',
|
||||
// Decompositions for Latin Extended-A
|
||||
'Ā' => 'A', 'ā' => 'a',
|
||||
'Ă' => 'A', 'ă' => 'a',
|
||||
'Ą' => 'A', 'ą' => 'a',
|
||||
'Ć' => 'C', 'ć' => 'c',
|
||||
'Ĉ' => 'C', 'ĉ' => 'c',
|
||||
'Ċ' => 'C', 'ċ' => 'c',
|
||||
'Č' => 'C', 'č' => 'c',
|
||||
'Ď' => 'D', 'ď' => 'd',
|
||||
'Đ' => 'D', 'đ' => 'd',
|
||||
'Ē' => 'E', 'ē' => 'e',
|
||||
'Ĕ' => 'E', 'ĕ' => 'e',
|
||||
'Ė' => 'E', 'ė' => 'e',
|
||||
'Ę' => 'E', 'ę' => 'e',
|
||||
'Ě' => 'E', 'ě' => 'e',
|
||||
'Ĝ' => 'G', 'ĝ' => 'g',
|
||||
'Ğ' => 'G', 'ğ' => 'g',
|
||||
'Ġ' => 'G', 'ġ' => 'g',
|
||||
'Ģ' => 'G', 'ģ' => 'g',
|
||||
'Ĥ' => 'H', 'ĥ' => 'h',
|
||||
'Ħ' => 'H', 'ħ' => 'h',
|
||||
'Ĩ' => 'I', 'ĩ' => 'i',
|
||||
'Ī' => 'I', 'ī' => 'i',
|
||||
'Ĭ' => 'I', 'ĭ' => 'i',
|
||||
'Į' => 'I', 'į' => 'i',
|
||||
'İ' => 'I', 'ı' => 'i',
|
||||
'IJ' => 'IJ', 'ij' => 'ij',
|
||||
'Ĵ' => 'J', 'ĵ' => 'j',
|
||||
'Ķ' => 'K', 'ķ' => 'k',
|
||||
'ĸ' => 'k', 'Ĺ' => 'L',
|
||||
'ĺ' => 'l', 'Ļ' => 'L',
|
||||
'ļ' => 'l', 'Ľ' => 'L',
|
||||
'ľ' => 'l', 'Ŀ' => 'L',
|
||||
'ŀ' => 'l', 'Ł' => 'L',
|
||||
'ł' => 'l', 'Ń' => 'N',
|
||||
'ń' => 'n', 'Ņ' => 'N',
|
||||
'ņ' => 'n', 'Ň' => 'N',
|
||||
'ň' => 'n', 'ʼn' => 'n',
|
||||
'Ŋ' => 'N', 'ŋ' => 'n',
|
||||
'Ō' => 'O', 'ō' => 'o',
|
||||
'Ŏ' => 'O', 'ŏ' => 'o',
|
||||
'Ő' => 'O', 'ő' => 'o',
|
||||
'Œ' => 'OE', 'œ' => 'oe',
|
||||
'Ŕ' => 'R', 'ŕ' => 'r',
|
||||
'Ŗ' => 'R', 'ŗ' => 'r',
|
||||
'Ř' => 'R', 'ř' => 'r',
|
||||
'Ś' => 'S', 'ś' => 's',
|
||||
'Ŝ' => 'S', 'ŝ' => 's',
|
||||
'Ş' => 'S', 'ş' => 's',
|
||||
'Š' => 'S', 'š' => 's',
|
||||
'Ţ' => 'T', 'ţ' => 't',
|
||||
'Ť' => 'T', 'ť' => 't',
|
||||
'Ŧ' => 'T', 'ŧ' => 't',
|
||||
'Ũ' => 'U', 'ũ' => 'u',
|
||||
'Ū' => 'U', 'ū' => 'u',
|
||||
'Ŭ' => 'U', 'ŭ' => 'u',
|
||||
'Ů' => 'U', 'ů' => 'u',
|
||||
'Ű' => 'U', 'ű' => 'u',
|
||||
'Ų' => 'U', 'ų' => 'u',
|
||||
'Ŵ' => 'W', 'ŵ' => 'w',
|
||||
'Ŷ' => 'Y', 'ŷ' => 'y',
|
||||
'Ÿ' => 'Y', 'Ź' => 'Z',
|
||||
'ź' => 'z', 'Ż' => 'Z',
|
||||
'ż' => 'z', 'Ž' => 'Z',
|
||||
'ž' => 'z', 'ſ' => 's',
|
||||
// Decompositions for Latin Extended-B
|
||||
'Ș' => 'S', 'ș' => 's',
|
||||
'Ț' => 'T', 'ț' => 't',
|
||||
// Euro Sign
|
||||
'€' => 'E',
|
||||
// GBP (Pound) Sign
|
||||
'£' => '',
|
||||
// Vowels with diacritic (Vietnamese)
|
||||
// unmarked
|
||||
'Ơ' => 'O', 'ơ' => 'o',
|
||||
'Ư' => 'U', 'ư' => 'u',
|
||||
// grave accent
|
||||
'Ầ' => 'A', 'ầ' => 'a',
|
||||
'Ằ' => 'A', 'ằ' => 'a',
|
||||
'Ề' => 'E', 'ề' => 'e',
|
||||
'Ồ' => 'O', 'ồ' => 'o',
|
||||
'Ờ' => 'O', 'ờ' => 'o',
|
||||
'Ừ' => 'U', 'ừ' => 'u',
|
||||
'Ỳ' => 'Y', 'ỳ' => 'y',
|
||||
// hook
|
||||
'Ả' => 'A', 'ả' => 'a',
|
||||
'Ẩ' => 'A', 'ẩ' => 'a',
|
||||
'Ẳ' => 'A', 'ẳ' => 'a',
|
||||
'Ẻ' => 'E', 'ẻ' => 'e',
|
||||
'Ể' => 'E', 'ể' => 'e',
|
||||
'Ỉ' => 'I', 'ỉ' => 'i',
|
||||
'Ỏ' => 'O', 'ỏ' => 'o',
|
||||
'Ổ' => 'O', 'ổ' => 'o',
|
||||
'Ở' => 'O', 'ở' => 'o',
|
||||
'Ủ' => 'U', 'ủ' => 'u',
|
||||
'Ử' => 'U', 'ử' => 'u',
|
||||
'Ỷ' => 'Y', 'ỷ' => 'y',
|
||||
// tilde
|
||||
'Ẫ' => 'A', 'ẫ' => 'a',
|
||||
'Ẵ' => 'A', 'ẵ' => 'a',
|
||||
'Ẽ' => 'E', 'ẽ' => 'e',
|
||||
'Ễ' => 'E', 'ễ' => 'e',
|
||||
'Ỗ' => 'O', 'ỗ' => 'o',
|
||||
'Ỡ' => 'O', 'ỡ' => 'o',
|
||||
'Ữ' => 'U', 'ữ' => 'u',
|
||||
'Ỹ' => 'Y', 'ỹ' => 'y',
|
||||
// acute accent
|
||||
'Ấ' => 'A', 'ấ' => 'a',
|
||||
'Ắ' => 'A', 'ắ' => 'a',
|
||||
'Ế' => 'E', 'ế' => 'e',
|
||||
'Ố' => 'O', 'ố' => 'o',
|
||||
'Ớ' => 'O', 'ớ' => 'o',
|
||||
'Ứ' => 'U', 'ứ' => 'u',
|
||||
// dot below
|
||||
'Ạ' => 'A', 'ạ' => 'a',
|
||||
'Ậ' => 'A', 'ậ' => 'a',
|
||||
'Ặ' => 'A', 'ặ' => 'a',
|
||||
'Ẹ' => 'E', 'ẹ' => 'e',
|
||||
'Ệ' => 'E', 'ệ' => 'e',
|
||||
'Ị' => 'I', 'ị' => 'i',
|
||||
'Ọ' => 'O', 'ọ' => 'o',
|
||||
'Ộ' => 'O', 'ộ' => 'o',
|
||||
'Ợ' => 'O', 'ợ' => 'o',
|
||||
'Ụ' => 'U', 'ụ' => 'u',
|
||||
'Ự' => 'U', 'ự' => 'u',
|
||||
'Ỵ' => 'Y', 'ỵ' => 'y',
|
||||
// Vowels with diacritic (Chinese, Hanyu Pinyin)
|
||||
'ɑ' => 'a',
|
||||
// macron
|
||||
'Ǖ' => 'U', 'ǖ' => 'u',
|
||||
// acute accent
|
||||
'Ǘ' => 'U', 'ǘ' => 'u',
|
||||
// caron
|
||||
'Ǎ' => 'A', 'ǎ' => 'a',
|
||||
'Ǐ' => 'I', 'ǐ' => 'i',
|
||||
'Ǒ' => 'O', 'ǒ' => 'o',
|
||||
'Ǔ' => 'U', 'ǔ' => 'u',
|
||||
'Ǚ' => 'U', 'ǚ' => 'u',
|
||||
// grave accent
|
||||
'Ǜ' => 'U', 'ǜ' => 'u',
|
||||
];
|
||||
$string = strtr($string, $chars);
|
||||
|
||||
$string = strtr($string, $chars);
|
||||
|
||||
return $string;
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
|
@ -1,254 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BMDSystemhausBlogBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'cn-tools';
|
||||
const NAME = 'BMD SYSTEMHAUS GesmbH';
|
||||
const CACHE_TIMEOUT = 21600; //6h
|
||||
const URI = 'https://www.bmd.com';
|
||||
const DONATION_URI = 'https://paypal.me/cntools';
|
||||
const DESCRIPTION = 'BMD Systemhaus - We make business easy';
|
||||
const BMD_FAV_ICON = 'https://www.bmd.com/favicon.ico';
|
||||
|
||||
const ITEMSTYLE = [
|
||||
'ilcr' => '<table width="100%"><tr><td style="vertical-align: top;">{data_img}</td><td style="vertical-align: top;">{data_content}</td></tr></table>',
|
||||
'clir' => '<table width="100%"><tr><td style="vertical-align: top;">{data_content}</td><td style="vertical-align: top;">{data_img}</td></tr></table>',
|
||||
'itcb' => '<div>{data_img}<br />{data_content}</div>',
|
||||
'ctib' => '<div>{data_content}<br />{data_img}</div>',
|
||||
'co' => '{data_content}',
|
||||
'io' => '{data_img}'
|
||||
];
|
||||
|
||||
const PARAMETERS = [
|
||||
'Blog' => [
|
||||
'country' => [
|
||||
'name' => 'Country',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Österreich' => 'at',
|
||||
'Deutschland' => 'de',
|
||||
'Schweiz' => 'ch',
|
||||
'Slovensko' => 'sk',
|
||||
'Cesko' => 'cz',
|
||||
'Hungary' => 'hu',
|
||||
],
|
||||
'defaultValue' => 'at',
|
||||
],
|
||||
'style' => [
|
||||
'name' => 'Style',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Image left, content right' => 'ilcr',
|
||||
'Content left, image right' => 'clir',
|
||||
'Image top, content bottom' => 'itcb',
|
||||
'Content top, image bottom' => 'ctib',
|
||||
'Content only' => 'co',
|
||||
'Image only' => 'io',
|
||||
],
|
||||
'defaultValue' => 'ilcr',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
//-----------------------------------------------------
|
||||
public function collectData()
|
||||
{
|
||||
// get website content
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
// Convert relative links in HTML into absolute links
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
// Convert lazy-loading images and frames (video embeds) into static elements
|
||||
$html = convertLazyLoading($html);
|
||||
|
||||
foreach ($html->find('div#bmdNewsList div#bmdNewsList-Item') as $element) {
|
||||
$itemScope = $element->find('div[itemscope=itemscope]', 0);
|
||||
|
||||
$item = [];
|
||||
|
||||
// set base article data
|
||||
$item['title'] = $this->getMetaItemPropContent($itemScope, 'headline');
|
||||
$item['timestamp'] = strtotime($this->getMetaItemPropContent($itemScope, 'datePublished'));
|
||||
$item['author'] = $this->getMetaItemPropContent($itemScope->find('div[itemprop=author]', 0), 'name');
|
||||
|
||||
// find article image
|
||||
$imageTag = '';
|
||||
$image = $element->find('div.mediaelement.mediaelement-image img', 0);
|
||||
if ((!is_null($image)) and ($image->src != '')) {
|
||||
$item['enclosures'] = [$image->src];
|
||||
$imageTag = '<img src="' . $image->src . '"/>';
|
||||
}
|
||||
|
||||
// begin with right style
|
||||
$content = self::ITEMSTYLE[$this->getInput('style')];
|
||||
|
||||
// render placeholder
|
||||
$content = str_replace('{data_content}', $this->getMetaItemPropContent($itemScope, 'description'), $content);
|
||||
$content = str_replace('{data_img}', $imageTag, $content);
|
||||
|
||||
// set finished content
|
||||
$item['content'] = $content;
|
||||
|
||||
// get link to article
|
||||
$link = $element->find('div#bmdNewsList-Text div#bmdNewsList-Title a', 0);
|
||||
if (!is_null($link)) {
|
||||
$item['uri'] = $link->href;
|
||||
}
|
||||
|
||||
// init categories
|
||||
$categories = [];
|
||||
$tmpOne = [];
|
||||
$tmpTwo = [];
|
||||
|
||||
// search first categorie span
|
||||
$catElem = $element->find('div#bmdNewsList-Text div#bmdNewsList-Category span.news-list-category', 0);
|
||||
$txt = trim($catElem->innertext);
|
||||
$tmpOne = explode('/', $txt);
|
||||
|
||||
// split by 2 spaces
|
||||
foreach ($tmpOne as $tmpElem) {
|
||||
$tmpElem = trim($tmpElem);
|
||||
$tmpData = preg_split('/ /', $tmpElem);
|
||||
$tmpTwo = array_merge($tmpTwo, $tmpData);
|
||||
}
|
||||
|
||||
// split by tabulator
|
||||
foreach ($tmpTwo as $tmpElem) {
|
||||
$tmpElem = trim($tmpElem);
|
||||
$tmpData = preg_split('/\t+/', $tmpElem);
|
||||
$categories = array_merge($categories, $tmpData);
|
||||
}
|
||||
|
||||
// trim each categorie entries
|
||||
$categories = array_map('trim', $categories);
|
||||
|
||||
// remove empty entries
|
||||
$categories = array_filter($categories, function ($value) {
|
||||
return !is_null($value) && $value !== '';
|
||||
});
|
||||
|
||||
// set categories
|
||||
if (count($categories) > 0) {
|
||||
$item['categories'] = $categories;
|
||||
}
|
||||
|
||||
// add item
|
||||
if (($item['title'] != '') and ($item['content'] != '') and ($item['uri'] != '')) {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
public function detectParameters($url)
|
||||
{
|
||||
try {
|
||||
$parsedUrl = Url::fromString($url);
|
||||
} catch (UrlException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!in_array($parsedUrl->getHost(), ['www.bmd.com', 'bmd.com'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lang = '';
|
||||
|
||||
// extract language from url
|
||||
$path = explode('/', $parsedUrl->getPath());
|
||||
if (count($path) > 1) {
|
||||
$lang = $path[1];
|
||||
|
||||
// validate data
|
||||
if ($this->getURIbyCountry($lang) == '') {
|
||||
$lang = '';
|
||||
}
|
||||
}
|
||||
|
||||
// if no country available, find language by browser
|
||||
if ($lang == '') {
|
||||
$srvLanguages = explode(';', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
|
||||
if (count($srvLanguages) > 0) {
|
||||
$languages = explode(',', $srvLanguages[0]);
|
||||
if (count($languages) > 0) {
|
||||
for ($i = 0; $i < count($languages); $i++) {
|
||||
$langDetails = explode('-', $languages[$i]);
|
||||
if (count($langDetails) > 1) {
|
||||
$lang = $langDetails[1];
|
||||
} else {
|
||||
$lang = substr($srvLanguages[0], 0, 2);
|
||||
}
|
||||
|
||||
// validate data
|
||||
if ($this->getURIbyCountry($lang) == '') {
|
||||
$lang = '';
|
||||
}
|
||||
|
||||
if ($lang != '') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no URL found by language, use AT as default
|
||||
if ($this->getURIbyCountry($lang) == '') {
|
||||
$lang = 'at';
|
||||
}
|
||||
|
||||
$params = [];
|
||||
$params['country'] = strtolower($lang);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
public function getURI()
|
||||
{
|
||||
$country = $this->getInput('country') ?? '';
|
||||
$lURI = $this->getURIbyCountry($country);
|
||||
return $lURI != '' ? $lURI : parent::getURI();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
public function getIcon()
|
||||
{
|
||||
return self::BMD_FAV_ICON;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
private function getMetaItemPropContent($elem, $key)
|
||||
{
|
||||
if (($key != '') and (!is_null($elem))) {
|
||||
$metaElem = $elem->find('meta[itemprop=' . $key . ']', 0);
|
||||
if (!is_null($metaElem)) {
|
||||
return $metaElem->getAttribute('content');
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
private function getURIbyCountry($country)
|
||||
{
|
||||
switch (strtolower($country)) {
|
||||
case 'at':
|
||||
return 'https://www.bmd.com/at/ueber-bmd/blog-ohne-filter.html';
|
||||
case 'de':
|
||||
return 'https://www.bmd.com/de/das-ist-bmd/blog.html';
|
||||
case 'ch':
|
||||
return 'https://www.bmd.com/ch/das-ist-bmd/blog.html';
|
||||
case 'sk':
|
||||
return 'https://www.bmd.com/sk/firma/blog.html';
|
||||
case 'cz':
|
||||
return 'https://www.bmd.com/cz/firma/news-blog.html';
|
||||
case 'hu':
|
||||
return 'https://www.bmd.com/hu/rolunk/hirek.html';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
@ -1,441 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BadDragonBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bad Dragon Bridge';
|
||||
const URI = 'https://bad-dragon.com/';
|
||||
const CACHE_TIMEOUT = 300; // 5min
|
||||
const DESCRIPTION = 'Returns sales or new clearance items';
|
||||
const MAINTAINER = 'Roliga';
|
||||
const PARAMETERS = [
|
||||
'Sales' => [
|
||||
],
|
||||
'Clearance' => [
|
||||
'ready_made' => [
|
||||
'name' => 'Ready Made',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'flop' => [
|
||||
'name' => 'Flops',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'skus' => [
|
||||
'name' => 'Products',
|
||||
'exampleValue' => 'chanceflared, crackers',
|
||||
'title' => 'Comma separated list of product SKUs'
|
||||
],
|
||||
'onesize' => [
|
||||
'name' => 'One-Size',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'mini' => [
|
||||
'name' => 'Mini',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'small' => [
|
||||
'name' => 'Small',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'medium' => [
|
||||
'name' => 'Medium',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'large' => [
|
||||
'name' => 'Large',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'extralarge' => [
|
||||
'name' => 'Extra Large',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'category' => [
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All' => 'all',
|
||||
'Accessories' => 'accessories',
|
||||
'Merchandise' => 'merchandise',
|
||||
'Dildos' => 'insertable',
|
||||
'Masturbators' => 'penetrable',
|
||||
'Packers' => 'packer',
|
||||
'Lil\' Squirts' => 'shooter',
|
||||
'Lil\' Vibes' => 'vibrator',
|
||||
'Wearables' => 'wearable'
|
||||
],
|
||||
'defaultValue' => 'all',
|
||||
],
|
||||
'soft' => [
|
||||
'name' => 'Soft Firmness',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'med_firm' => [
|
||||
'name' => 'Medium Firmness',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'firm' => [
|
||||
'name' => 'Firm',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'split' => [
|
||||
'name' => 'Split Firmness',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'maxprice' => [
|
||||
'name' => 'Max Price',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'defaultValue' => 300
|
||||
],
|
||||
'minprice' => [
|
||||
'name' => 'Min Price',
|
||||
'type' => 'number',
|
||||
'defaultValue' => 0
|
||||
],
|
||||
'cumtube' => [
|
||||
'name' => 'Cumtube',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'suctionCup' => [
|
||||
'name' => 'Suction Cup',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'noAccessories' => [
|
||||
'name' => 'No Accessories',
|
||||
'type' => 'checkbox'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
/*
|
||||
* This sets index $strFrom (or $strTo if set) in $outArr to 'on' if
|
||||
* $inArr[$param] contains $strFrom.
|
||||
* It is used for translating BD's shop filter URLs into something we can use.
|
||||
*
|
||||
* For the query '?type[]=ready_made&type[]=flop' we would have an array like:
|
||||
* Array (
|
||||
* [type] => Array (
|
||||
* [0] => ready_made
|
||||
* [1] => flop
|
||||
* )
|
||||
* )
|
||||
* which could be translated into:
|
||||
* Array (
|
||||
* [ready_made] => on
|
||||
* [flop] => on
|
||||
* )
|
||||
* */
|
||||
private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null)
|
||||
{
|
||||
if (isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
|
||||
$outArr[($strTo ?: $strFrom)] = 'on';
|
||||
}
|
||||
}
|
||||
|
||||
public function detectParameters($url)
|
||||
{
|
||||
$params = [];
|
||||
|
||||
// Sale
|
||||
$regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
|
||||
if (preg_match($regex, $url, $matches) > 0) {
|
||||
$params['context'] = 'Sales';
|
||||
return $params;
|
||||
}
|
||||
|
||||
// Clearance
|
||||
$regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/';
|
||||
if (preg_match($regex, $url, $matches) > 0) {
|
||||
parse_str(parse_url($url, PHP_URL_QUERY), $urlParams);
|
||||
|
||||
$this->setParam($urlParams, $params, 'type', 'ready_made');
|
||||
$this->setParam($urlParams, $params, 'type', 'flop');
|
||||
|
||||
if (isset($urlParams['skus'])) {
|
||||
$skus = [];
|
||||
foreach ($urlParams['skus'] as $sku) {
|
||||
is_string($sku) && $skus[] = $sku;
|
||||
is_array($sku) && $skus[] = $sku[0];
|
||||
}
|
||||
$params['skus'] = implode(',', $skus);
|
||||
}
|
||||
|
||||
$this->setParam($urlParams, $params, 'sizes', 'onesize');
|
||||
$this->setParam($urlParams, $params, 'sizes', 'mini');
|
||||
$this->setParam($urlParams, $params, 'sizes', 'small');
|
||||
$this->setParam($urlParams, $params, 'sizes', 'medium');
|
||||
$this->setParam($urlParams, $params, 'sizes', 'large');
|
||||
$this->setParam($urlParams, $params, 'sizes', 'extralarge');
|
||||
|
||||
if (isset($urlParams['category'])) {
|
||||
$params['category'] = strtolower($urlParams['category']);
|
||||
} else {
|
||||
$params['category'] = 'all';
|
||||
}
|
||||
|
||||
$this->setParam($urlParams, $params, 'firmnessValues', 'soft');
|
||||
$this->setParam($urlParams, $params, 'firmnessValues', 'medium', 'med_firm');
|
||||
$this->setParam($urlParams, $params, 'firmnessValues', 'firm');
|
||||
$this->setParam($urlParams, $params, 'firmnessValues', 'split');
|
||||
|
||||
if (isset($urlParams['price'])) {
|
||||
isset($urlParams['price']['max'])
|
||||
&& $params['maxprice'] = $urlParams['price']['max'];
|
||||
isset($urlParams['price']['min'])
|
||||
&& $params['minprice'] = $urlParams['price']['min'];
|
||||
}
|
||||
|
||||
isset($urlParams['cumtube'])
|
||||
&& $urlParams['cumtube'] === '1'
|
||||
&& $params['cumtube'] = 'on';
|
||||
isset($urlParams['suctionCup'])
|
||||
&& $urlParams['suctionCup'] === '1'
|
||||
&& $params['suctionCup'] = 'on';
|
||||
isset($urlParams['noAccessories'])
|
||||
&& $urlParams['noAccessories'] === '1'
|
||||
&& $params['noAccessories'] = 'on';
|
||||
$params['context'] = 'Clearance';
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Sales':
|
||||
return 'Bad Dragon Sales';
|
||||
case 'Clearance':
|
||||
return 'Bad Dragon Clearance Search';
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Sales':
|
||||
return self::URI . 'sales';
|
||||
case 'Clearance':
|
||||
return $this->inputToURL();
|
||||
default:
|
||||
return parent::getURI();
|
||||
}
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Sales':
|
||||
$sales = json_decode(getContents(self::URI . 'api/sales'));
|
||||
|
||||
foreach ($sales as $sale) {
|
||||
$item = [];
|
||||
|
||||
$item['title'] = $sale->title;
|
||||
$item['timestamp'] = strtotime($sale->startDate);
|
||||
|
||||
$item['uri'] = $this->getURI() . '/' . $sale->slug;
|
||||
|
||||
$contentHTML = '<p><img src="' . $sale->image->url . '"></p>';
|
||||
if (isset($sale->endDate)) {
|
||||
$contentHTML .= '<p><b>This promotion ends on '
|
||||
. gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate))
|
||||
. '</b></p>';
|
||||
} else {
|
||||
$contentHTML .= '<p><b>This promotion never ends</b></p>';
|
||||
}
|
||||
$ul = false;
|
||||
$content = json_decode($sale->content);
|
||||
foreach ($content->blocks as $block) {
|
||||
switch ($block->type) {
|
||||
case 'header-one':
|
||||
$contentHTML .= '<h1>' . $block->text . '</h1>';
|
||||
break;
|
||||
case 'header-two':
|
||||
$contentHTML .= '<h2>' . $block->text . '</h2>';
|
||||
break;
|
||||
case 'header-three':
|
||||
$contentHTML .= '<h3>' . $block->text . '</h3>';
|
||||
break;
|
||||
case 'unordered-list-item':
|
||||
if (!$ul) {
|
||||
$contentHTML .= '<ul>';
|
||||
$ul = true;
|
||||
}
|
||||
$contentHTML .= '<li>' . $block->text . '</li>';
|
||||
break;
|
||||
default:
|
||||
if ($ul) {
|
||||
$contentHTML .= '</ul>';
|
||||
$ul = false;
|
||||
}
|
||||
$contentHTML .= '<p>' . $block->text . '</p>';
|
||||
break;
|
||||
}
|
||||
}
|
||||
$item['content'] = $contentHTML;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
break;
|
||||
case 'Clearance':
|
||||
$toyData = json_decode(getContents($this->inputToURL(true)));
|
||||
|
||||
$productList = json_decode(getContents(self::URI . 'api/inventory-toy/product-list'));
|
||||
|
||||
foreach ($toyData->toys as $toy) {
|
||||
$item = [];
|
||||
|
||||
$item['uri'] = $this->getURI()
|
||||
. '#'
|
||||
. $toy->id;
|
||||
$item['timestamp'] = strtotime($toy->created);
|
||||
|
||||
foreach ($productList as $product) {
|
||||
if ($product->sku == $toy->sku) {
|
||||
$item['title'] = $product->name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// images
|
||||
$content = '<p>';
|
||||
foreach ($toy->images as $image) {
|
||||
$content .= '<a href="'
|
||||
. $image->fullFilename
|
||||
. '"><img src="'
|
||||
. $image->thumbFilename
|
||||
. '" /></a>';
|
||||
}
|
||||
// price
|
||||
$content .= '</p><p><b>Price:</b> $'
|
||||
. $toy->price
|
||||
// size
|
||||
. '<br /><b>Size:</b> '
|
||||
. $toy->size
|
||||
// color
|
||||
. '<br /><b>Color:</b> '
|
||||
. $toy->color
|
||||
// features
|
||||
. '<br /><b>Features:</b> '
|
||||
. ($toy->suction_cup ? 'Suction cup' : '')
|
||||
. ($toy->suction_cup && $toy->cumtube ? ', ' : '')
|
||||
. ($toy->cumtube ? 'Cumtube' : '')
|
||||
. ($toy->suction_cup || $toy->cumtube ? '' : 'None');
|
||||
// firmness
|
||||
$firmnessTexts = [
|
||||
'2' => 'Extra soft',
|
||||
'3' => 'Soft',
|
||||
'5' => 'Medium',
|
||||
'8' => 'Firm'
|
||||
];
|
||||
$firmnesses = explode('/', $toy->firmness);
|
||||
if (count($firmnesses) === 2) {
|
||||
$content .= '<br /><b>Firmness:</b> '
|
||||
. $firmnessTexts[$firmnesses[0]]
|
||||
. ', '
|
||||
. $firmnessTexts[$firmnesses[1]];
|
||||
} else {
|
||||
$content .= '<br /><b>Firmness:</b> '
|
||||
. $firmnessTexts[$firmnesses[0]];
|
||||
}
|
||||
// flop
|
||||
if ($toy->type === 'flop') {
|
||||
$content .= '<br /><b>Flop reason:</b> '
|
||||
. $toy->flop_reason;
|
||||
}
|
||||
$content .= '</p>';
|
||||
$item['content'] = $content;
|
||||
|
||||
$enclosures = [];
|
||||
foreach ($toy->images as $image) {
|
||||
$enclosures[] = $image->fullFilename;
|
||||
}
|
||||
$item['enclosures'] = $enclosures;
|
||||
|
||||
$categories = [];
|
||||
$categories[] = $toy->sku;
|
||||
$categories[] = $toy->type;
|
||||
$categories[] = $toy->size;
|
||||
if ($toy->cumtube) {
|
||||
$categories[] = 'cumtube';
|
||||
}
|
||||
if ($toy->suction_cup) {
|
||||
$categories[] = 'suction_cup';
|
||||
}
|
||||
$item['categories'] = $categories;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function inputToURL($api = false)
|
||||
{
|
||||
$url = self::URI;
|
||||
$url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
|
||||
|
||||
// Default parameters
|
||||
$url .= 'limit=60';
|
||||
$url .= '&page=1';
|
||||
$url .= '&sort[field]=created';
|
||||
$url .= '&sort[direction]=desc';
|
||||
|
||||
// Product types
|
||||
$url .= ($this->getInput('ready_made') ? '&type[]=ready_made' : '');
|
||||
$url .= ($this->getInput('flop') ? '&type[]=flop' : '');
|
||||
|
||||
// Product names
|
||||
foreach (array_filter(explode(',', $this->getInput('skus'))) as $sku) {
|
||||
$url .= '&skus[]=' . urlencode(trim($sku));
|
||||
}
|
||||
|
||||
// Size
|
||||
$url .= ($this->getInput('onesize') ? '&sizes[]=onesize' : '');
|
||||
$url .= ($this->getInput('mini') ? '&sizes[]=mini' : '');
|
||||
$url .= ($this->getInput('small') ? '&sizes[]=small' : '');
|
||||
$url .= ($this->getInput('medium') ? '&sizes[]=medium' : '');
|
||||
$url .= ($this->getInput('large') ? '&sizes[]=large' : '');
|
||||
$url .= ($this->getInput('extralarge') ? '&sizes[]=extralarge' : '');
|
||||
|
||||
// Category
|
||||
$url .= ($this->getInput('category') ? '&category='
|
||||
. urlencode($this->getInput('category')) : '');
|
||||
|
||||
// Firmness
|
||||
if ($api) {
|
||||
$url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : '');
|
||||
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : '');
|
||||
$url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : '');
|
||||
if ($this->getInput('split')) {
|
||||
$url .= '&firmnessValues[]=3/5';
|
||||
$url .= '&firmnessValues[]=3/8';
|
||||
$url .= '&firmnessValues[]=8/3';
|
||||
$url .= '&firmnessValues[]=5/8';
|
||||
$url .= '&firmnessValues[]=8/5';
|
||||
}
|
||||
} else {
|
||||
$url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : '');
|
||||
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : '');
|
||||
$url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : '');
|
||||
$url .= ($this->getInput('split') ? '&firmnessValues[]=split' : '');
|
||||
}
|
||||
|
||||
// Price
|
||||
$url .= ($this->getInput('maxprice') ? '&price[max]='
|
||||
. $this->getInput('maxprice') : '&price[max]=300');
|
||||
$url .= ($this->getInput('minprice') ? '&price[min]='
|
||||
. $this->getInput('minprice') : '&price[min]=0');
|
||||
|
||||
// Features
|
||||
$url .= ($this->getInput('cumtube') ? '&cumtube=1' : '');
|
||||
$url .= ($this->getInput('suctionCup') ? '&suctionCup=1' : '');
|
||||
$url .= ($this->getInput('noAccessories') ? '&noAccessories=1' : '');
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Baka Updates Manga Releases';
|
||||
const URI = 'https://www.mangaupdates.com/';
|
||||
const DESCRIPTION = 'Get the latest series releases';
|
||||
const MAINTAINER = 'fulmeek, KamaleiZestri';
|
||||
const PARAMETERS = [
|
||||
'By series' => [
|
||||
'series_id' => [
|
||||
'name' => 'Series ID',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'exampleValue' => '188066'
|
||||
]
|
||||
],
|
||||
'By list' => [
|
||||
'list_id' => [
|
||||
'name' => 'List ID and Type',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => '4395&list=read'
|
||||
]
|
||||
]
|
||||
];
|
||||
const LIMIT_COLS = 5;
|
||||
const LIMIT_ITEMS = 10;
|
||||
const RELEASES_URL = 'https://www.mangaupdates.com/releases.html';
|
||||
|
||||
private $feedName = '';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
if ($this -> queriedContext == 'By series') {
|
||||
$this -> collectDataBySeries();
|
||||
} else { //queriedContext == 'By list'
|
||||
$this -> collectDataByList();
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if ($this -> queriedContext == 'By series') {
|
||||
$series_id = $this->getInput('series_id');
|
||||
if (!empty($series_id)) {
|
||||
return self::URI . 'releases.html?search=' . $series_id . '&stype=series';
|
||||
}
|
||||
} else { //queriedContext == 'By list'
|
||||
return self::RELEASES_URL;
|
||||
}
|
||||
|
||||
return self::URI;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!empty($this->feedName)) {
|
||||
return $this->feedName . ' - ' . self::NAME;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
private function getSanitizedHash($string)
|
||||
{
|
||||
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
|
||||
}
|
||||
|
||||
private function filterText($text)
|
||||
{
|
||||
return rtrim($text, '* ');
|
||||
}
|
||||
|
||||
private function filterHTML($text)
|
||||
{
|
||||
return $this->filterText(html_entity_decode($text));
|
||||
}
|
||||
|
||||
private function findID($manga)
|
||||
{
|
||||
// sometimes new series are on the release list that have no ID. just drop them.
|
||||
if (@$this -> filterHTML($manga -> find('a', 0) -> href) != null) {
|
||||
preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match);
|
||||
return $match[1];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function collectDataBySeries()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
// content is an unstructured pile of divs, ugly to parse
|
||||
$cols = $html->find('div#main_content div.row > div.text');
|
||||
if (!$cols) {
|
||||
returnServerError('No releases');
|
||||
}
|
||||
|
||||
$rows = array_slice(
|
||||
array_chunk($cols, self::LIMIT_COLS),
|
||||
0,
|
||||
self::LIMIT_ITEMS
|
||||
);
|
||||
|
||||
if (isset($rows[0][1])) {
|
||||
$this->feedName = $this->filterHTML($rows[0][1]->plaintext);
|
||||
}
|
||||
|
||||
foreach ($rows as $cols) {
|
||||
if (count($cols) < self::LIMIT_COLS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = [];
|
||||
$title = [];
|
||||
|
||||
$item['content'] = '';
|
||||
|
||||
$objDate = $cols[0];
|
||||
if ($objDate) {
|
||||
$item['timestamp'] = strtotime($objDate->plaintext);
|
||||
}
|
||||
|
||||
$objTitle = $cols[1];
|
||||
if ($objTitle) {
|
||||
$title[] = $this->filterHTML($objTitle->plaintext);
|
||||
$item['content'] .= '<p>Series: ' . $this->filterText($objTitle->innertext) . '</p>';
|
||||
}
|
||||
|
||||
$objVolume = $cols[2];
|
||||
if ($objVolume && !empty($objVolume->plaintext)) {
|
||||
$title[] = 'Vol.' . $objVolume->plaintext;
|
||||
}
|
||||
|
||||
$objChapter = $cols[3];
|
||||
if ($objChapter && !empty($objChapter->plaintext)) {
|
||||
$title[] = 'Chp.' . $objChapter->plaintext;
|
||||
}
|
||||
|
||||
$objAuthor = $cols[4];
|
||||
if ($objAuthor && !empty($objAuthor->plaintext)) {
|
||||
$item['author'] = $this->filterHTML($objAuthor->plaintext);
|
||||
$item['content'] .= '<p>Groups: ' . $this->filterText($objAuthor->innertext) . '</p>';
|
||||
}
|
||||
|
||||
$item['title'] = implode(' ', $title);
|
||||
$item['uri'] = $this->getURI();
|
||||
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function collectDataByList()
|
||||
{
|
||||
$this -> feedName = 'Releases';
|
||||
$list = [];
|
||||
|
||||
$releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL);
|
||||
|
||||
$list_id = $this -> getInput('list_id');
|
||||
$listHTML = getSimpleHTMLDOM('https://www.mangaupdates.com/mylist.html?id=' . $list_id);
|
||||
|
||||
//get ids of the manga that the user follows,
|
||||
$parts = $listHTML -> find('table#ptable tr > td.pl');
|
||||
foreach ($parts as $part) {
|
||||
$list[] = $this -> findID($part);
|
||||
}
|
||||
|
||||
//similar to above, but the divs are in groups of 3.
|
||||
$cols = $releasesHTML -> find('div#main_content div.row > div.pbreak');
|
||||
$rows = array_slice(array_chunk($cols, 3), 0);
|
||||
|
||||
foreach ($rows as $cols) {
|
||||
//check if current manga is in user's list.
|
||||
$id = $this -> findId($cols[0]);
|
||||
if (!array_search($id, $list)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = [];
|
||||
$title = [];
|
||||
|
||||
$item['content'] = '';
|
||||
|
||||
$objTitle = $cols[0];
|
||||
if ($objTitle) {
|
||||
$title[] = $this->filterHTML($objTitle->plaintext);
|
||||
$item['content'] .= '<p>Series: ' . $this->filterHTML($objTitle -> innertext) . '</p>';
|
||||
}
|
||||
|
||||
$objVolChap = $cols[1];
|
||||
if ($objVolChap && !empty($objVolChap->plaintext)) {
|
||||
$title[] = $this -> filterHTML($objVolChap -> innertext);
|
||||
}
|
||||
|
||||
$objAuthor = $cols[2];
|
||||
if ($objAuthor && !empty($objAuthor->plaintext)) {
|
||||
$item['author'] = $this->filterHTML($objAuthor -> plaintext);
|
||||
$item['content'] .= '<p>Groups: ' . $this->filterHTML($objAuthor -> innertext) . '</p>';
|
||||
}
|
||||
|
||||
$item['title'] = implode(' ', $title);
|
||||
$item['uri'] = self::URI . 'releases.html?search=' . $id . '&stype=series';
|
||||
$item['uid'] = $this->getSanitizedHash($item['title'] . $item['author']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,425 +1,67 @@
|
||||
<?php
|
||||
class BandcampBridge extends BridgeAbstract {
|
||||
|
||||
class BandcampBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'sebsauvage, Roliga';
|
||||
const NAME = 'Bandcamp Bridge';
|
||||
const URI = 'https://bandcamp.com/';
|
||||
const CACHE_TIMEOUT = 600; // 10min
|
||||
const DESCRIPTION = 'New bandcamp releases by tag, band or album';
|
||||
const PARAMETERS = [
|
||||
'By tag' => [
|
||||
'tag' => [
|
||||
'name' => 'tag',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'hip-hop-rap'
|
||||
]
|
||||
],
|
||||
'By band' => [
|
||||
'band' => [
|
||||
'name' => 'band',
|
||||
'type' => 'text',
|
||||
'title' => 'Band name as seen in the band page URL',
|
||||
'required' => true,
|
||||
'exampleValue' => 'aesoprock'
|
||||
],
|
||||
'type' => [
|
||||
'name' => 'Articles are',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Releases' => 'releases',
|
||||
'Releases, new one when track list changes' => 'changes',
|
||||
'Individual tracks' => 'tracks'
|
||||
],
|
||||
'defaultValue' => 'changes'
|
||||
],
|
||||
'limit' => [
|
||||
'name' => 'limit',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'Number of releases to return',
|
||||
'defaultValue' => 5
|
||||
]
|
||||
],
|
||||
'By label' => [
|
||||
'label' => [
|
||||
'name' => 'label',
|
||||
'type' => 'text',
|
||||
'title' => 'label name as seen in the label page URL',
|
||||
'required' => true
|
||||
],
|
||||
'type' => [
|
||||
'name' => 'Articles are',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Releases' => 'releases',
|
||||
'Releases, new one when track list changes' => 'changes',
|
||||
'Individual tracks' => 'tracks'
|
||||
],
|
||||
'defaultValue' => 'changes'
|
||||
],
|
||||
'limit' => [
|
||||
'name' => 'limit',
|
||||
'type' => 'number',
|
||||
'title' => 'Number of releases to return',
|
||||
'defaultValue' => 5
|
||||
]
|
||||
],
|
||||
'By album' => [
|
||||
'band' => [
|
||||
'name' => 'band',
|
||||
'type' => 'text',
|
||||
'title' => 'Band name as seen in the album page URL',
|
||||
'required' => true,
|
||||
'exampleValue' => 'aesoprock'
|
||||
],
|
||||
'album' => [
|
||||
'name' => 'album',
|
||||
'type' => 'text',
|
||||
'title' => 'Album name as seen in the album page URL',
|
||||
'required' => true,
|
||||
'exampleValue' => 'appleseed'
|
||||
],
|
||||
'type' => [
|
||||
'name' => 'Articles are',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Releases' => 'releases',
|
||||
'Releases, new one when track list changes' => 'changes',
|
||||
'Individual tracks' => 'tracks'
|
||||
],
|
||||
'defaultValue' => 'tracks'
|
||||
]
|
||||
]
|
||||
];
|
||||
const IMGURI = 'https://f4.bcbits.com/';
|
||||
const IMGSIZE_300PX = 23;
|
||||
const IMGSIZE_700PX = 16;
|
||||
const MAINTAINER = 'sebsauvage';
|
||||
const NAME = 'Bandcamp Tag';
|
||||
const URI = 'https://bandcamp.com/';
|
||||
const CACHE_TIMEOUT = 600; // 10min
|
||||
const DESCRIPTION = 'New bandcamp release by tag';
|
||||
const PARAMETERS = array( array(
|
||||
'tag' => array(
|
||||
'name' => 'tag',
|
||||
'type' => 'text',
|
||||
'required' => true
|
||||
)
|
||||
));
|
||||
|
||||
private $feedName;
|
||||
public function getIcon() {
|
||||
return 'https://s4.bcbits.com/img/bc_favicon.ico';
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://s4.bcbits.com/img/bc_favicon.ico';
|
||||
}
|
||||
public function collectData(){
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('No results for this query.');
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'By tag':
|
||||
$url = self::URI . 'api/hub/1/dig_deeper';
|
||||
$data = $this->buildRequestJson();
|
||||
$header = [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($data),
|
||||
];
|
||||
$opts = [
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_POSTFIELDS => $data,
|
||||
];
|
||||
$content = getContents($url, $header, $opts);
|
||||
foreach($html->find('li.item') as $release) {
|
||||
$script = $release->find('div.art', 0)->getAttribute('onclick');
|
||||
$uri = ltrim($script, "return 'url(");
|
||||
$uri = rtrim($uri, "')");
|
||||
|
||||
$json = json_decode($content);
|
||||
$item = array();
|
||||
$item['author'] = $release->find('div.itemsubtext', 0)->plaintext
|
||||
. ' - '
|
||||
. $release->find('div.itemtext', 0)->plaintext;
|
||||
|
||||
if ($json->ok !== true) {
|
||||
returnServerError('Invalid response');
|
||||
}
|
||||
$item['title'] = $release->find('div.itemsubtext', 0)->plaintext
|
||||
. ' - '
|
||||
. $release->find('div.itemtext', 0)->plaintext;
|
||||
|
||||
foreach ($json->items as $entry) {
|
||||
$url = $entry->tralbum_url;
|
||||
$artist = $entry->artist;
|
||||
$title = $entry->title;
|
||||
// e.g. record label is the releaser, but not the artist
|
||||
$releaser = $entry->band_name !== $entry->artist ? $entry->band_name : null;
|
||||
$item['content'] = '<img src="'
|
||||
. $uri
|
||||
. '"/><br/>'
|
||||
. $release->find('div.itemsubtext', 0)->plaintext
|
||||
. ' - '
|
||||
. $release->find('div.itemtext', 0)->plaintext;
|
||||
|
||||
$full_title = $artist . ' - ' . $title;
|
||||
$full_artist = $artist;
|
||||
if (isset($releaser)) {
|
||||
$full_title .= ' (' . $releaser . ')';
|
||||
$full_artist .= ' (' . $releaser . ')';
|
||||
}
|
||||
$small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX);
|
||||
$img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX);
|
||||
$item['id'] = $release->find('a', 0)->getAttribute('href');
|
||||
$item['uri'] = $release->find('a', 0)->getAttribute('href');
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$item = [
|
||||
'uri' => $url,
|
||||
'author' => $full_artist,
|
||||
'title' => $full_title
|
||||
];
|
||||
$item['content'] = "<img src='$small_img' /><br/>$full_title";
|
||||
$item['enclosures'] = [$img];
|
||||
$this->items[] = $item;
|
||||
}
|
||||
break;
|
||||
case 'By band':
|
||||
case 'By label':
|
||||
case 'By album':
|
||||
$html = getSimpleHTMLDOMCached($this->getURI(), 86400);
|
||||
public function getURI(){
|
||||
if(!is_null($this->getInput('tag'))) {
|
||||
return self::URI . 'tag/' . urlencode($this->getInput('tag')) . '?sort_field=date';
|
||||
}
|
||||
|
||||
if ($html->find('meta[name=title]', 0)) {
|
||||
$this->feedName = $html->find('meta[name=title]', 0)->content;
|
||||
} else {
|
||||
$this->feedName = str_replace('Music | ', '', $html->find('title', 0)->plaintext);
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
$regex = '/band_id=(\d+)/';
|
||||
if (preg_match($regex, $html, $matches) == false) {
|
||||
returnServerError('Unable to find band ID on: ' . $this->getURI());
|
||||
}
|
||||
$band_id = $matches[1];
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('tag'))) {
|
||||
return $this->getInput('tag') . ' - Bandcamp Tag';
|
||||
}
|
||||
|
||||
$tralbums = [];
|
||||
switch ($this->queriedContext) {
|
||||
case 'By band':
|
||||
case 'By label':
|
||||
$query_data = [
|
||||
'band_id' => $band_id
|
||||
];
|
||||
$band_data = $this->apiGet('mobile/22/band_details', $query_data);
|
||||
|
||||
$num_albums = min(count($band_data->discography), $this->getInput('limit'));
|
||||
for ($i = 0; $i < $num_albums; $i++) {
|
||||
$album_basic_data = $band_data->discography[$i];
|
||||
|
||||
// 'a' or 't' for albums and individual tracks respectively
|
||||
$tralbum_type = substr($album_basic_data->item_type, 0, 1);
|
||||
|
||||
$query_data = [
|
||||
'band_id' => $band_id,
|
||||
'tralbum_type' => $tralbum_type,
|
||||
'tralbum_id' => $album_basic_data->item_id
|
||||
];
|
||||
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
|
||||
}
|
||||
break;
|
||||
case 'By album':
|
||||
$regex = '/album=(\d+)/';
|
||||
if (preg_match($regex, $html, $matches) == false) {
|
||||
returnServerError('Unable to find album ID on: ' . $this->getURI());
|
||||
}
|
||||
$album_id = $matches[1];
|
||||
|
||||
$query_data = [
|
||||
'band_id' => $band_id,
|
||||
'tralbum_type' => 'a',
|
||||
'tralbum_id' => $album_id
|
||||
];
|
||||
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($tralbums as $tralbum_data) {
|
||||
if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
|
||||
foreach ($tralbum_data->tracks as $track) {
|
||||
$query_data = [
|
||||
'band_id' => $band_id,
|
||||
'tralbum_type' => 't',
|
||||
'tralbum_id' => $track->track_id
|
||||
];
|
||||
$track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
|
||||
|
||||
$this->items[] = $this->buildTralbumItem($track_data);
|
||||
}
|
||||
} else {
|
||||
$this->items[] = $this->buildTralbumItem($tralbum_data);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildTralbumItem($tralbum_data)
|
||||
{
|
||||
$band_data = $tralbum_data->band;
|
||||
|
||||
// Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
|
||||
// Format artist/author like: ARTIST (OPTIONAL RELEASER)
|
||||
//
|
||||
// If the album/track is released under a label/a band other than the artist
|
||||
// themselves, append that releaser name to the title and artist/author.
|
||||
//
|
||||
// This sadly doesn't always work right for individual tracks as the artist
|
||||
// of the track is always set to the releaser.
|
||||
$artist = $tralbum_data->tralbum_artist;
|
||||
$full_title = $artist . ' - ' . $tralbum_data->title;
|
||||
$full_artist = $artist;
|
||||
if (isset($tralbum_data->label)) {
|
||||
$full_title .= ' (' . $tralbum_data->label . ')';
|
||||
$full_artist .= ' (' . $tralbum_data->label . ')';
|
||||
} elseif ($band_data->name !== $artist) {
|
||||
$full_title .= ' (' . $band_data->name . ')';
|
||||
$full_artist .= ' (' . $band_data->name . ')';
|
||||
}
|
||||
|
||||
$small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
|
||||
$img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
|
||||
|
||||
$item = [
|
||||
'uri' => $tralbum_data->bandcamp_url,
|
||||
'author' => $full_artist,
|
||||
'title' => $full_title,
|
||||
'enclosures' => [$img],
|
||||
'timestamp' => $tralbum_data->release_date
|
||||
];
|
||||
|
||||
$item['categories'] = [];
|
||||
foreach ($tralbum_data->tags as $tag) {
|
||||
$item['categories'][] = $tag->norm_name;
|
||||
}
|
||||
|
||||
// Give articles a unique UID depending on its track list
|
||||
// Releases should then show up as new articles when tracks are added
|
||||
if ($this->getInput('type') === 'changes') {
|
||||
$item['uid'] = "bandcamp/$band_data->band_id/$tralbum_data->id/";
|
||||
foreach ($tralbum_data->tracks as $track) {
|
||||
$item['uid'] .= $track->track_id;
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] = "<img src='$small_img' /><br/>$full_title<br/>";
|
||||
if ($tralbum_data->type === 'a') {
|
||||
$item['content'] .= '<ol>';
|
||||
foreach ($tralbum_data->tracks as $track) {
|
||||
$item['content'] .= "<li>$track->title</li>";
|
||||
}
|
||||
$item['content'] .= '</ol>';
|
||||
}
|
||||
if (!empty($tralbum_data->about)) {
|
||||
$item['content'] .= '<p>'
|
||||
. nl2br($tralbum_data->about)
|
||||
. '</p>';
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function buildRequestJson()
|
||||
{
|
||||
$requestJson = [
|
||||
'tag' => $this->getInput('tag'),
|
||||
'page' => 1,
|
||||
'sort' => 'date'
|
||||
];
|
||||
return json_encode($requestJson);
|
||||
}
|
||||
|
||||
private function getImageUrl($id, $size)
|
||||
{
|
||||
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
|
||||
}
|
||||
|
||||
private function apiGet($endpoint, $query_data)
|
||||
{
|
||||
$url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
|
||||
// todo: 429 Too Many Requests happens a lot
|
||||
$response = getContents($url);
|
||||
$data = json_decode($response);
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'By tag':
|
||||
if (!is_null($this->getInput('tag'))) {
|
||||
return self::URI
|
||||
. 'tag/'
|
||||
. urlencode($this->getInput('tag'))
|
||||
. '?sort_field=date';
|
||||
}
|
||||
break;
|
||||
case 'By label':
|
||||
if (!is_null($this->getInput('label'))) {
|
||||
return 'https://'
|
||||
. $this->getInput('label')
|
||||
. '.bandcamp.com/music';
|
||||
}
|
||||
break;
|
||||
case 'By band':
|
||||
if (!is_null($this->getInput('band'))) {
|
||||
return 'https://'
|
||||
. $this->getInput('band')
|
||||
. '.bandcamp.com/music';
|
||||
}
|
||||
break;
|
||||
case 'By album':
|
||||
if (!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) {
|
||||
return 'https://'
|
||||
. $this->getInput('band')
|
||||
. '.bandcamp.com/album/'
|
||||
. $this->getInput('album');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'By tag':
|
||||
if (!is_null($this->getInput('tag'))) {
|
||||
return $this->getInput('tag') . ' - Bandcamp Tag';
|
||||
}
|
||||
break;
|
||||
case 'By band':
|
||||
if (isset($this->feedName)) {
|
||||
return $this->feedName . ' - Bandcamp Band';
|
||||
} elseif (!is_null($this->getInput('band'))) {
|
||||
return $this->getInput('band') . ' - Bandcamp Band';
|
||||
}
|
||||
break;
|
||||
case 'By label':
|
||||
if (isset($this->feedName)) {
|
||||
return $this->feedName . ' - Bandcamp Label';
|
||||
} elseif (!is_null($this->getInput('label'))) {
|
||||
return $this->getInput('label') . ' - Bandcamp Label';
|
||||
}
|
||||
break;
|
||||
case 'By album':
|
||||
if (isset($this->feedName)) {
|
||||
return $this->feedName . ' - Bandcamp Album';
|
||||
} elseif (!is_null($this->getInput('album'))) {
|
||||
return $this->getInput('album') . ' - Bandcamp Album';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function detectParameters($url)
|
||||
{
|
||||
$params = [];
|
||||
|
||||
// By tag
|
||||
$regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/';
|
||||
if (preg_match($regex, $url, $matches) > 0) {
|
||||
$params['context'] = 'By tag';
|
||||
$params['tag'] = urldecode($matches[2]);
|
||||
return $params;
|
||||
}
|
||||
|
||||
// By band
|
||||
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
|
||||
if (preg_match($regex, $url, $matches) > 0) {
|
||||
$params['context'] = 'By band';
|
||||
$params['band'] = urldecode($matches[2]);
|
||||
return $params;
|
||||
}
|
||||
|
||||
// By album
|
||||
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
|
||||
if (preg_match($regex, $url, $matches) > 0) {
|
||||
$params['context'] = 'By album';
|
||||
$params['band'] = urldecode($matches[2]);
|
||||
$params['album'] = urldecode($matches[3]);
|
||||
return $params;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
|
@ -1,162 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BandcampDailyBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bandcamp Daily Bridge';
|
||||
const URI = 'https://daily.bandcamp.com';
|
||||
const DESCRIPTION = 'Returns newest articles';
|
||||
const MAINTAINER = 'VerifiedJoseph';
|
||||
const PARAMETERS = [
|
||||
'Latest articles' => [],
|
||||
'Best of' => [
|
||||
'best-content' => [
|
||||
'name' => 'content',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Best Ambient' => 'best-ambient',
|
||||
'Best Beat Tapes' => 'best-beat-tapes',
|
||||
'Best Dance 12\'s' => 'best-dance-12s',
|
||||
'Best Contemporary Classical' => 'best-contemporary-classical',
|
||||
'Best Electronic' => 'best-electronic',
|
||||
'Best Experimental' => 'best-experimental',
|
||||
'Best Hip-Hop' => 'best-hip-hop',
|
||||
'Best Jazz' => 'best-jazz',
|
||||
'Best Metal' => 'best-metal',
|
||||
'Best Punk' => 'best-punk',
|
||||
'Best Reissues' => 'best-reissues',
|
||||
'Best Soul' => 'best-soul',
|
||||
],
|
||||
'defaultValue' => 'best-ambient',
|
||||
],
|
||||
],
|
||||
'Genres' => [
|
||||
'genres-content' => [
|
||||
'name' => 'content',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Acoustic' => 'genres/acoustic',
|
||||
'Alternative' => 'genres/alternative',
|
||||
'Ambient' => 'genres/ambient',
|
||||
'Blues' => 'genres/blues',
|
||||
'Classical' => 'genres/classical',
|
||||
'Comedy' => 'genres/comedy',
|
||||
'Country' => 'genres/country',
|
||||
'Devotional' => 'genres/devotional',
|
||||
'Electronic' => 'genres/electronic',
|
||||
'Experimental' => 'genres/experimental',
|
||||
'Folk' => 'genres/folk',
|
||||
'Funk' => 'genres/funk',
|
||||
'Hip-Hop/Rap' => 'genres/hip-hop-rap',
|
||||
'Jazz' => 'genres/jazz',
|
||||
'Kids' => 'genres/kids',
|
||||
'Latin' => 'genres/latin',
|
||||
'Metal' => 'genres/metal',
|
||||
'Pop' => 'genres/pop',
|
||||
'Punk' => 'genres/punk',
|
||||
'R&B/Soul' => 'genres/r-b-soul',
|
||||
'Reggae' => 'genres/reggae',
|
||||
'Rock' => 'genres/rock',
|
||||
'Soundtrack' => 'genres/soundtrack',
|
||||
'Spoken Word' => 'genres/spoken-word',
|
||||
'World' => 'genres/world',
|
||||
],
|
||||
'defaultValue' => 'genres/acoustic',
|
||||
],
|
||||
],
|
||||
'Franchises' => [
|
||||
'franchises-content' => [
|
||||
'name' => 'content',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Lists' => 'lists',
|
||||
'Features' => 'features',
|
||||
'Album of the Day' => 'album-of-the-day',
|
||||
'Acid Test' => 'acid-test',
|
||||
'Bandcamp Navigator' => 'bandcamp-navigator',
|
||||
'Big Ups' => 'big-ups',
|
||||
'Certified' => 'certified',
|
||||
'Gallery' => 'gallery',
|
||||
'Hidden Gems' => 'hidden-gems',
|
||||
'High Scores' => 'high-scores',
|
||||
'Label Profile' => 'label-profile',
|
||||
'Lifetime Achievement' => 'lifetime-achievement',
|
||||
'Scene Report' => 'scene-report',
|
||||
'Seven Essential Releases' => 'seven-essential-releases',
|
||||
'The Merch Table' => 'the-merch-table',
|
||||
],
|
||||
'defaultValue' => 'lists',
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
$articles = $html->find('articles-list', 0);
|
||||
|
||||
foreach ($articles->find('div.list-article') as $index => $article) {
|
||||
$item = [];
|
||||
|
||||
$articlePath = $article->find('a.title', 0)->href;
|
||||
|
||||
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600);
|
||||
|
||||
$item['uri'] = $articlePath;
|
||||
$item['title'] = $articlePageHtml->find('article-title', 0)->innertext;
|
||||
$item['author'] = $articlePageHtml->find('article-credits > a', 0)->innertext;
|
||||
$item['content'] = html_entity_decode($articlePageHtml->find('meta[name="description"]', 0)->content, ENT_QUOTES);
|
||||
$item['timestamp'] = $articlePageHtml->find('meta[property="article:published_time"]', 0)->content;
|
||||
$item['categories'][] = $articlePageHtml->find('meta[property="article:section"]', 0)->content;
|
||||
|
||||
if ($articlePageHtml->find('meta[property="article:tag"]', 0)) {
|
||||
$item['categories'][] = $articlePageHtml->find('meta[property="article:tag"]', 0)->content;
|
||||
}
|
||||
|
||||
$item['enclosures'][] = $articlePageHtml->find('meta[name="twitter:image"]', 0)->content;
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
if (count($this->items) >= 10) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Latest articles':
|
||||
return self::URI . '/latest';
|
||||
case 'Best of':
|
||||
case 'Genres':
|
||||
case 'Franchises':
|
||||
// TODO Switch to array_key_first once php >= 7.3
|
||||
$contentKey = key(self::PARAMETERS[$this->queriedContext]);
|
||||
return self::URI . '/' . $this->getInput($contentKey);
|
||||
default:
|
||||
return parent::getURI();
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Latest articles':
|
||||
return $this->queriedContext . ' - Bandcamp Daily';
|
||||
case 'Best of':
|
||||
case 'Genres':
|
||||
case 'Franchises':
|
||||
$contentKey = array_key_first(self::PARAMETERS[$this->queriedContext]);
|
||||
$contentValues = array_flip(self::PARAMETERS[$this->queriedContext][$contentKey]['values']);
|
||||
|
||||
return $contentValues[$this->getInput($contentKey)] . ' - Bandcamp Daily';
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,34 @@
|
||||
<?php
|
||||
class BastaBridge extends BridgeAbstract {
|
||||
|
||||
class BastaBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'qwertygc';
|
||||
const NAME = 'Bastamag Bridge';
|
||||
const URI = 'https://www.bastamag.net/';
|
||||
const CACHE_TIMEOUT = 7200; // 2h
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
const MAINTAINER = 'qwertygc';
|
||||
const NAME = 'Bastamag Bridge';
|
||||
const URI = 'http://www.bastamag.net/';
|
||||
const CACHE_TIMEOUT = 7200; // 2h
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend');
|
||||
public function collectData(){
|
||||
// Replaces all relative image URLs by absolute URLs.
|
||||
// Relative URLs always start with 'local/'!
|
||||
function replaceImageUrl($content){
|
||||
return preg_replace('/src=["\']{1}([^"\']+)/ims', 'src=\'' . self::URI . '$1\'', $content);
|
||||
}
|
||||
|
||||
$limit = 0;
|
||||
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend')
|
||||
or returnServerError('Could not request Bastamag.');
|
||||
|
||||
foreach ($html->find('item') as $element) {
|
||||
if ($limit < 10) {
|
||||
$item = [];
|
||||
$item['title'] = $element->find('title', 0)->innertext;
|
||||
$item['uri'] = $element->find('guid', 0)->plaintext;
|
||||
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
|
||||
$limit = 0;
|
||||
|
||||
$html = getSimpleHTMLDOM($item['uri']);
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
$item['content'] = $html->find('div.texte', 0)->innertext;
|
||||
$this->items[] = $item;
|
||||
$limit++;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach($html->find('item') as $element) {
|
||||
if($limit < 10) {
|
||||
$item = array();
|
||||
$item['title'] = $element->find('title', 0)->innertext;
|
||||
$item['uri'] = $element->find('guid', 0)->plaintext;
|
||||
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);
|
||||
$item['content'] = replaceImageUrl(getSimpleHTMLDOM($item['uri'])->find('div.texte', 0)->innertext);
|
||||
$this->items[] = $item;
|
||||
$limit++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BinanceBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Binance Blog';
|
||||
const URI = 'https://www.binance.com/en/blog';
|
||||
const DESCRIPTION = 'Subscribe to the Binance blog.';
|
||||
const MAINTAINER = 'thefranke';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = 'https://www.binance.com/bapi/composite/v1/public/content/blog/list?category=&tag=&page=1&size=12';
|
||||
$json = getContents($url);
|
||||
$data = Json::decode($json, false);
|
||||
foreach ($data->data->blogList as $post) {
|
||||
$item = [];
|
||||
$item['title'] = $post->title;
|
||||
// Url slug not in json
|
||||
//$item['uri'] = $uri;
|
||||
$item['timestamp'] = $post->postTimeUTC / 1000;
|
||||
$item['author'] = 'Binance';
|
||||
$item['content'] = $post->brief;
|
||||
//$item['categories'] = $category;
|
||||
$item['uid'] = $post->idStr;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://bin.bnbstatic.com/static/images/common/favicon.ico';
|
||||
}
|
||||
}
|
@ -1,44 +1,47 @@
|
||||
<?php
|
||||
class BlaguesDeMerdeBridge extends BridgeAbstract {
|
||||
|
||||
class BlaguesDeMerdeBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'superbaillot.net, logmanoriginal';
|
||||
const NAME = 'Blagues De Merde';
|
||||
const URI = 'http://www.blaguesdemerde.fr/';
|
||||
const CACHE_TIMEOUT = 7200; // 2h
|
||||
const DESCRIPTION = 'Blagues De Merde';
|
||||
const MAINTAINER = 'superbaillot.net, logmanoriginal';
|
||||
const NAME = 'Blagues De Merde';
|
||||
const URI = 'http://www.blaguesdemerde.fr/';
|
||||
const CACHE_TIMEOUT = 7200; // 2h
|
||||
const DESCRIPTION = 'Blagues De Merde';
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return self::URI . 'assets/img/favicon.ico';
|
||||
}
|
||||
public function getIcon() {
|
||||
return self::URI . 'assets/img/favicon.ico';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
public function collectData(){
|
||||
|
||||
foreach ($html->find('div.blague') as $element) {
|
||||
$item = [];
|
||||
$html = getSimpleHTMLDOM(self::URI)
|
||||
or returnServerError('Could not request BDM.');
|
||||
|
||||
$item['uri'] = static::URI . '#' . $element->id;
|
||||
$item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext;
|
||||
foreach($html->find('div.blague') as $element) {
|
||||
|
||||
// Let the title be everything up to the first <br>
|
||||
$item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]);
|
||||
$item = array();
|
||||
|
||||
$item['content'] = strip_tags($element->find('div.text', 0));
|
||||
$item['uri'] = static::URI . '#' . $element->id;
|
||||
$item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext;
|
||||
|
||||
// timestamp is part of:
|
||||
// <p>Par <strong>{author}</strong> le {date} dans <strong>{category}</strong></p>
|
||||
preg_match(
|
||||
'/.+le(.+)dans.*/',
|
||||
$element->find('div[class="blague-footer"]', 0)->plaintext,
|
||||
$matches
|
||||
);
|
||||
// Let the title be everything up to the first <br>
|
||||
$item['title'] = trim(explode("\n", $element->find('div.text', 0)->plaintext)[0]);
|
||||
|
||||
$item['timestamp'] = strtotime($matches[1]);
|
||||
$item['content'] = strip_tags($element->find('div.text', 0));
|
||||
|
||||
// timestamp is part of:
|
||||
// <p>Par <strong>{author}</strong> le {date} dans <strong>{category}</strong></p>
|
||||
preg_match(
|
||||
'/.+le(.+)dans.*/',
|
||||
$element->find('div[class="blague-footer"]', 0)->plaintext,
|
||||
$matches
|
||||
);
|
||||
|
||||
$item['timestamp'] = strtotime($matches[1]);
|
||||
|
||||
$this->items[] = $item;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BleepingComputerBridge extends FeedExpander
|
||||
{
|
||||
const MAINTAINER = 'csisoap';
|
||||
const NAME = 'Bleeping Computer';
|
||||
const URI = 'https://www.bleepingcomputer.com/';
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$feed = static::URI . 'feed/';
|
||||
$this->collectExpandableDatas($feed);
|
||||
}
|
||||
|
||||
protected function parseItem(array $item)
|
||||
{
|
||||
$article_html = getSimpleHTMLDOMCached($item['uri']);
|
||||
if (!$article_html) {
|
||||
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
|
||||
return $item;
|
||||
}
|
||||
|
||||
$article_content = $article_html->find('div.articleBody', 0)->innertext;
|
||||
$article_content = stripRecursiveHTMLSection($article_content, 'div', '<div class="cz-related-article-wrapp');
|
||||
$item['content'] = trim($article_content);
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BlizzardNewsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Blizzard News';
|
||||
const URI = 'https://news.blizzard.com';
|
||||
const DESCRIPTION = 'Blizzard (game company) newsfeed';
|
||||
const MAINTAINER = 'Niehztog';
|
||||
const PARAMETERS = [
|
||||
'' => [
|
||||
'locale' => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Deutsch' => 'de-de',
|
||||
'English (EU)' => 'en-gb',
|
||||
'English (US)' => 'en-us',
|
||||
'Español (EU)' => 'es-es',
|
||||
'Español (AL)' => 'es-mx',
|
||||
'Français' => 'fr-fr',
|
||||
'Italiano' => 'it-it',
|
||||
'日本語' => 'ja-jp',
|
||||
'한국어' => 'ko-kr',
|
||||
'Polski' => 'pl-pl',
|
||||
'Português (AL)' => 'pt-br',
|
||||
'Русский' => 'ru-ru',
|
||||
'ภาษาไทย' => 'th-th',
|
||||
'简体中文' => 'zh-cn',
|
||||
'繁體中文' => 'zh-tw'
|
||||
],
|
||||
'defaultValue' => 'en-us',
|
||||
'title' => 'Select your language'
|
||||
]
|
||||
]
|
||||
];
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
private const PRODUCT_IDS = [
|
||||
'blt525c436e4a1b0a97',
|
||||
'blt54fbd3787a705054',
|
||||
'blt2031aef34200656d',
|
||||
'blt795c314400d7ded9',
|
||||
'blt5cfc6affa3ca0638',
|
||||
'blt2e50e1521bb84dc6',
|
||||
'blt376fb94931906b6f',
|
||||
'blt81d46fcb05ab8811',
|
||||
'bltede2389c0a8885aa',
|
||||
'blt24859ba8086fb294',
|
||||
'blte27d02816a8ff3e1',
|
||||
'blt2caca37e42f19839',
|
||||
'blt90855744d00cd378',
|
||||
'bltec70ad0ea4fd6d1d',
|
||||
'blt500c1f8b5470bfdb'
|
||||
];
|
||||
|
||||
private const API_PATH = '/api/news/blizzard?';
|
||||
|
||||
/**
|
||||
* Source Web page URL (should provide either HTML or XML content)
|
||||
* @return string
|
||||
*/
|
||||
private function getSourceUrl(): string
|
||||
{
|
||||
$locale = $this->getInput('locale');
|
||||
if ('zh-cn' === $locale) {
|
||||
$baseUrl = 'https://cn.news.blizzard.com' . self::API_PATH;
|
||||
} else {
|
||||
$baseUrl = 'https://news.blizzard.com/' . $locale . self::API_PATH;
|
||||
}
|
||||
return $baseUrl .= http_build_query([
|
||||
'feedCxpProductIds' => self::PRODUCT_IDS
|
||||
]);
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$feedContent = json_decode(getContents($this->getSourceUrl()), true);
|
||||
|
||||
foreach ($feedContent['feed']['contentItems'] as $entry) {
|
||||
$properties = $entry['properties'];
|
||||
|
||||
$item = [];
|
||||
|
||||
$item['title'] = $this->filterChars($properties['title']);
|
||||
$item['content'] = $this->filterChars($properties['summary']);
|
||||
$item['uri'] = $properties['newsUrl'];
|
||||
$item['author'] = $this->filterChars($properties['author']);
|
||||
$item['timestamp'] = strtotime($properties['lastUpdated']);
|
||||
$item['enclosures'] = [$properties['staticAsset']['imageUrl']];
|
||||
$item['categories'] = [$this->filterChars($properties['cxpProduct']['title'])];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function filterChars($content)
|
||||
{
|
||||
return htmlspecialchars($content, ENT_XML1);
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return <<<icon
|
||||
https://dfbmfbnnydoln.cloudfront.net/production/images/favicons/favicon.ba01bb119359d74970b02902472fd82e96b5aba7.ico
|
||||
icon;
|
||||
}
|
||||
}
|
69
bridges/BloombergBridge.php
Normal file
69
bridges/BloombergBridge.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
class BloombergBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bloomberg';
|
||||
const URI = 'https://www.bloomberg.com/';
|
||||
const DESCRIPTION = 'Trending stories from Bloomberg';
|
||||
const MAINTAINER = 'mdemoss';
|
||||
|
||||
const PARAMETERS = array(
|
||||
'Trending Stories' => array(),
|
||||
'From Search' => array(
|
||||
'q' => array(
|
||||
'name' => 'Keyword',
|
||||
'required' => true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function getName()
|
||||
{
|
||||
switch($this->queriedContext) {
|
||||
case 'Trending Stories':
|
||||
return self::NAME . ' Trending Stories';
|
||||
case 'From Search':
|
||||
if (!is_null($this->getInput('q'))) {
|
||||
return self::NAME . ' Search : ' . $this->getInput('q');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getIcon() {
|
||||
return 'https://assets.bwbx.io/s3/javelin/public/hub/images/favicon-black-63fe5249d3.png';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
switch($this->queriedContext) {
|
||||
case 'Trending Stories': // Get list of top new <article>s from the front page.
|
||||
$html = getSimpleHTMLDOMCached($this->getURI(), 300);
|
||||
$stories = $html->find('ul.top-news-v3__stories article.top-news-v3-story');
|
||||
break;
|
||||
case 'From Search': // Get list of <article> elements from search.
|
||||
$html = getSimpleHTMLDOMCached(
|
||||
$this->getURI() .
|
||||
'search?sort=time:desc&page=1&query=' .
|
||||
urlencode($this->getInput('q')), 300
|
||||
);
|
||||
$stories = $html->find('div.search-result-items article.search-result-story');
|
||||
break;
|
||||
}
|
||||
foreach ($stories as $element) {
|
||||
$item['uri'] = $element->find('h1 a', 0)->href;
|
||||
if (preg_match('#^https://#i', $item['uri']) !== 1) {
|
||||
$item['uri'] = $this->getURI() . $item['uri'];
|
||||
}
|
||||
$articleHtml = getSimpleHTMLDOMCached($item['uri']);
|
||||
if (!$articleHtml) {
|
||||
continue;
|
||||
}
|
||||
$item['title'] = $element->find('h1 a', 0)->plaintext;
|
||||
$item['timestamp'] = strtotime($articleHtml->find('meta[name=iso-8601-publish-date],meta[name=date]', 0)->content);
|
||||
$item['content'] = $articleHtml->find('meta[name=description]', 0)->content;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,620 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BlueskyBridge extends BridgeAbstract
|
||||
{
|
||||
//Initial PR by [RSSBridge contributors](https://github.com/RSS-Bridge/rss-bridge/issues/4058).
|
||||
//Modified from [©DIYgod and contributors at RSSHub](https://github.com/DIYgod/RSSHub/tree/master/lib/routes/bsky), MIT License';
|
||||
const NAME = 'Bluesky Bridge';
|
||||
const URI = 'https://bsky.app';
|
||||
const DESCRIPTION = 'Fetches posts from Bluesky';
|
||||
const MAINTAINER = 'mruac';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'data_source' => [
|
||||
'name' => 'Bluesky Data Source',
|
||||
'type' => 'list',
|
||||
'defaultValue' => 'Profile',
|
||||
'values' => [
|
||||
'Profile' => 'getAuthorFeed',
|
||||
],
|
||||
'title' => 'Select the type of data source to fetch from Bluesky.'
|
||||
],
|
||||
'user_id' => [
|
||||
'name' => 'User Handle or DID',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'title' => 'ATProto / Bsky.app handle or DID'
|
||||
],
|
||||
'feed_filter' => [
|
||||
'name' => 'Feed type',
|
||||
'type' => 'list',
|
||||
'defaultValue' => 'posts_and_author_threads',
|
||||
'values' => [
|
||||
'Posts feed' => 'posts_and_author_threads',
|
||||
'All posts and replies' => 'posts_with_replies',
|
||||
'Root posts only' => 'posts_no_replies',
|
||||
'Media only' => 'posts_with_media',
|
||||
]
|
||||
],
|
||||
|
||||
'include_reposts' => [
|
||||
'name' => 'Include Reposts?',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => 'checked'
|
||||
],
|
||||
|
||||
'include_reply_context' => [
|
||||
'name' => 'Include Reply context?',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
|
||||
'verbose_title' => [
|
||||
'name' => 'Use verbose feed item titles?',
|
||||
'type' => 'checkbox'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
private $profile;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
if ($this->profile['handle'] === 'handle.invalid') {
|
||||
return sprintf('Bluesky - %s', $this->profile['displayName']);
|
||||
} else {
|
||||
return sprintf('Bluesky - %s (@%s)', $this->profile['displayName'], $this->profile['handle']);
|
||||
}
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
if ($this->profile['handle'] === 'handle.invalid') {
|
||||
return self::URI . '/profile/' . $this->profile['did'];
|
||||
} else {
|
||||
return self::URI . '/profile/' . $this->profile['handle'];
|
||||
}
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
return $this->profile['avatar'];
|
||||
}
|
||||
return parent::getIcon();
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
return $this->profile['description'];
|
||||
}
|
||||
return parent::getDescription();
|
||||
}
|
||||
|
||||
private function parseExternal($external, $did)
|
||||
{
|
||||
$description = '';
|
||||
$externalUri = $external['uri'];
|
||||
$externalTitle = e($external['title']);
|
||||
$externalDescription = e($external['description']);
|
||||
$thumb = $external['thumb'] ?? null;
|
||||
|
||||
if (preg_match('/http(|s):\/\/media\.tenor\.com/', $externalUri)) {
|
||||
//tenor gif embed
|
||||
$tenorInterstitial = str_replace('media.tenor.com', 'media1.tenor.com/m', $externalUri);
|
||||
$description .= "<figure><a href=\"$tenorInterstitial\"><img src=\"$externalUri\"/></a><figcaption>$externalTitle</figcaption></figure>";
|
||||
} else {
|
||||
//link embed preview
|
||||
$host = parse_url($externalUri)['host'];
|
||||
$thumbDesc = $thumb ? ('<img src="https://cdn.bsky.app/img/feed_thumbnail/plain/' . $did . '/' . $thumb['ref']['$link'] . '@jpeg"/>') : '';
|
||||
$externalDescription = strlen($externalDescription) > 0 ? "<figcaption>($host) $externalDescription</figcaption>" : '';
|
||||
$description .= '<br><blockquote><b><a href="' . $externalUri . '">' . $externalTitle . '</a></b>';
|
||||
$description .= '<figure>' . $thumbDesc . $externalDescription . '</figure></blockquote>';
|
||||
}
|
||||
return $description;
|
||||
}
|
||||
|
||||
private function textToDescription($record)
|
||||
{
|
||||
if (isset($record['value'])) {
|
||||
$record = $record['value'];
|
||||
}
|
||||
$text = $record['text'];
|
||||
$text_copy = $text;
|
||||
$text = nl2br(e($text));
|
||||
if (isset($record['facets'])) {
|
||||
$facets = $record['facets'];
|
||||
foreach ($facets as $facet) {
|
||||
if ($facet['features'][0]['$type'] === 'app.bsky.richtext.facet#link') {
|
||||
$substring = substr($text_copy, $facet['index']['byteStart'], $facet['index']['byteEnd'] - $facet['index']['byteStart']);
|
||||
$text = str_replace($substring, '<a href="' . $facet['features'][0]['uri'] . '">' . $substring . '</a>', $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$user_id = $this->getInput('user_id');
|
||||
$handle_match = preg_match('/(?:[a-zA-Z]*\.)+([a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)/', $user_id, $handle_res); //gets the TLD in $handle_match[1]
|
||||
$did_match = preg_match('/did:plc:[a-z2-7]{24}/', $user_id); //https://github.com/did-method-plc/did-method-plc#identifier-syntax
|
||||
$exclude = ['alt', 'arpa', 'example', 'internal', 'invalid', 'local', 'localhost', 'onion']; //https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
|
||||
if ($handle_match == true && array_search($handle_res[1], $exclude) == false) {
|
||||
//valid bsky handle
|
||||
$did = $this->resolveHandle($user_id);
|
||||
} elseif ($did_match == true) {
|
||||
//valid DID
|
||||
$did = $user_id;
|
||||
} else {
|
||||
returnClientError('Invalid ATproto handle or DID provided.');
|
||||
}
|
||||
|
||||
$filter = $this->getInput('feed_filter') ?: 'posts_and_author_threads';
|
||||
$replyContext = $this->getInput('include_reply_context');
|
||||
|
||||
$this->profile = $this->getProfile($did);
|
||||
$authorFeed = $this->getAuthorFeed($did, $filter);
|
||||
|
||||
foreach ($authorFeed['feed'] as $post) {
|
||||
$postRecord = $post['post']['record'];
|
||||
|
||||
$item = [];
|
||||
$item['uri'] = self::URI . '/profile/' . $this->fallbackAuthor($post['post']['author'], 'url') . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
|
||||
$item['title'] = $this->getInput('verbose_title') ? $this->generateVerboseTitle($post) : strtok($postRecord['text'], "\n");
|
||||
$item['timestamp'] = strtotime($postRecord['createdAt']);
|
||||
$item['author'] = $this->fallbackAuthor($post['post']['author'], 'display');
|
||||
|
||||
$postAuthorDID = $post['post']['author']['did'];
|
||||
$postAuthorHandle = $post['post']['author']['handle'] !== 'handle.invalid' ? '<i>@' . $post['post']['author']['handle'] . '</i> ' : '';
|
||||
$postDisplayName = $post['post']['author']['displayName'] ?? '';
|
||||
$postDisplayName = e($postDisplayName);
|
||||
$postUri = $item['uri'];
|
||||
|
||||
if (Debug::isEnabled()) {
|
||||
$url = explode('/', $post['post']['uri']);
|
||||
error_log('https://bsky.app/profile/' . $url[2] . '/post/' . $url[4]);
|
||||
}
|
||||
|
||||
$description = '';
|
||||
$description .= '<p>';
|
||||
//post
|
||||
$description .= $this->getPostDescription(
|
||||
$postDisplayName,
|
||||
$postAuthorHandle,
|
||||
$postUri,
|
||||
$postRecord,
|
||||
'post'
|
||||
);
|
||||
|
||||
if (isset($postRecord['embed']['$type'])) {
|
||||
//post link embed
|
||||
if ($postRecord['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($postRecord['embed']['external'], $postAuthorDID);
|
||||
} elseif (
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$postRecord['embed']['media']['$type'] === 'app.bsky.embed.external'
|
||||
) {
|
||||
$description .= $this->parseExternal($postRecord['embed']['media']['external'], $postAuthorDID);
|
||||
}
|
||||
|
||||
//post images
|
||||
if (
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$postRecord['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
$images = $post['post']['embed']['images'] ?? $post['post']['embed']['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
|
||||
//post video
|
||||
if (
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$postRecord['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$postRecord['embed']['video'] ?? $postRecord['embed']['media']['video'],
|
||||
$postAuthorDID
|
||||
);
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
|
||||
//quote post
|
||||
if (
|
||||
isset($postRecord['embed']) &&
|
||||
(
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.record' ||
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia'
|
||||
) &&
|
||||
isset($post['post']['embed']['record'])
|
||||
) {
|
||||
$description .= '<p>';
|
||||
$quotedRecord = $post['post']['embed']['record']['record'] ?? $post['post']['embed']['record'];
|
||||
|
||||
if (isset($quotedRecord['notFound']) && $quotedRecord['notFound']) { //deleted post
|
||||
$description .= 'Quoted post deleted.';
|
||||
} elseif (isset($quotedRecord['detached']) && $quotedRecord['detached']) { //detached quote
|
||||
$uri_explode = explode('/', $quotedRecord['uri']);
|
||||
$uri_reconstructed = self::URI . '/profile/' . $uri_explode[2] . '/post/' . $uri_explode[4];
|
||||
$description .= '<a href="' . $uri_reconstructed . '">Quoted post detached.</a>';
|
||||
} elseif (isset($quotedRecord['blocked']) && $quotedRecord['blocked']) { //blocked by quote author
|
||||
$description .= 'Author of quoted post has blocked OP.';
|
||||
} elseif (($quotedRecord['$type'] ?? '') === 'app.bsky.feed.defs#generatorView') {
|
||||
$description .= '</p>';
|
||||
$description .= $this->getGeneratorViewDescription($quotedRecord);
|
||||
$description .= '<p>';
|
||||
} else {
|
||||
$quotedAuthorDid = $quotedRecord['author']['did'];
|
||||
$quotedDisplayName = $quotedRecord['author']['displayName'] ?? '';
|
||||
$quotedDisplayName = e($quotedDisplayName);
|
||||
$quotedAuthorHandle = $quotedRecord['author']['handle'] !== 'handle.invalid' ? '<i>@' . $quotedRecord['author']['handle'] . '</i>' : '';
|
||||
|
||||
$parts = explode('/', $quotedRecord['uri']);
|
||||
$quotedPostId = end($parts);
|
||||
$quotedPostUri = self::URI . '/profile/' . $this->fallbackAuthor($quotedRecord['author'], 'url') . '/post/' . $quotedPostId;
|
||||
|
||||
//quoted post - post
|
||||
$description .= $this->getPostDescription(
|
||||
$quotedDisplayName,
|
||||
$quotedAuthorHandle,
|
||||
$quotedPostUri,
|
||||
$quotedRecord,
|
||||
'quote'
|
||||
);
|
||||
|
||||
if (isset($quotedRecord['value']['embed']['$type'])) {
|
||||
//quoted post - post link embed
|
||||
if ($quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($quotedRecord['value']['embed']['external'], $quotedAuthorDid);
|
||||
}
|
||||
|
||||
//quoted post - post video
|
||||
if (
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$quotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$quotedRecord['value']['embed']['video'] ?? $quotedRecord['value']['embed']['media']['video'],
|
||||
$quotedAuthorDid
|
||||
);
|
||||
}
|
||||
|
||||
//quoted post - post images
|
||||
if (
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$quotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
foreach ($quotedRecord['embeds'] as $embed) {
|
||||
if (
|
||||
$embed['$type'] === 'app.bsky.embed.images#view' ||
|
||||
($embed['$type'] === 'app.bsky.embed.recordWithMedia#view' && $embed['media']['$type'] === 'app.bsky.embed.images#view')
|
||||
) {
|
||||
$images = $embed['images'] ?? $embed['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
}
|
||||
|
||||
//reply
|
||||
if ($replyContext && isset($post['reply']) && !isset($post['reply']['parent']['notFound'])) {
|
||||
$replyPost = $post['reply']['parent'];
|
||||
$replyPostRecord = $replyPost['record'];
|
||||
$description .= '<hr/>';
|
||||
$description .= '<p>';
|
||||
|
||||
$replyPostAuthorDID = $replyPost['author']['did'];
|
||||
$replyPostAuthorHandle = $replyPost['author']['handle'] !== 'handle.invalid' ? '<i>@' . $replyPost['author']['handle'] . '</i> ' : '';
|
||||
$replyPostDisplayName = $replyPost['author']['displayName'] ?? '';
|
||||
$replyPostDisplayName = e($replyPostDisplayName);
|
||||
$replyPostUri = self::URI . '/profile/' . $this->fallbackAuthor($replyPost['author'], 'url') . '/post/' . explode('app.bsky.feed.post/', $replyPost['uri'])[1];
|
||||
|
||||
// reply post
|
||||
$description .= $this->getPostDescription(
|
||||
$replyPostDisplayName,
|
||||
$replyPostAuthorHandle,
|
||||
$replyPostUri,
|
||||
$replyPostRecord,
|
||||
'reply'
|
||||
);
|
||||
|
||||
if (isset($replyPostRecord['embed']['$type'])) {
|
||||
//post link embed
|
||||
if ($replyPostRecord['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($replyPostRecord['embed']['external'], $replyPostAuthorDID);
|
||||
} elseif (
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.external'
|
||||
) {
|
||||
$description .= $this->parseExternal($replyPostRecord['embed']['media']['external'], $replyPostAuthorDID);
|
||||
}
|
||||
|
||||
//post images
|
||||
if (
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
$images = $replyPost['embed']['images'] ?? $replyPost['embed']['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
|
||||
//post video
|
||||
if (
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$replyPostRecord['embed']['video'] ?? $replyPostRecord['embed']['media']['video'],
|
||||
$replyPostAuthorDID
|
||||
);
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
|
||||
//quote post
|
||||
if (
|
||||
isset($replyPostRecord['embed']) &&
|
||||
($replyPostRecord['embed']['$type'] === 'app.bsky.embed.record' || $replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia') &&
|
||||
isset($replyPost['embed']['record'])
|
||||
) {
|
||||
$description .= '<p>';
|
||||
$replyQuotedRecord = $replyPost['embed']['record']['record'] ?? $replyPost['embed']['record'];
|
||||
|
||||
if (isset($replyQuotedRecord['notFound']) && $replyQuotedRecord['notFound']) { //deleted post
|
||||
$description .= 'Quoted post deleted.';
|
||||
} elseif (isset($replyQuotedRecord['detached']) && $replyQuotedRecord['detached']) { //detached quote
|
||||
$uri_explode = explode('/', $replyQuotedRecord['uri']);
|
||||
$uri_reconstructed = self::URI . '/profile/' . $uri_explode[2] . '/post/' . $uri_explode[4];
|
||||
$description .= '<a href="' . $uri_reconstructed . '">Quoted post detached.</a>';
|
||||
} elseif (isset($replyQuotedRecord['blocked']) && $replyQuotedRecord['blocked']) { //blocked by quote author
|
||||
$description .= 'Author of quoted post has blocked OP.';
|
||||
} elseif (($replyQuotedRecord['$type'] ?? '') === 'app.bsky.feed.defs#generatorView') {
|
||||
$description .= '</p>';
|
||||
$description .= $this->getGeneratorViewDescription($replyQuotedRecord);
|
||||
$description .= '<p>';
|
||||
} else {
|
||||
$quotedAuthorDid = $replyQuotedRecord['author']['did'];
|
||||
$quotedDisplayName = $replyQuotedRecord['author']['displayName'] ?? '';
|
||||
$quotedDisplayName = e($quotedDisplayName);
|
||||
$quotedAuthorHandle = $replyQuotedRecord['author']['handle'] !== 'handle.invalid' ? '<i>@' . $replyQuotedRecord['author']['handle'] . '</i>' : '';
|
||||
|
||||
$parts = explode('/', $replyQuotedRecord['uri']);
|
||||
$quotedPostId = end($parts);
|
||||
$quotedPostUri = self::URI . '/profile/' . $this->fallbackAuthor($replyQuotedRecord['author'], 'url') . '/post/' . $quotedPostId;
|
||||
|
||||
//quoted post - post
|
||||
$description .= $this->getPostDescription(
|
||||
$quotedDisplayName,
|
||||
$quotedAuthorHandle,
|
||||
$quotedPostUri,
|
||||
$replyQuotedRecord,
|
||||
'quote'
|
||||
);
|
||||
|
||||
if (isset($replyQuotedRecord['value']['embed']['$type'])) {
|
||||
//quoted post - post link embed
|
||||
if ($replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($replyQuotedRecord['value']['embed']['external'], $quotedAuthorDid);
|
||||
}
|
||||
|
||||
//quoted post - post video
|
||||
if (
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyQuotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$replyQuotedRecord['value']['embed']['video'] ?? $replyQuotedRecord['value']['embed']['media']['video'],
|
||||
$quotedAuthorDid
|
||||
);
|
||||
}
|
||||
|
||||
//quoted post - post images
|
||||
if (
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyQuotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
foreach ($replyQuotedRecord['embeds'] as $embed) {
|
||||
if (
|
||||
$embed['$type'] === 'app.bsky.embed.images#view' ||
|
||||
($embed['$type'] === 'app.bsky.embed.recordWithMedia#view' && $embed['media']['$type'] === 'app.bsky.embed.images#view')
|
||||
) {
|
||||
$images = $embed['images'] ?? $embed['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] = $description;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getPostVideoDescription(array $video, $authorDID)
|
||||
{
|
||||
//https://video.bsky.app/watch/$did/$cid/thumbnail.jpg
|
||||
$videoCID = $video['ref']['$link'];
|
||||
$videoMime = $video['mimeType'];
|
||||
$thumbnail = "poster=\"https://video.bsky.app/watch/$authorDID/$videoCID/thumbnail.jpg\"" ?? '';
|
||||
$videoURL = "https://bsky.social/xrpc/com.atproto.sync.getBlob?did=$authorDID&cid=$videoCID";
|
||||
return "<figure><video loop $thumbnail controls src=\"$videoURL\" type=\"$videoMime\"/></figure>";
|
||||
}
|
||||
|
||||
private function getPostImageDescription(array $image)
|
||||
{
|
||||
$thumbnailUrl = $image['thumb'];
|
||||
$fullsizeUrl = $image['fullsize'];
|
||||
$alt = strlen($image['alt']) > 0 ? '<figcaption>' . e($image['alt']) . '</figcaption>' : '';
|
||||
return "<figure><a href=\"$fullsizeUrl\"><img src=\"$thumbnailUrl\"></a>$alt</figure>";
|
||||
}
|
||||
|
||||
private function getPostDescription(
|
||||
string $postDisplayName,
|
||||
string $postAuthorHandle,
|
||||
string $postUri,
|
||||
array $postRecord,
|
||||
string $type
|
||||
) {
|
||||
$description = '';
|
||||
if ($type === 'quote') {
|
||||
// Quoted post/reply from bbb @bbb.com:
|
||||
$postType = isset($postRecord['reply']) ? 'reply' : 'post';
|
||||
$description .= "<a href=\"$postUri\">Quoted $postType</a> from <b>$postDisplayName</b> $postAuthorHandle:<br>";
|
||||
} elseif ($type === 'reply') {
|
||||
// Replying to aaa @aaa.com's post/reply:
|
||||
$postType = isset($postRecord['reply']) ? 'reply' : 'post';
|
||||
$description .= "Replying to <b>$postDisplayName</b> $postAuthorHandle's <a href=\"$postUri\">$postType</a>:<br>";
|
||||
} else {
|
||||
// aaa @aaa.com posted:
|
||||
$description .= "<b>$postDisplayName</b> $postAuthorHandle <a href=\"$postUri\">posted</a>:<br>";
|
||||
}
|
||||
$description .= $this->textToDescription($postRecord);
|
||||
return $description;
|
||||
}
|
||||
|
||||
//used if handle verification fails, fallsback to displayName or DID depending on context.
|
||||
private function fallbackAuthor($author, $reason)
|
||||
{
|
||||
if ($author['handle'] === 'handle.invalid') {
|
||||
switch ($reason) {
|
||||
case 'url':
|
||||
return $author['did'];
|
||||
case 'display':
|
||||
$displayName = $author['displayName'] ?? '';
|
||||
return e($displayName);
|
||||
}
|
||||
}
|
||||
return $author['handle'];
|
||||
}
|
||||
|
||||
private function generateVerboseTitle($post)
|
||||
{
|
||||
//use "Post by A, replying to B, quoting C" instead of post contents
|
||||
$title = '';
|
||||
if (isset($post['reason']) && str_contains($post['reason']['$type'], 'reasonRepost')) {
|
||||
$title .= 'Repost by ' . $this->fallbackAuthor($post['reason']['by'], 'display') . ', post by ' . $this->fallbackAuthor($post['post']['author'], 'display');
|
||||
} else {
|
||||
$title .= 'Post by ' . $this->fallbackAuthor($post['post']['author'], 'display');
|
||||
}
|
||||
|
||||
if (isset($post['reply'])) {
|
||||
if (isset($post['reply']['parent']['blocked'])) {
|
||||
$replyAuthor = 'blocked user';
|
||||
} elseif (isset($post['reply']['parent']['notFound'])) {
|
||||
$replyAuthor = 'deleted post';
|
||||
} else {
|
||||
$replyAuthor = $this->fallbackAuthor($post['reply']['parent']['author'], 'display');
|
||||
}
|
||||
$title .= ', replying to ' . $replyAuthor;
|
||||
}
|
||||
if (isset($post['post']['embed']) && isset($post['post']['embed']['record'])) {
|
||||
if (isset($post['post']['embed']['record']['blocked'])) {
|
||||
$quotedAuthor = 'blocked user';
|
||||
} elseif (isset($post['post']['embed']['record']['notFound'])) {
|
||||
$quotedAuthor = 'deleted post';
|
||||
} elseif (isset($post['post']['embed']['record']['detached'])) {
|
||||
$quotedAuthor = 'detached post';
|
||||
} else {
|
||||
$quotedAuthor = $this->fallbackAuthor($post['post']['embed']['record']['record']['author'] ?? $post['post']['embed']['record']['author'], 'display');
|
||||
}
|
||||
$title .= ', quoting ' . $quotedAuthor;
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
private function resolveHandle($handle)
|
||||
{
|
||||
$uri = 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle);
|
||||
$response = json_decode(getContents($uri), true);
|
||||
return $response['did'];
|
||||
}
|
||||
|
||||
private function getProfile($did)
|
||||
{
|
||||
$uri = 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=' . urlencode($did);
|
||||
$response = json_decode(getContents($uri), true);
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getAuthorFeed($did, $filter)
|
||||
{
|
||||
$uri = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=' . urlencode($did) . '&filter=' . urlencode($filter) . '&limit=30';
|
||||
if (Debug::isEnabled()) {
|
||||
error_log($uri);
|
||||
}
|
||||
$response = json_decode(getContents($uri), true);
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function getGeneratorViewDescription(array $record): string
|
||||
{
|
||||
$avatar = e($record['avatar']);
|
||||
$displayName = e($record['displayName']);
|
||||
$displayHandle = e($record['creator']['handle']);
|
||||
$likeCount = e($record['likeCount']);
|
||||
preg_match('/\/([^\/]+)$/', $record['uri'], $matches);
|
||||
$uri = e('https://bsky.app/profile/' . $record['creator']['did'] . '/feed/' . $matches[1]);
|
||||
|
||||
return <<<END
|
||||
<a href="{$uri}" style="color: inherit;">
|
||||
<div style="border: 1px solid #333; padding: 10px;">
|
||||
<div style="display: flex; margin-bottom: 10px;">
|
||||
<img src="{$avatar}" height="50" width="50" style="margin-right: 10px;">
|
||||
<div style="display: flex; flex-direction: column; justify-content: center;">
|
||||
<h3>{$displayName}</h3>
|
||||
<span>Feed by @{$displayHandle}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span>Liked by {$likeCount} users</span>
|
||||
</div>
|
||||
</a>
|
||||
END;
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BodaccBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'BODACC';
|
||||
const URI = 'https://bodacc-datadila.opendatasoft.com/';
|
||||
const DESCRIPTION = 'Fetches announces from the French Government "Bulletin Officiel Des Annonces Civiles et Commerciales".';
|
||||
const CACHE_TIMEOUT = 86400;
|
||||
const MAINTAINER = 'quent1';
|
||||
const PARAMETERS = [
|
||||
'Annonces commerciales' => [
|
||||
'departement' => [
|
||||
'name' => 'Département',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tous' => null,
|
||||
'Ain' => '01',
|
||||
'Aisne' => '02',
|
||||
'Allier' => '03',
|
||||
'Alpes-de-Haute-Provence' => '04',
|
||||
'Hautes-Alpes' => '05',
|
||||
'Alpes-Maritimes' => '06',
|
||||
'Ardèche' => '07',
|
||||
'Ardennes' => '08',
|
||||
'Ariège' => '09',
|
||||
'Aube' => '10',
|
||||
'Aude' => '11',
|
||||
'Aveyron' => '12',
|
||||
'Bouches-du-Rhône' => '13',
|
||||
'Calvados' => '14',
|
||||
'Cantal' => '15',
|
||||
'Charente' => '16',
|
||||
'Charente-Maritime' => '17',
|
||||
'Cher' => '18',
|
||||
'Corrèze' => '19',
|
||||
'Corse-du-Sud' => '2A',
|
||||
'Haute-Corse' => '2B',
|
||||
'Côte-d\'Or' => '21',
|
||||
'Côtes-d\'Armor' => '22',
|
||||
'Creuse' => '23',
|
||||
'Dordogne' => '24',
|
||||
'Doubs' => '25',
|
||||
'Drôme' => '26',
|
||||
'Eure' => '27',
|
||||
'Eure-et-Loir' => '28',
|
||||
'Finistère' => '29',
|
||||
'Gard' => '30',
|
||||
'Haute-Garonne' => '31',
|
||||
'Gers' => '32',
|
||||
'Gironde' => '33',
|
||||
'Hérault' => '34',
|
||||
'Ille-et-Vilaine' => '35',
|
||||
'Indre' => '36',
|
||||
'Indre-et-Loire' => '37',
|
||||
'Isère' => '38',
|
||||
'Jura' => '39',
|
||||
'Landes' => '40',
|
||||
'Loir-et-Cher' => '41',
|
||||
'Loire' => '42',
|
||||
'Haute-Loire' => '43',
|
||||
'Loire-Atlantique' => '44',
|
||||
'Loiret' => '45',
|
||||
'Lot' => '46',
|
||||
'Lot-et-Garonne' => '47',
|
||||
'Lozère' => '48',
|
||||
'Maine-et-Loire' => '49',
|
||||
'Manche' => '50',
|
||||
'Marne' => '51',
|
||||
'Haute-Marne' => '52',
|
||||
'Mayenne' => '53',
|
||||
'Meurthe-et-Moselle' => '54',
|
||||
'Meuse' => '55',
|
||||
'Morbihan' => '56',
|
||||
'Moselle' => '57',
|
||||
'Nièvre' => '58',
|
||||
'Nord' => '59',
|
||||
'Oise' => '60',
|
||||
'Orne' => '61',
|
||||
'Pas-de-Calais' => '62',
|
||||
'Puy-de-Dôme' => '63',
|
||||
'Pyrénées-Atlantiques' => '64',
|
||||
'Hautes-Pyrénées' => '65',
|
||||
'Pyrénées-Orientales' => '66',
|
||||
'Bas-Rhin' => '67',
|
||||
'Haut-Rhin' => '68',
|
||||
'Rhône' => '69',
|
||||
'Haute-Saône' => '70',
|
||||
'Saône-et-Loire' => '71',
|
||||
'Sarthe' => '72',
|
||||
'Savoie' => '73',
|
||||
'Haute-Savoie' => '74',
|
||||
'Paris' => '75',
|
||||
'Seine-Maritime' => '76',
|
||||
'Seine-et-Marne' => '77',
|
||||
'Yvelines' => '78',
|
||||
'Deux-Sèvres' => '79',
|
||||
'Somme' => '80',
|
||||
'Tarn' => '81',
|
||||
'Tarn-et-Garonne' => '82',
|
||||
'Var' => '83',
|
||||
'Vaucluse' => '84',
|
||||
'Vendée' => '85',
|
||||
'Vienne' => '86',
|
||||
'Haute-Vienne' => '87',
|
||||
'Vosges' => '88',
|
||||
'Yonne' => '89',
|
||||
'Territoire de Belfort' => '90',
|
||||
'Essonne' => '91',
|
||||
'Hauts-de-Seine' => '92',
|
||||
'Seine-Saint-Denis' => '93',
|
||||
'Val-de-Marne' => '94',
|
||||
'Val-d\'Oise' => '95',
|
||||
'Guadeloupe' => '971',
|
||||
'Martinique' => '972',
|
||||
'Guyane' => '973',
|
||||
'La Réunion' => '974',
|
||||
'Saint-Pierre-et-Miquelon' => '975',
|
||||
'Mayotte' => '976',
|
||||
'Saint-Barthélemy' => '977',
|
||||
'Saint-Martin' => '978',
|
||||
'Terres australes et antarctiques françaises' => '984',
|
||||
'Wallis-et-Futuna' => '986',
|
||||
'Polynésie française' => '987',
|
||||
'Nouvelle-Calédonie' => '988',
|
||||
'Île de Clipperton' => '989'
|
||||
]
|
||||
],
|
||||
'famille' => [
|
||||
'name' => 'Famille',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Toutes' => null,
|
||||
'Annonces diverses' => 'divers',
|
||||
'Créations' => 'creation',
|
||||
'Dépôts des comptes' => 'dpc',
|
||||
'Immatriculations' => 'immatriculation',
|
||||
'Modifications diverses' => 'modification',
|
||||
'Procédures collectives' => 'collective',
|
||||
'Procédures de conciliation' => 'conciliation',
|
||||
'Procédures de rétablissement professionnel' => 'retablissement_professionnel',
|
||||
'Radiations' => 'radiation',
|
||||
'Ventes et cessions' => 'vente'
|
||||
]
|
||||
],
|
||||
'type' => [
|
||||
'name' => 'Type',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Tous' => null,
|
||||
'Avis initial' => 'annonce',
|
||||
'Avis d\'annulation' => 'annulation',
|
||||
'Avis rectificatif' => 'rectificatif'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$parameters = [
|
||||
'select' => 'id,dateparution,typeavis_lib,familleavis_lib,commercant,ville,cp',
|
||||
'order_by' => 'id desc',
|
||||
'limit' => 50,
|
||||
];
|
||||
|
||||
$where = [];
|
||||
if (!empty($this->getInput('departement'))) {
|
||||
$where[] = 'numerodepartement="' . $this->getInput('departement') . '"';
|
||||
}
|
||||
|
||||
if (!empty($this->getInput('famille'))) {
|
||||
$where[] = 'familleavis="' . $this->getInput('famille') . '"';
|
||||
}
|
||||
|
||||
if (!empty($this->getInput('type'))) {
|
||||
$where[] = 'typeavis="' . $this->getInput('type') . '"';
|
||||
}
|
||||
|
||||
if ($where !== []) {
|
||||
$parameters['where'] = implode(' and ', $where);
|
||||
}
|
||||
|
||||
$url = urljoin(self::URI, '/api/explore/v2.1/catalog/datasets/annonces-commerciales/records?' . http_build_query($parameters));
|
||||
|
||||
$data = Json::decode(getContents($url), false);
|
||||
|
||||
foreach ($data->results as $result) {
|
||||
if (
|
||||
!isset(
|
||||
$result->id,
|
||||
$result->dateparution,
|
||||
$result->typeavis_lib,
|
||||
$result->familleavis_lib,
|
||||
$result->commercant,
|
||||
$result->ville,
|
||||
$result->cp
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = sprintf(
|
||||
'[%s] %s - %s à %s (%s)',
|
||||
$result->typeavis_lib,
|
||||
$result->familleavis_lib,
|
||||
$result->commercant,
|
||||
$result->ville,
|
||||
$result->cp
|
||||
);
|
||||
|
||||
$this->items[] = [
|
||||
'uid' => $result->id,
|
||||
'timestamp' => strtotime($result->dateparution),
|
||||
'title' => $title,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,77 +1,45 @@
|
||||
<?php
|
||||
require_once('GelbooruBridge.php');
|
||||
|
||||
class BooruprojectBridge extends DanbooruBridge
|
||||
{
|
||||
const MAINTAINER = 'mitsukarenai';
|
||||
const NAME = 'Booruproject';
|
||||
const URI = 'https://booru.org/';
|
||||
const DESCRIPTION = 'Returns images from given page of booruproject';
|
||||
const PARAMETERS = [
|
||||
'global' => [
|
||||
'p' => [
|
||||
'name' => 'page',
|
||||
'defaultValue' => 0,
|
||||
'type' => 'number'
|
||||
],
|
||||
't' => [
|
||||
'name' => 'tags',
|
||||
'required' => true,
|
||||
'exampleValue' => 'tagme',
|
||||
'title' => 'Use "all" to get all posts'
|
||||
]
|
||||
],
|
||||
'Booru subdomain (subdomain.booru.org)' => [
|
||||
'i' => [
|
||||
'name' => 'Subdomain',
|
||||
'required' => true,
|
||||
'exampleValue' => 'rm'
|
||||
]
|
||||
]
|
||||
];
|
||||
class BooruprojectBridge extends GelbooruBridge {
|
||||
|
||||
const PATHTODATA = '.thumb';
|
||||
const IDATTRIBUTE = 'id';
|
||||
const TAGATTRIBUTE = 'title';
|
||||
const PIDBYPAGE = 20;
|
||||
const MAINTAINER = 'mitsukarenai';
|
||||
const NAME = 'Booruproject';
|
||||
const URI = 'http://booru.org/';
|
||||
const DESCRIPTION = 'Returns images from given page of booruproject';
|
||||
const PARAMETERS = array(
|
||||
'global' => array(
|
||||
'p' => array(
|
||||
'name' => 'page',
|
||||
'type' => 'number'
|
||||
),
|
||||
't' => array(
|
||||
'name' => 'tags'
|
||||
)
|
||||
),
|
||||
'Booru subdomain (subdomain.booru.org)' => array(
|
||||
'i' => array(
|
||||
'name' => 'Subdomain',
|
||||
'required' => true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
protected function getFullURI()
|
||||
{
|
||||
return $this->getURI()
|
||||
. 'index.php?page=post&s=list&pid='
|
||||
. ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '')
|
||||
. '&tags=' . urlencode($this->getInput('t'));
|
||||
}
|
||||
const PIDBYPAGE = 20;
|
||||
|
||||
protected function getTags($element)
|
||||
{
|
||||
$tags = parent::getTags($element);
|
||||
$tags = explode(' ', $tags);
|
||||
public function getURI(){
|
||||
if(!is_null($this->getInput('i'))) {
|
||||
return 'http://' . $this->getInput('i') . '.booru.org/';
|
||||
}
|
||||
|
||||
// Remove statistics from the tags list (identified by colon)
|
||||
foreach ($tags as $key => $tag) {
|
||||
if (strpos($tag, ':') !== false) {
|
||||
unset($tags[$key]);
|
||||
}
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
return implode(' ', $tags);
|
||||
}
|
||||
public function getName(){
|
||||
if(!is_null($this->getInput('i'))) {
|
||||
return static::NAME . ' ' . $this->getInput('i');
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if (!is_null($this->getInput('i'))) {
|
||||
return 'https://' . $this->getInput('i') . '.booru.org/';
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('i'))) {
|
||||
return static::NAME . ' ' . $this->getInput('i');
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BrutBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Brut Bridge';
|
||||
const URI = 'https://www.brut.media';
|
||||
const DESCRIPTION = 'Returns 10 newest videos by category and edition';
|
||||
const MAINTAINER = 'VerifiedJoseph';
|
||||
const PARAMETERS = [[
|
||||
'category' => [
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'News' => 'news',
|
||||
'International' => 'international',
|
||||
'Economy' => 'economy',
|
||||
'Science and Technology' => 'science-and-technology',
|
||||
'Entertainment' => 'entertainment',
|
||||
'Sports' => 'sport',
|
||||
'Nature' => 'nature',
|
||||
'Health' => 'health',
|
||||
],
|
||||
'defaultValue' => 'news',
|
||||
],
|
||||
'edition' => [
|
||||
'name' => ' Edition',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'United States' => 'us',
|
||||
'United Kingdom' => 'uk',
|
||||
'France' => 'fr',
|
||||
'Spain' => 'es',
|
||||
'India' => 'in',
|
||||
'Mexico' => 'mx',
|
||||
],
|
||||
'defaultValue' => 'us',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getURI();
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
$regex = '/window.__PRELOADED_STATE__ = (.*);/';
|
||||
preg_match($regex, $html, $parts);
|
||||
$data = Json::decode($parts[1], false);
|
||||
foreach ($data->medias->index as $uid => $media) {
|
||||
$this->items[] = [
|
||||
'uid' => $uid,
|
||||
'title' => $media->metadata->slug,
|
||||
'uri' => $media->share_url,
|
||||
'timestamp' => $media->published_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
|
||||
return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
|
||||
return $this->getKey('category') . ' - ' . $this->getKey('edition') . ' - Brut.';
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BugzillaBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bugzilla Bridge';
|
||||
const URI = 'https://www.bugzilla.org/';
|
||||
const DESCRIPTION = 'Bridge for any Bugzilla instance';
|
||||
const MAINTAINER = 'Yaman Qalieh';
|
||||
const PARAMETERS = [
|
||||
'global' => [
|
||||
'instance' => [
|
||||
'name' => 'Instance URL',
|
||||
'required' => true,
|
||||
'exampleValue' => 'https://bugzilla.mozilla.org'
|
||||
]
|
||||
],
|
||||
'Bug comments' => [
|
||||
'id' => [
|
||||
'name' => 'Bug tracking ID',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'Insert bug tracking ID',
|
||||
'exampleValue' => 121241
|
||||
],
|
||||
'limit' => [
|
||||
'name' => 'Number of comments to return',
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'title' => 'Specify number of comments to return',
|
||||
'defaultValue' => -1
|
||||
],
|
||||
'skiptags' => [
|
||||
'name' => 'Skip offtopic comments',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Excludes comments tagged as advocacy, metoo, or offtopic from the feed'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
const SKIPPED_ACTIVITY = [
|
||||
'cc' => true,
|
||||
'comment_tag' => true
|
||||
];
|
||||
|
||||
const SKIPPED_TAGS = ['advocacy', 'metoo', 'offtopic'];
|
||||
|
||||
private $instance;
|
||||
private $bugid;
|
||||
private $buguri;
|
||||
private $title;
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->title)) {
|
||||
return $this->title;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
return $this->buguri ?? parent::getURI();
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$this->instance = rtrim($this->getInput('instance'), '/');
|
||||
$this->bugid = $this->getInput('id');
|
||||
$this->buguri = $this->instance . '/show_bug.cgi?id=' . $this->bugid;
|
||||
|
||||
$url = $this->instance . '/rest/bug/' . $this->bugid;
|
||||
$this->getTitle($url);
|
||||
$this->collectComments($url . '/comment');
|
||||
$this->collectUpdates($url . '/history');
|
||||
|
||||
usort($this->items, function ($a, $b) {
|
||||
return $b['timestamp'] <=> $a['timestamp'];
|
||||
});
|
||||
|
||||
if ($this->getInput('limit') > 0) {
|
||||
$this->items = array_slice($this->items, 0, $this->getInput('limit'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTitle($url)
|
||||
{
|
||||
// Only request the summary for a faster request
|
||||
$json = self::getJSON($url . '?include_fields=summary');
|
||||
$this->title = 'Bug ' . $this->bugid . ' - ' .
|
||||
$json['bugs'][0]['summary'] . ' - ' .
|
||||
// Remove https://
|
||||
substr($this->instance, 8);
|
||||
}
|
||||
|
||||
protected function collectComments($url)
|
||||
{
|
||||
$json = self::getJSON($url);
|
||||
|
||||
// Array of comments is here
|
||||
if (!isset($json['bugs'][$this->bugid]['comments'])) {
|
||||
returnClientError('Cannot find REST endpoint');
|
||||
}
|
||||
|
||||
foreach ($json['bugs'][$this->bugid]['comments'] as $comment) {
|
||||
$item = [];
|
||||
if (
|
||||
$this->getInput('skiptags') and
|
||||
array_intersect(self::SKIPPED_TAGS, $comment['tags'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$item['categories'] = $comment['tags'];
|
||||
$item['uri'] = $this->buguri . '#c' . $comment['count'];
|
||||
$item['title'] = 'Comment ' . $comment['count'];
|
||||
$item['timestamp'] = $comment['creation_time'];
|
||||
$item['author'] = $this->getUser($comment['creator']);
|
||||
$item['content'] = $comment['text'];
|
||||
if (isset($comment['is_markdown']) and $comment['is_markdown']) {
|
||||
$item['content'] = markdownToHtml($item['content']);
|
||||
}
|
||||
if (!is_null($comment['attachment_id'])) {
|
||||
$item['enclosures'] = [$this->instance . '/attachment.cgi?id=' . $comment['attachment_id']];
|
||||
}
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
protected function collectUpdates($url)
|
||||
{
|
||||
$json = self::getJSON($url);
|
||||
|
||||
// Array of changesets which contain an array of changes
|
||||
if (!isset($json['bugs']['0']['history'])) {
|
||||
returnClientError('Cannot find REST endpoint');
|
||||
}
|
||||
|
||||
foreach ($json['bugs']['0']['history'] as $changeset) {
|
||||
$author = $this->getUser($changeset['who']);
|
||||
$timestamp = $changeset['when'];
|
||||
foreach ($changeset['changes'] as $change) {
|
||||
// Skip updates to the cc list and comment tagging
|
||||
if (isset(self::SKIPPED_ACTIVITY[$change['field_name']])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = [];
|
||||
$item['uri'] = $this->buguri;
|
||||
$item['title'] = 'Updated';
|
||||
$item['timestamp'] = $timestamp;
|
||||
$item['author'] = $author;
|
||||
$item['content'] = ucfirst($change['field_name']) . ': ' .
|
||||
($change['removed'] === '' ? '[nothing]' : $change['removed']) . ' -> ' .
|
||||
($change['added'] === '' ? '[nothing]' : $change['added']);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUser($user)
|
||||
{
|
||||
// Check if the user endpoint is available
|
||||
if ($this->loadCacheValue($this->instance . 'userEndpointClosed')) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$cache = $this->loadCacheValue($this->instance . $user);
|
||||
if ($cache) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$url = $this->instance . '/rest/user/' . $user . '?include_fields=real_name';
|
||||
try {
|
||||
$json = self::getJSON($url);
|
||||
if (isset($json['error']) and $json['error']) {
|
||||
throw new Exception();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->saveCacheValue($this->instance . 'userEndpointClosed', true);
|
||||
return $user;
|
||||
}
|
||||
|
||||
$username = $json['users']['0']['real_name'];
|
||||
|
||||
if (empty($username)) {
|
||||
$username = $user;
|
||||
}
|
||||
$this->saveCacheValue($this->instance . $user, $username);
|
||||
return $username;
|
||||
}
|
||||
|
||||
protected static function getJSON($url)
|
||||
{
|
||||
$headers = [
|
||||
'Accept: application/json',
|
||||
];
|
||||
return json_decode(getContents($url, $headers), true);
|
||||
}
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BukowskisBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bukowskis';
|
||||
const URI = 'https://www.bukowskis.com';
|
||||
const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house';
|
||||
const MAINTAINER = 'Qluxzz';
|
||||
const PARAMETERS = [[
|
||||
'category' => [
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All categories' => '',
|
||||
'Art' => [
|
||||
'All' => 'art',
|
||||
'Classic Art' => 'art.classic-art',
|
||||
'Classic Finnish Art' => 'art.classic-finnish-art',
|
||||
'Classic Swedish Art' => 'art.classic-swedish-art',
|
||||
'Contemporary' => 'art.contemporary',
|
||||
'Modern Finnish Art' => 'art.modern-finnish-art',
|
||||
'Modern International Art' => 'art.modern-international-art',
|
||||
'Modern Swedish Art' => 'art.modern-swedish-art',
|
||||
'Old Masters' => 'art.old-masters',
|
||||
'Other' => 'art.other',
|
||||
'Photographs' => 'art.photographs',
|
||||
'Prints' => 'art.prints',
|
||||
'Sculpture' => 'art.sculpture',
|
||||
'Swedish Old Masters' => 'art.swedish-old-masters',
|
||||
],
|
||||
'Asian Ceramics & Works of Art' => [
|
||||
'All' => 'asian-ceramics-works-of-art',
|
||||
'Other' => 'asian-ceramics-works-of-art.other',
|
||||
'Porcelain' => 'asian-ceramics-works-of-art.porcelain',
|
||||
],
|
||||
'Books & Manuscripts' => [
|
||||
'All' => 'books-manuscripts',
|
||||
'Books' => 'books-manuscripts.books',
|
||||
],
|
||||
'Carpets, rugs & textiles' => [
|
||||
'All' => 'carpets-rugs-textiles',
|
||||
'European' => 'carpets-rugs-textiles.european',
|
||||
'Oriental' => 'carpets-rugs-textiles.oriental',
|
||||
'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world',
|
||||
'Scandinavian' => 'carpets-rugs-textiles.scandinavian',
|
||||
],
|
||||
'Ceramics & porcelain' => [
|
||||
'All' => 'ceramics-porcelain',
|
||||
'Ceramic ware' => 'ceramics-porcelain.ceramic-ware',
|
||||
'European' => 'ceramics-porcelain.european',
|
||||
'Rest of the world' => 'ceramics-porcelain.rest-of-the-world',
|
||||
'Scandinavian' => 'ceramics-porcelain.scandinavian',
|
||||
],
|
||||
'Collectibles' => [
|
||||
'All' => 'collectibles',
|
||||
'Advertising & Retail' => 'collectibles.advertising-retail',
|
||||
'Memorabilia' => 'collectibles.memorabilia',
|
||||
'Movies & music' => 'collectibles.movies-music',
|
||||
'Other' => 'collectibles.other',
|
||||
'Retro & Popular Culture' => 'collectibles.retro-popular-culture',
|
||||
'Technica & Nautica' => 'collectibles.technica-nautica',
|
||||
'Toys' => 'collectibles.toys',
|
||||
],
|
||||
'Design' => [
|
||||
'All' => 'design',
|
||||
'Art glass' => 'design.art-glass',
|
||||
'Furniture' => 'design.furniture',
|
||||
'Other' => 'design.other',
|
||||
],
|
||||
'Folk art' => [
|
||||
'All' => 'folk-art',
|
||||
'All categories' => 'lots',
|
||||
],
|
||||
'Furniture' => [
|
||||
'All' => 'furniture',
|
||||
'Armchairs & Sofas' => 'furniture.armchairs-sofas',
|
||||
'Cabinets & Bureaus' => 'furniture.cabinets-bureaus',
|
||||
'Chairs' => 'furniture.chairs',
|
||||
'Garden furniture' => 'furniture.garden-furniture',
|
||||
'Mirrors' => 'furniture.mirrors',
|
||||
'Other' => 'furniture.other',
|
||||
'Shelves & Book cases' => 'furniture.shelves-book-cases',
|
||||
'Tables' => 'furniture.tables',
|
||||
],
|
||||
'Glassware' => [
|
||||
'All' => 'glassware',
|
||||
'Glassware' => 'glassware.glassware',
|
||||
'Other' => 'glassware.other',
|
||||
],
|
||||
'Jewellery' => [
|
||||
'All' => 'jewellery',
|
||||
'Bracelets' => 'jewellery.bracelets',
|
||||
'Brooches' => 'jewellery.brooches',
|
||||
'Earrings' => 'jewellery.earrings',
|
||||
'Necklaces & Pendants' => 'jewellery.necklaces-pendants',
|
||||
'Other' => 'jewellery.other',
|
||||
'Rings' => 'jewellery.rings',
|
||||
],
|
||||
'Lighting' => [
|
||||
'All' => 'lighting',
|
||||
'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras',
|
||||
'Ceiling lights' => 'lighting.ceiling-lights',
|
||||
'Chandeliers' => 'lighting.chandeliers',
|
||||
'Floor lights' => 'lighting.floor-lights',
|
||||
'Other' => 'lighting.other',
|
||||
'Table lights' => 'lighting.table-lights',
|
||||
'Wall lights' => 'lighting.wall-lights',
|
||||
],
|
||||
'Militaria' => [
|
||||
'All' => 'militaria',
|
||||
'Honors & Medals' => 'militaria.honors-medals',
|
||||
'Other militaria' => 'militaria.other-militaria',
|
||||
'Weaponry' => 'militaria.weaponry',
|
||||
],
|
||||
'Miscellaneous' => [
|
||||
'All' => 'miscellaneous',
|
||||
'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter',
|
||||
'Nickel silver' => 'miscellaneous.nickel-silver',
|
||||
'Oriental' => 'miscellaneous.oriental',
|
||||
'Other' => 'miscellaneous.other',
|
||||
],
|
||||
'Silver' => [
|
||||
'All' => 'silver',
|
||||
'Candle sticks' => 'silver.candle-sticks',
|
||||
'Cups & Bowls' => 'silver.cups-bowls',
|
||||
'Cutlery' => 'silver.cutlery',
|
||||
'Other' => 'silver.other',
|
||||
],
|
||||
'Timepieces' => [
|
||||
'All' => 'timepieces',
|
||||
'Other' => 'timepieces.other',
|
||||
'Pocket watches' => 'timepieces.pocket-watches',
|
||||
'Table clocks' => 'timepieces.table-clocks',
|
||||
'Wrist watches' => 'timepieces.wrist-watches',
|
||||
],
|
||||
'Vintage & Fashion' => [
|
||||
'All' => 'vintage-fashion',
|
||||
'Accessories' => 'vintage-fashion.accessories',
|
||||
'Bags & Trunks' => 'vintage-fashion.bags-trunks',
|
||||
'Clothes' => 'vintage-fashion.clothes',
|
||||
],
|
||||
]
|
||||
],
|
||||
'sort_order' => [
|
||||
'name' => 'Sort order',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'Ending soon' => 'ending',
|
||||
'Most recent' => 'recent',
|
||||
'Most bids' => 'most',
|
||||
'Fewest bids' => 'fewest',
|
||||
'Lowest price' => 'lowest',
|
||||
'Highest price' => 'highest',
|
||||
'Lowest estimate' => 'low',
|
||||
'Highest estimate' => 'high',
|
||||
'Alphabetical' => 'alphabetical',
|
||||
],
|
||||
],
|
||||
'language' => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'English' => 'en',
|
||||
'Swedish' => 'sv',
|
||||
'Finnish' => 'fi'
|
||||
],
|
||||
],
|
||||
]];
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
private $title;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$baseUrl = 'https://www.bukowskis.com';
|
||||
$category = $this->getInput('category');
|
||||
$language = $this->getInput('language');
|
||||
$sort_order = $this->getInput('sort_order');
|
||||
|
||||
$url = $baseUrl . '/' . $language . '/lots';
|
||||
|
||||
if ($category) {
|
||||
$url = $url . '/category/' . $category;
|
||||
}
|
||||
|
||||
if ($sort_order) {
|
||||
$url = $url . '/sort/' . $sort_order;
|
||||
}
|
||||
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$this->title = htmlspecialchars_decode($html->find('title', 0)->innertext);
|
||||
|
||||
foreach ($html->find('div.c-lot-index-lot') as $lot) {
|
||||
$title = $lot->find('a.c-lot-index-lot__title', 0)->plaintext;
|
||||
$relative_url = $lot->find('a.c-lot-index-lot__link', 0)->href;
|
||||
$images = json_decode(
|
||||
htmlspecialchars_decode(
|
||||
$lot
|
||||
->find('img.o-aspect-ratio__image', 0)
|
||||
->getAttribute('data-thumbnails')
|
||||
)
|
||||
);
|
||||
|
||||
$this->items[] = [
|
||||
'title' => $title,
|
||||
'uri' => $baseUrl . $relative_url,
|
||||
'uid' => $relative_url,
|
||||
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
|
||||
'enclosures' => array_slice($images, 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->title ?: parent::getName();
|
||||
}
|
||||
}
|
@ -1,90 +1,87 @@
|
||||
<?php
|
||||
class BundesbankBridge extends BridgeAbstract {
|
||||
|
||||
class BundesbankBridge extends BridgeAbstract
|
||||
{
|
||||
const PARAM_LANG = 'lang';
|
||||
const PARAM_LANG = 'lang';
|
||||
|
||||
const LANG_EN = 'en';
|
||||
const LANG_DE = 'de';
|
||||
const LANG_EN = 'en';
|
||||
const LANG_DE = 'de';
|
||||
|
||||
const NAME = 'Bundesbank Bridge';
|
||||
const URI = 'https://www.bundesbank.de/';
|
||||
const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)';
|
||||
const MAINTAINER = 'logmanoriginal';
|
||||
const CACHE_TIMEOUT = 86400; // 24 hours
|
||||
const NAME = 'Bundesbank Bridge';
|
||||
const URI = 'https://www.bundesbank.de/';
|
||||
const DESCRIPTION = 'Returns the latest studies of the Bundesbank (Germany)';
|
||||
const MAINTAINER = 'logmanoriginal';
|
||||
const CACHE_TIMEOUT = 86400; // 24 hours
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
self::PARAM_LANG => [
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'defaultValue' => self::LANG_DE,
|
||||
'values' => [
|
||||
'English' => self::LANG_EN,
|
||||
'Deutsch' => self::LANG_DE
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
self::PARAM_LANG => array(
|
||||
'name' => 'Language',
|
||||
'type' => 'list',
|
||||
'required' => true,
|
||||
'defaultValue' => self::LANG_DE,
|
||||
'values' => array(
|
||||
'English' => self::LANG_EN,
|
||||
'Deutsch' => self::LANG_DE
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico';
|
||||
}
|
||||
public function getIcon() {
|
||||
return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico';
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
switch ($this->getInput(self::PARAM_LANG)) {
|
||||
case self::LANG_EN:
|
||||
return self::URI . 'en/publications/reports/studies';
|
||||
case self::LANG_DE:
|
||||
return self::URI . 'de/publikationen/berichte/studien';
|
||||
}
|
||||
public function getURI() {
|
||||
switch($this->getInput(self::PARAM_LANG)) {
|
||||
case self::LANG_EN: return self::URI . 'en/publications/reports/studies';
|
||||
case self::LANG_DE: return self::URI . 'de/publikationen/berichte/studien';
|
||||
}
|
||||
|
||||
return parent::getURI();
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
public function collectData() {
|
||||
|
||||
$html = defaultLinkTo($html, $this->getURI());
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('No response for ' . $this->getURI());
|
||||
|
||||
foreach ($html->find('ul.resultlist li') as $study) {
|
||||
$item = [];
|
||||
$html = defaultLinkTo($html, $this->getURI());
|
||||
|
||||
$item['uri'] = $study->find('.teasable__link', 0)->href;
|
||||
foreach($html->find('ul.resultlist li') as $study) {
|
||||
$item = array();
|
||||
|
||||
// Get title without child elements (i.e. subtitle)
|
||||
$title = $study->find('.teasable__title div.h2', 0);
|
||||
$item['uri'] = $study->find('.teasable__link', 0)->href;
|
||||
|
||||
foreach ($title->children as &$child) {
|
||||
$child->outertext = '';
|
||||
}
|
||||
// Get title without child elements (i.e. subtitle)
|
||||
$title = $study->find('.teasable__title div.h2', 0);
|
||||
|
||||
$item['title'] = $title->innertext;
|
||||
foreach($title->children as &$child) {
|
||||
$child->outertext = '';
|
||||
}
|
||||
|
||||
// Add subtitle to the content if it exists
|
||||
$item['content'] = '';
|
||||
$item['title'] = $title->innertext;
|
||||
|
||||
if ($subtitle = $study->find('.teasable__subtitle', 0)) {
|
||||
$item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>';
|
||||
}
|
||||
// Add subtitle to the content if it exists
|
||||
$item['content'] = '';
|
||||
|
||||
$teasable = $study->find('.teasable__text', 0);
|
||||
$teasableText = $teasable->plaintext ?? '';
|
||||
$item['content'] .= '<p>' . $teasableText . '</p>';
|
||||
if($subtitle = $study->find('.teasable__subtitle', 0)) {
|
||||
$item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>';
|
||||
}
|
||||
|
||||
$item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext);
|
||||
$item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
|
||||
|
||||
// Downloads and older studies don't have images
|
||||
if ($study->find('.teasable__image', 0)) {
|
||||
$item['enclosures'] = [
|
||||
$study->find('.teasable__image img', 0)->src
|
||||
];
|
||||
}
|
||||
$item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext);
|
||||
|
||||
// Downloads and older studies don't have images
|
||||
if($study->find('.teasable__image', 0)) {
|
||||
$item['enclosures'] = array(
|
||||
$study->find('.teasable__image img', 0)->src
|
||||
);
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BundestagParteispendenBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'mibe';
|
||||
const NAME = 'Deutscher Bundestag - Parteispenden';
|
||||
const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000';
|
||||
|
||||
const CACHE_TIMEOUT = 86400; // 24h
|
||||
const DESCRIPTION = 'Returns the latest "soft money" donations to parties represented in the German Bundestag.';
|
||||
const CONTENT_TEMPLATE = <<<TMPL
|
||||
<p><b>Partei:</b><br>%s</p>
|
||||
<p><b>Spendenbetrag:</b><br>%s</p>
|
||||
<p><b>Spender:</b><br>%s</p>
|
||||
<p><b>Eingang der Spende:</b><br>%s</p>
|
||||
TMPL;
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://www.bundestag.de/static/appdata/includes/images/layout/favicon.ico';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$ajaxUri = <<<URI
|
||||
https://www.bundestag.de/ajax/filterlist/de/parlament/praesidium/parteienfinanzierung/fundstellen50000/462002-462002
|
||||
URI;
|
||||
// Get the main page
|
||||
$html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT);
|
||||
|
||||
// Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year.
|
||||
$firstAnchor = $html->find('a', 0)
|
||||
or returnServerError('Could not find the proper HTML element.');
|
||||
|
||||
$url = $firstAnchor->href;
|
||||
|
||||
// Get the actual page with the soft money donations
|
||||
$html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT);
|
||||
|
||||
$rows = $html->find('table.table > tbody > tr')
|
||||
or returnServerError('Could not find the proper HTML elements.');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$item = $this->generateItemFromRow($row);
|
||||
if (is_array($item)) {
|
||||
$item['uri'] = $url;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function generateItemFromRow(simple_html_dom_node $row)
|
||||
{
|
||||
// The row must have 5 columns. There are monthly header rows, which are ignored here.
|
||||
if (count($row->children) != 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = [];
|
||||
|
||||
// | column | paragraph inside column
|
||||
$party = $row->children[0]->children[0]->innertext;
|
||||
$amount = $row->children[1]->children[0]->innertext . ' €';
|
||||
$donor = $row->children[2]->children[0]->innertext;
|
||||
$date = $row->children[3]->children[0]->innertext;
|
||||
$dip = $row->children[4]->children[0]->find('a.dipLink', 0);
|
||||
|
||||
// Strip whitespace from date string.
|
||||
$date = str_replace(' ', '', $date);
|
||||
|
||||
$content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date);
|
||||
|
||||
$item = [
|
||||
'title' => $party . ': ' . $amount,
|
||||
'content' => $content,
|
||||
'uid' => sha1($content),
|
||||
];
|
||||
|
||||
// Try to get the link to the official document
|
||||
if ($dip != null) {
|
||||
$item['enclosures'] = [$dip->href];
|
||||
}
|
||||
|
||||
// Try to parse the date
|
||||
$dateTime = DateTime::createFromFormat('d.m.Y', $date);
|
||||
if ($dateTime !== false) {
|
||||
$item['timestamp'] = $dateTime->getTimestamp();
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
class BundesverbandFuerFreieKammernBridge extends XPathAbstract
|
||||
{
|
||||
const NAME = 'Bundesverband für freie Kammern e.V.';
|
||||
const URI = 'https://www.bffk.de/aktuelles/aktuelle-nachrichten.html';
|
||||
const DESCRIPTION = 'Aktuelle Nachrichten';
|
||||
const MAINTAINER = 'hleskien';
|
||||
|
||||
const FEED_SOURCE_URL = 'https://www.bffk.de/aktuelles/aktuelle-nachrichten.html';
|
||||
//const XPATH_EXPRESSION_FEED_ICON = './/link[@rel="icon"]/@href';
|
||||
const XPATH_EXPRESSION_ITEM = '//ul[@class="article-list"]/li';
|
||||
const XPATH_EXPRESSION_ITEM_TITLE = './/a/text()';
|
||||
const XPATH_EXPRESSION_ITEM_CONTENT = './/a/text()';
|
||||
const XPATH_EXPRESSION_ITEM_URI = './/a/@href';
|
||||
//const XPATH_EXPRESSION_ITEM_AUTHOR = './/';
|
||||
const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/span/i';
|
||||
//const XPATH_EXPRESSION_ITEM_ENCLOSURES = './';
|
||||
//const XPATH_EXPRESSION_ITEM_CATEGORIES = './/';
|
||||
|
||||
protected function formatItemTimestamp($value)
|
||||
{
|
||||
$value = trim($value, '()');
|
||||
$dti = DateTimeImmutable::createFromFormat('d.m.Y', $value);
|
||||
$dti = $dti->setTime(0, 0, 0);
|
||||
return $dti->getTimestamp();
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
class CBCEditorsBlogBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'quickwick';
|
||||
const NAME = 'CBC Editors Blog';
|
||||
const URI = 'https://www.cbc.ca/news/editorsblog';
|
||||
const DESCRIPTION = 'Recent CBC Editor\'s Blog posts';
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
// Loop on each blog post entry
|
||||
foreach ($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) {
|
||||
$headline = ($element->find('.headline', 0))->innertext;
|
||||
$timestamp = ($element->find('time', 0))->datetime;
|
||||
$articleUri = 'https://www.cbc.ca' . $element->href;
|
||||
$summary = ($element->find('div.description', 0))->innertext;
|
||||
$thumbnailUris = ($element->find('img[loading=lazy]', 0))->srcset;
|
||||
$thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w');
|
||||
|
||||
// Fill item
|
||||
$item = [];
|
||||
$item['uri'] = $articleUri;
|
||||
$item['id'] = $item['uri'];
|
||||
$item['timestamp'] = $timestamp;
|
||||
$item['title'] = $headline;
|
||||
$item['content'] = '<img src="'
|
||||
. $thumbnailUri . '" /><br>' . $summary;
|
||||
$item['author'] = 'Editor\'s Blog';
|
||||
|
||||
if (isset($item['title'])) {
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,118 +1,109 @@
|
||||
<?php
|
||||
class CNETBridge extends BridgeAbstract {
|
||||
|
||||
class CNETBridge extends SitemapBridge
|
||||
{
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = 'CNET News';
|
||||
const URI = 'https://www.cnet.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'topic' => [
|
||||
'name' => 'Topic',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'All articles' => '',
|
||||
'Tech' => 'tech',
|
||||
'Money' => 'personal-finance',
|
||||
'Home' => 'home',
|
||||
'Wellness' => 'health',
|
||||
'Energy' => 'home/energy-and-utilities',
|
||||
'Deals' => 'deals',
|
||||
'Computing' => 'tech/computing',
|
||||
'Mobile' => 'tech/mobile',
|
||||
'Science' => 'science',
|
||||
'Services' => 'tech/services-and-software'
|
||||
]
|
||||
],
|
||||
'limit' => self::LIMIT
|
||||
]
|
||||
];
|
||||
const MAINTAINER = 'ORelio';
|
||||
const NAME = 'CNET News';
|
||||
const URI = 'https://www.cnet.com/';
|
||||
const CACHE_TIMEOUT = 3600; // 1h
|
||||
const DESCRIPTION = 'Returns the newest articles.';
|
||||
const PARAMETERS = array(
|
||||
array(
|
||||
'topic' => array(
|
||||
'name' => 'Topic',
|
||||
'type' => 'list',
|
||||
'values' => array(
|
||||
'All articles' => '',
|
||||
'Apple' => 'apple',
|
||||
'Google' => 'google',
|
||||
'Microsoft' => 'tags-microsoft',
|
||||
'Computers' => 'topics-computers',
|
||||
'Mobile' => 'topics-mobile',
|
||||
'Sci-Tech' => 'topics-sci-tech',
|
||||
'Security' => 'topics-security',
|
||||
'Internet' => 'topics-internet',
|
||||
'Tech Industry' => 'topics-tech-industry'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$topic = $this->getInput('topic');
|
||||
$limit = $this->getInput('limit');
|
||||
$limit = empty($limit) ? 10 : $limit;
|
||||
private function cleanArticle($article_html) {
|
||||
$offset_p = strpos($article_html, '<p>');
|
||||
$offset_figure = strpos($article_html, '<figure');
|
||||
$offset = ($offset_figure < $offset_p ? $offset_figure : $offset_p);
|
||||
$article_html = substr($article_html, $offset);
|
||||
$article_html = str_replace('href="/', 'href="' . self::URI, $article_html);
|
||||
$article_html = str_replace(' height="0"', '', $article_html);
|
||||
$article_html = str_replace('<noscript>', '', $article_html);
|
||||
$article_html = str_replace('</noscript>', '', $article_html);
|
||||
$article_html = StripWithDelimiters($article_html, '<a class="clickToEnlarge', '</a>');
|
||||
$article_html = stripWithDelimiters($article_html, '<span class="nowPlaying', '</span>');
|
||||
$article_html = stripWithDelimiters($article_html, '<span class="duration', '</span>');
|
||||
$article_html = stripWithDelimiters($article_html, '<script', '</script>');
|
||||
$article_html = stripWithDelimiters($article_html, '<svg', '</svg>');
|
||||
return $article_html;
|
||||
}
|
||||
|
||||
$url_pattern = empty($topic) ? '' : self::URI . $topic;
|
||||
$sitemap_latest = self::URI . 'sitemaps/article/' . date('Y/m') . '.xml';
|
||||
$sitemap_previous = self::URI . 'sitemaps/article/' . date('Y/m', strtotime('last day of previous month')) . '.xml';
|
||||
public function collectData() {
|
||||
|
||||
$links = array_merge(
|
||||
$this->sitemapXmlToList($this->getSitemapXml($sitemap_latest, true), $url_pattern, $limit),
|
||||
$this->sitemapXmlToList($this->getSitemapXml($sitemap_previous, true), $url_pattern, $limit)
|
||||
);
|
||||
// Retrieve and check user input
|
||||
$topic = str_replace('-', '/', $this->getInput('topic'));
|
||||
if (!empty($topic) && (substr_count($topic, '/') > 1 || !ctype_alpha(str_replace('/', '', $topic))))
|
||||
returnClientError('Invalid topic: ' . $topic);
|
||||
|
||||
if ($limit > 0 && count($links) > $limit) {
|
||||
$links = array_slice($links, 0, $limit);
|
||||
}
|
||||
// Retrieve webpage
|
||||
$pageUrl = self::URI . (empty($topic) ? 'news/' : $topic . '/');
|
||||
$html = getSimpleHTMLDOM($pageUrl)
|
||||
or returnServerError('Could not request CNET: ' . $pageUrl);
|
||||
|
||||
if (empty($links)) {
|
||||
returnClientError('Failed to retrieve article list');
|
||||
}
|
||||
// Process articles
|
||||
foreach($html->find('div.assetBody, div.riverPost') as $element) {
|
||||
|
||||
foreach ($links as $article_uri) {
|
||||
$article_dom = convertLazyLoading(getSimpleHTMLDOMCached($article_uri));
|
||||
$title = trim($article_dom->find('h1', 0)->plaintext);
|
||||
$author = $article_dom->find('span.c-assetAuthor_name', 0);
|
||||
$headline = $article_dom->find('p.c-contentHeader_description', 0);
|
||||
$content = $article_dom->find('div.c-pageArticle_content, div.single-article__content, div.article-main-body', 0);
|
||||
$date = null;
|
||||
$enclosure = null;
|
||||
if(count($this->items) >= 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($article_dom->find('script[type=application/ld+json]') as $ldjson) {
|
||||
$datePublished = extractFromDelimiters($ldjson->innertext, '"datePublished":"', '"');
|
||||
if ($datePublished !== false) {
|
||||
$date = strtotime($datePublished);
|
||||
}
|
||||
$imageObject = extractFromDelimiters($ldjson->innertext, 'ImageObject","url":"', '"');
|
||||
if ($imageObject !== false) {
|
||||
$enclosure = $imageObject;
|
||||
}
|
||||
}
|
||||
$article_title = trim($element->find('h2, h3', 0)->plaintext);
|
||||
$article_uri = self::URI . substr($element->find('a', 0)->href, 1);
|
||||
$article_thumbnail = $element->parent()->find('img[src]', 0)->src;
|
||||
$article_timestamp = strtotime($element->find('time.assetTime, div.timeAgo', 0)->plaintext);
|
||||
$article_author = trim($element->find('a[rel=author], a.name', 0)->plaintext);
|
||||
$article_content = '<p><b>' . trim($element->find('p.dek', 0)->plaintext) . '</b></p>';
|
||||
|
||||
foreach ($content->find('div.c-shortcodeGallery') as $cleanup) {
|
||||
$cleanup->outertext = '';
|
||||
}
|
||||
if (is_null($article_thumbnail))
|
||||
$article_thumbnail = extractFromDelimiters($element->innertext, '<img src="', '"');
|
||||
|
||||
foreach ($content->find('figure') as $figure) {
|
||||
$img = $figure->find('img', 0);
|
||||
if ($img) {
|
||||
$figure->outertext = $img->outertext;
|
||||
}
|
||||
}
|
||||
if (!empty($article_title) && !empty($article_uri) && strpos($article_uri, self::URI . 'news/') !== false) {
|
||||
|
||||
$content = $content->innertext;
|
||||
$article_html = getSimpleHTMLDOMCached($article_uri) or $article_html = null;
|
||||
|
||||
if ($enclosure) {
|
||||
$content = "<div><img src=\"$enclosure\" /></div>" . $content;
|
||||
}
|
||||
if (!is_null($article_html)) {
|
||||
|
||||
if ($headline) {
|
||||
$content = '<p><b>' . $headline->plaintext . '</b></p><br />' . $content;
|
||||
}
|
||||
if (empty($article_thumbnail))
|
||||
$article_thumbnail = $article_html->find('div.originalImage', 0);
|
||||
if (empty($article_thumbnail))
|
||||
$article_thumbnail = $article_html->find('span.imageContainer', 0);
|
||||
if (is_object($article_thumbnail))
|
||||
$article_thumbnail = $article_thumbnail->find('img', 0)->src;
|
||||
|
||||
$item = [];
|
||||
$item['uri'] = $article_uri;
|
||||
$item['title'] = $title;
|
||||
$article_content .= trim(
|
||||
$this->cleanArticle(
|
||||
extractFromDelimiters(
|
||||
$article_html, '<article', '<footer'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ($author) {
|
||||
$item['author'] = $author->plaintext;
|
||||
}
|
||||
|
||||
$item['content'] = $content;
|
||||
|
||||
if (!is_null($date)) {
|
||||
$item['timestamp'] = $date;
|
||||
}
|
||||
|
||||
if (!is_null($enclosure)) {
|
||||
$item['enclosures'] = [$enclosure];
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
$item = array();
|
||||
$item['uri'] = $article_uri;
|
||||
$item['title'] = $article_title;
|
||||
$item['author'] = $article_author;
|
||||
$item['timestamp'] = $article_timestamp;
|
||||
$item['enclosures'] = array($article_thumbnail);
|
||||
$item['content'] = $article_content;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user