Compare commits

..

2 Commits
master ... fix1

Author SHA1 Message Date
Dag
a535121ab1 Merge remote-tracking branch 'origin/master' into fix1 2022-04-08 22:37:16 +02:00
Dag
b55c5090e6 fix: require curl extension 2022-04-08 19:54:08 +02:00
735 changed files with 57277 additions and 79209 deletions

View File

@ -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

View File

@ -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"
}

View File

@ -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"
}
}
]
}

View File

@ -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;
}
}

View File

@ -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'

View File

@ -1,4 +0,0 @@
# Reformat code base to PSR12
4f75591060d95208a301bc6bf460d875631b29cc
# Fix coding style missed by phpbcf
951092eef374db048b77bac85e75e3547bfac702

3
.gitattributes vendored
View File

@ -1,6 +1,5 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
*.sh text eol=lf
# Custom for Visual Studio # Custom for Visual Studio
*.cs diff=csharp *.cs diff=csharp
@ -47,6 +46,8 @@ phpcs.xml export-ignore
phpcompatibility.xml export-ignore phpcompatibility.xml export-ignore
tests/ export-ignore tests/ export-ignore
cache/.gitkeep export-ignore cache/.gitkeep export-ignore
bridges/DemoBridge.php export-ignore
bridges/FeedExpanderExampleBridge.php export-ignore
## Composer ## Composer
# #

7
.github/.gitignore vendored
View File

@ -1,7 +0,0 @@
# Visual Studio Code
.vscode/*
# Generated files
comment*.md
comment*.txt
*.html

View File

@ -1,7 +1,49 @@
### Pull request policy ### 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. * [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 ### 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. * [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)
* [abstract and final declarations MUST precede the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#abstract-and-final-declarations-must-precede-the-visibility-declaration)
* [static declaration MUST come after the visibility declaration](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#static-declaration-must-come-after-the-visibility-declaration)
* [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)

View File

@ -49,9 +49,9 @@ Please describe what you expect from the bridge. Whenever possible provide sampl
- _Default limit_: 5 - _Default limit_: 5
- [ ] Load full articles - [ ] Load full articles
- _Cache articles_ (articles are stored in a local cache on first request): yes - _Cache articles_ (articles are stored in a local cache on first request): yes
- _Cache timeout_ : 24 hours - _Cache timeout_ (max = 24 hours): 24 hours
- [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage) - [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage)
- _Timeout_ (default = 5 minutes): 5 minutes - _Timeout_ (default = 5 minutes, max = 24 hours): 5 minutes
<!--Be aware that some options might not be available for your specific request due to technical limitations!--> <!--Be aware that some options might not be available for your specific request due to technical limitations!-->
@ -60,5 +60,5 @@ Please describe what you expect from the bridge. Whenever possible provide sampl
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. 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. You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/For-developers) developer section.
--> -->

249
.github/prtester.py vendored
View File

@ -1,208 +1,101 @@
import argparse
import requests import requests
import re
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import datetime from datetime import datetime
from typing import Iterable import os.path
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 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 # 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. # 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 # It also replaces the default static CSS link with a hardcoded link to @em92's public instance, so viewing
# the HTML file locally will actually work as designed. # the HTML file locally will actually work as designed.
ARTIFACT_FILE_EXTENSION = '.html' def testBridges(bridges,status):
for bridge in bridges:
class Instance: if bridge.get('data-ref'): # Some div entries are empty, this ignores those
name = '' bridgeid = bridge.get('id')
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 bridgeid = bridgeid.split('-')[1] # this extracts a readable bridge name from the bridge metadata
print(f'{bridgeid}{instance_suffix}') bridgestring = '/?action=display&bridge=' + bridgeid + '&format=Html'
bridge_name = bridgeid.replace('Bridge', '') forms = bridge.find_all("form")
context_forms = bridge_card.find_all("form") formid = 1
form_number = 1 for form in forms:
for context_form in context_forms:
# a bridge can have multiple contexts, named 'forms' in html # 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 code will produce a fully working formstring that should create a working feed when called
# this will create an example feed for every single context, to test them all # this will create an example feed for every single context, to test them all
context_parameters = {} formstring = ''
error_messages = [] errormessages = []
context_name = '*untitled*' parameters = form.find_all("input")
context_name_element = context_form.find_previous_sibling('h5') lists = form.find_all("select")
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 # 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 # the default or examplevalue and then combines it all together into the formstring
# if an example or default value is missing for a required attribute, it will throw an error # if an example or default value is missing for a required attribute, it will throw an error
# any non-required fields are not tested!!! # any non-required fields are not tested!!!
for parameter in parameters: for parameter in parameters:
parameter_type = parameter.get('type') if parameter.get('type') == 'hidden' and parameter.get('name') == 'context':
parameter_name = parameter.get('name') cleanvalue = parameter.get('value').replace(" ","+")
if parameter_type == 'hidden': formstring = formstring + '&' + parameter.get('name') + '=' + cleanvalue
context_parameters[parameter_name] = parameter.get('value') if parameter.get('type') == 'number' or parameter.get('type') == 'text':
if parameter_type == 'number' or parameter_type == 'text':
if parameter.has_attr('required'): if parameter.has_attr('required'):
if parameter.get('placeholder') == '': if parameter.get('placeholder') == '':
if parameter.get('value') == '': if parameter.get('value') == '':
error_messages.append(f'Missing example or default value for parameter "{parameter_name}"') errormessages.append(parameter.get('name'))
else: else:
context_parameters[parameter_name] = parameter.get('value') formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('value')
else: else:
context_parameters[parameter_name] = parameter.get('placeholder') formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('placeholder')
# same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the url parameters # same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the formstring
if parameter_type == 'checkbox': if parameter.get('type') == 'checkbox':
if parameter.has_attr('checked'): if parameter.has_attr('checked'):
context_parameters[parameter_name] = 'on' formstring = formstring + '&' + parameter.get('name') + '=on'
for listing in lists: for list in lists:
selectionvalue = '' selectionvalue = ''
listname = listing.get('name') for selectionentry in list.contents:
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: if 'selected' in selectionentry.attrs:
selectionvalue = selectionentry.get('value') selectionvalue = selectionentry.get('value')
break break
context_parameters[listname] = selectionvalue if selectionvalue == '':
artifact_url = 'about:blank' selectionvalue = list.contents[0].get('value')
if error_messages: formstring = formstring + '&' + list.get('name') + '=' + selectionvalue
status = '<br>'.join(map(lambda m: f'❌ `{m}`', error_messages)) if not errormessages:
# if all example/default values are present, form the full request string, run the request, replace the static css
# file with the url of em's public instance and then upload it to termpad.com, a pastebin-like-site.
r = requests.get(URL + bridgestring + formstring)
pagetext = r.text.replace('static/HtmlFormat.css','https://feed.eugenemolotov.ru/static/HtmlFormat.css')
pagetext = pagetext.encode("utf_8")
termpad = requests.post(url="https://termpad.com/", data=pagetext)
termpadurl = termpad.text
termpadurl = termpadurl.replace('termpad.com/','termpad.com/raw/')
termpadurl = termpadurl.replace('\n','')
with open(os.getcwd() + '/comment.txt', 'a+') as file:
file.write("\n")
file.write("| [`" + bridgeid + '-' + status + '-context' + str(formid) + "`](" + termpadurl + ") | " + date_time + " |")
else: else:
# if all example/default values are present, form the full request url, run the request, add a <base> tag with # if there are errors (which means that a required value has no example or default value), log out which error appeared
# the url of em's public instance to the response text (so that relative paths work, e.g. to the static css file) and termpad = requests.post(url="https://termpad.com/", data=str(errormessages))
# then save it to a html file. termpadurl = termpad.text
context_parameters.update({ termpadurl = termpadurl.replace('termpad.com/','termpad.com/raw/')
'action': 'display', termpadurl = termpadurl.replace('\n','')
'bridge': bridgeid, with open(os.getcwd() + '/comment.txt', 'a+') as file:
'format': 'Html', file.write("\n")
}) file.write("| [`" + bridgeid + '-' + status + '-context' + str(formid) + "`](" + termpadurl + ") | " + date_time + " |")
request_url = f'{instance.url}/?{urllib.parse.urlencode(context_parameters)}' formid += 1
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: gitstatus = ["current", "pr"]
# trim whitespace and remove text that can break the table or is simply unnecessary now = datetime.now()
clean_value = re.sub(r'^\[[^\]]+\]\s*rssbridge\.|[\|`]', '', value.strip()) date_time = now.strftime("%Y-%m-%d, %H:%M:%S")
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__': with open(os.getcwd() + '/comment.txt', 'w+') as file:
parser = argparse.ArgumentParser() file.write(''' ## Pull request artifacts
parser.add_argument('--instances', nargs='+') | file | last change |
parser.add_argument('--no-upload', action='store_true') | ---- | ------ |''')
parser.add_argument('--reduced-upload', action='store_true')
parser.add_argument('--title', default='Pull request artifacts') for status in gitstatus: # run this twice, once for the current version, once for the PR version
parser.add_argument('--output-file', default=os.getcwd() + '/comment.txt') if status == "current":
args = parser.parse_args() port = "3000" # both ports are defined in the corresponding workflow .yml file
instances = [] elif status == "pr":
if args.instances: port = "3001"
for instance_arg in args.instances: URL = "http://localhost:" + port
instance_arg_parts = instance_arg.split('::') page = requests.get(URL) # Use python requests to grab the rss-bridge main page
instance = Instance() soup = BeautifulSoup(page.content, "html.parser") # use bs4 to turn the page into soup
instance.name = instance_arg_parts[1].strip() if len(instance_arg_parts) >= 2 else '' bridges = soup.find_all("section") # get a soup-formatted list of all bridges on the rss-bridge page
instance.url = instance_arg_parts[0].strip().rstrip("/") testBridges(bridges,status) # run the main scraping code with the list of bridges and the info if this is for the current version or the pr version
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
);

View File

@ -17,11 +17,11 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2.3.4
- -
name: Docker meta name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v3.5.0
with: with:
images: | images: |
${{ env.DOCKERHUB_SLUG }} ${{ env.DOCKERHUB_SLUG }}
@ -33,26 +33,26 @@ jobs:
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/20') }} type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/20') }}
- -
name: Set up QEMU name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v1
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v1.6.0
- -
name: Login to DockerHub name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v1.10.0
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- -
name: Login to GitHub Container Registry name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v1.10.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- -
name: Build and push name: Build and push
uses: docker/bake-action@v5 uses: docker/bake-action@v1.6.0
with: with:
files: | files: |
./docker-bake.hcl ./docker-bake.hcl

View File

@ -9,11 +9,11 @@ jobs:
documentation: documentation:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
with: with:
persist-credentials: false persist-credentials: false
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@2.17.1
with: with:
php-version: 8.0 php-version: 8.0
- name: Install dependencies - name: Install dependencies
@ -21,7 +21,7 @@ jobs:
- name: Generate documentation - name: Generate documentation
run: daux generate run: daux generate
- name: Deploy same repository 🚀 - name: Deploy same repository 🚀
uses: JamesIves/github-pages-deploy-action@v4 uses: JamesIves/github-pages-deploy-action@v4.2.5
with: with:
folder: "static" folder: "static"
branch: gh-pages branch: gh-pages

View File

@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
php-versions: ['7.4'] php-versions: ['7.1', '7.2', '7.3', '7.4']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php-versions }} php-version: ${{ matrix.php-versions }}
@ -24,13 +24,12 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
php-versions: ['7.4'] php-versions: ['7.1', '7.4']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php-versions }} 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 dealerdirect/phpcodesniffer-composer-installer
- run: composer global require phpcompatibility/php-compatibility - run: composer global require phpcompatibility/php-compatibility
- run: ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p - run: ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p
@ -38,7 +37,7 @@ jobs:
executable_php_files_check: executable_php_files_check:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- run: | - run: |
if find -name "*.php" -executable -type f -print -exec false {} + if find -name "*.php" -executable -type f -print -exec false {} +
then then

View File

@ -5,41 +5,24 @@ on:
branches: [ master ] branches: [ master ]
jobs: 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: test-pr:
name: Generate HTML name: Generate HTML
runs-on: ubuntu-latest 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 # Needs additional permissions https://github.com/actions/first-interaction/issues/10#issuecomment-1041402989
steps: steps:
- name: Check out self - name: Check out self
uses: actions/checkout@v4 uses: actions/checkout@v2.3.2
with: with:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Check out rss-bridge - name: Check out rss-bridge
run: | run: |
PR=${{github.event.number}}; 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 -O requirements.txt https://raw.githubusercontent.com/RSS-Bridge/rss-bridge/master/.github/prtester-requirements.txt;
wget https://raw.githubusercontent.com/$GITHUB_REPOSITORY/${{ github.event.pull_request.base.ref }}/.github/prtester.py; wget https://raw.githubusercontent.com/RSS-Bridge/rss-bridge/master/.github/prtester.py;
wget https://patch-diff.githubusercontent.com/raw/$GITHUB_REPOSITORY/pull/$PR.patch; wget https://patch-diff.githubusercontent.com/raw/$GITHUB_REPOSITORY/pull/$PR.patch;
touch DEBUG; 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 cat $PR.patch | grep " bridges/.*\.php" | sed "s= bridges/\(.*\)Bridge.php.*=\1=g" | sort | uniq > whitelist.txt
- name: Start Docker - Current - name: Start Docker - Current
run: | 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 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
@ -48,9 +31,9 @@ jobs:
docker build -t prbuild .; 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 docker run -d -v $GITHUB_WORKSPACE/whitelist.txt:/app/whitelist.txt -v $GITHUB_WORKSPACE/DEBUG:/app/DEBUG -p 3001:80 prbuild
- name: Setup python - name: Setup python
uses: actions/setup-python@v5 uses: actions/setup-python@v2
with: with:
python-version: '3.13' python-version: '3.7'
cache: 'pip' cache: 'pip'
- name: Install requirements - name: Install requirements
run: | run: |
@ -65,18 +48,11 @@ jobs:
body="${body//'%'/'%25'}"; body="${body//'%'/'%25'}";
body="${body//$'\n'/'%0A'}"; body="${body//$'\n'/'%0A'}";
body="${body//$'\r'/'%0D'}"; body="${body//$'\r'/'%0D'}";
echo "bodylength=${#body}" >> $GITHUB_OUTPUT echo "::set-output name=bodylength::${#body}"
env: echo "::set-output name=body::$body"
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 - name: Find Comment
if: ${{ steps.testrun.outputs.bodylength > 130 }} if: ${{ steps.testrun.outputs.bodylength > 130 }}
uses: peter-evans/find-comment@v3 uses: peter-evans/find-comment@v2
id: fc id: fc
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -84,43 +60,10 @@ jobs:
body-includes: Pull request artifacts body-includes: Pull request artifacts
- name: Create or update comment - name: Create or update comment
if: ${{ steps.testrun.outputs.bodylength > 130 }} if: ${{ steps.testrun.outputs.bodylength > 130 }}
uses: peter-evans/create-or-update-comment@v4 uses: peter-evans/create-or-update-comment@v2
with: with:
comment-id: ${{ steps.fc.outputs.comment-id }} comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
body-file: comment.txt body: |
${{ steps.testrun.outputs.body }}
edit-mode: replace 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

View File

@ -7,17 +7,28 @@ on:
branches: [ master ] branches: [ master ]
jobs: jobs:
phpunit7:
runs-on: ubuntu-20.04
strategy:
matrix:
php-versions: ['7.1', '7.2', '7.3']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- run: composer global require phpunit/phpunit ^7
- run: phpunit --configuration=phpunit.xml --include-path=lib/
phpunit8: phpunit8:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
php-versions: ['7.4', '8.0', '8.1'] php-versions: ['7.3', '7.4', '8.0', '8.1']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2 - uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php-versions }} php-version: ${{ matrix.php-versions }}
env: - run: composer global require phpunit/phpunit ^8
update: true - run: phpunit --configuration=phpunit.xml --include-path=lib/
- run: composer install
- run: composer test

5
.gitignore vendored
View File

@ -6,6 +6,7 @@ data/
*.pydevproject *.pydevproject
.project .project
.metadata .metadata
bin/
tmp/ tmp/
*.tmp *.tmp
*.bak *.bak
@ -228,10 +229,6 @@ pip-log.txt
/whitelist.txt /whitelist.txt
DEBUG DEBUG
config.ini.php config.ini.php
config/*
!config/nginx.conf
!config/php-fpm.conf
!config/php.ini
###################### ######################
## VisualStudioCode ## ## VisualStudioCode ##

View File

@ -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)

View File

@ -1,72 +1,24 @@
FROM debian:12-slim AS rssbridge FROM php:7-apache-buster
LABEL description="RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one." 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 repository="https://github.com/RSS-Bridge/rss-bridge"
LABEL website="https://github.com/RSS-Bridge/rss-bridge" LABEL website="https://github.com/RSS-Bridge/rss-bridge"
ARG DEBIAN_FRONTEND=noninteractive ENV APACHE_DOCUMENT_ROOT=/app
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 RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \
ENV CURL_IMPERSONATE ff91esr && apt-get --yes update \
&& apt-get --yes --no-install-recommends install \
# logs should go to stdout / stderr zlib1g-dev \
RUN ln -sfT /dev/stderr /var/log/nginx/error.log; \ libmemcached-dev \
ln -sfT /dev/stdout /var/log/nginx/access.log; \ && rm -rf /var/lib/apt/lists/* \
chown -R --no-dereference www-data:adm /var/log/nginx/ && pecl install memcached \
&& docker-php-ext-enable memcached \
COPY ./config/nginx.conf /etc/nginx/sites-available/default && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
COPY ./config/php-fpm.conf /etc/php/8.2/fpm/pool.d/rss-bridge.conf && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
COPY ./config/php.ini /etc/php/8.2/fpm/conf.d/90-rss-bridge.ini && sed -ri -e 's/(MinProtocol\s*=\s*)TLSv1\.2/\1None/' /etc/ssl/openssl.cnf \
&& sed -ri -e 's/(CipherString\s*=\s*DEFAULT)@SECLEVEL=2/\1/' /etc/ssl/openssl.cnf
COPY --chown=www-data:www-data ./ /app/ COPY --chown=www-data:www-data ./ /app/
EXPOSE 80 CMD ["/app/docker-entrypoint.sh"]
ENTRYPOINT ["/app/docker-entrypoint.sh"]

777
README.md
View File

@ -1,527 +1,336 @@
# RSS-Bridge
![RSS-Bridge](static/logo_600px.png) ![RSS-Bridge](static/logo_600px.png)
===
RSS-Bridge is a PHP web application.
It generates web feeds for websites that don't have one.
Officially hosted instance: https://rss-bridge.org/bridge01/
IRC channel #rssbridge at https://libera.chat/
[Full documentation](https://rss-bridge.github.io/rss-bridge/index.html)
Alternatively find another
[public instance](https://rss-bridge.github.io/rss-bridge/General/Public_Hosts.html).
Requires minimum PHP 7.4.
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE)
[![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest)
[![irc.libera.chat](https://img.shields.io/badge/irc.libera.chat-%23rssbridge-blue.svg)](https://web.libera.chat/#rssbridge) [![irc.libera.chat](https://img.shields.io/badge/irc.libera.chat-%23rssbridge-blue.svg)](https://web.libera.chat/#rssbridge)
[![Actions Status](https://img.shields.io/github/actions/workflow/status/RSS-Bridge/rss-bridge/tests.yml?branch=master&label=GitHub%20Actions&logo=github)](https://github.com/RSS-Bridge/rss-bridge/actions) [![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#rssbridge:libera.chat)
[![Actions Status](https://img.shields.io/github/workflow/status/RSS-Bridge/rss-bridge/Tests/master?label=GitHub%20Actions&logo=github)](https://github.com/RSS-Bridge/rss-bridge/actions)
[![Docker Build Status](https://img.shields.io/docker/cloud/build/rssbridge/rss-bridge?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/)
||| RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode.
|:-:|:-:|
|![Screenshot #1](/static/screenshot-1.png?raw=true)|![Screenshot #2](/static/screenshot-2.png?raw=true)|
|![Screenshot #3](/static/screenshot-3.png?raw=true)|![Screenshot #4](/static/screenshot-4.png?raw=true)|
|![Screenshot #5](/static/screenshot-5.png?raw=true)|![Screenshot #6](/static/screenshot-6.png?raw=true)|
## A subset of bridges (15/447) **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).
* `CssSelectorBridge`: [Scrape out a feed using CSS selectors](https://rss-bridge.org/bridge01/#bridge-CssSelectorBridge) Supported sites/pages (examples)
* `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 * `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/) (There is an [issue](https://github.com/RSS-Bridge/rss-bridge/issues/2047) for public instances)
* `FlickrExplore` : [Latest interesting images](http://www.flickr.com/explore) from Flickr
* `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 (It is recommended to [configure](https://rss-bridge.github.io/rss-bridge/Bridge_Specific/Instagram.html) this bridge to work)
* `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
### How to install on traditional shared web hosting And [many more](bridges/), thanks to the community!
RSS-Bridge can basically be unzipped into a web folder. Should be working instantly. Output format
===
Latest zip: RSS-Bridge is capable of producing several output formats:
https://github.com/RSS-Bridge/rss-bridge/archive/refs/heads/master.zip (2MB)
### How to install on Debian 12 (nginx + php-fpm) * `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
These instructions have been tested on a fresh Debian 12 VM from Digital Ocean (1vcpu-512mb-10gb, 5 USD/month). You can extend RSS-Bridge with your own format, using the [Format API](https://github.com/RSS-Bridge/rss-bridge/wiki/Format-API)!
```shell Screenshot
timedatectl set-timezone Europe/Oslo ===
apt install git nginx php8.2-fpm php-mbstring php-simplexml php-curl php-intl Welcome screen:
# Create a user account ![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_rss-bridge_welcome.png)
useradd --shell /bin/bash --create-home rss-bridge
cd /var/www ***
# Create folder and change its ownership to rss-bridge RSS-Bridge hashtag (#rss-bridge) search on Twitter, in Atom format (as displayed by Firefox):
mkdir rss-bridge && chown rss-bridge:rss-bridge rss-bridge/
# Become rss-bridge ![Screenshot](https://github.com/RSS-Bridge/rss-bridge/wiki/images/screenshot_twitterbridge_atom.png)
su rss-bridge
# Clone master branch into existing folder Requirements
git clone https://github.com/RSS-Bridge/rss-bridge.git rss-bridge/ ===
cd rss-bridge
# Copy over the default config (OPTIONAL) RSS-Bridge requires PHP 7.1 or higher with following extensions enabled:
cp -v config.default.ini.php config.ini.php
# Recursively give full permissions to user/owner - [`openssl`](https://secure.php.net/manual/en/book.openssl.php)
chmod 700 --recursive ./ - [`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)
- [`filter`](https://secure.php.net/manual/en/book.filter.php)
- [`sqlite3`](http://php.net/manual/en/book.sqlite3.php) (only when using SQLiteCache)
# Give read and execute to others on folder ./static Find more information on our [Wiki](https://github.com/rss-bridge/rss-bridge/wiki)
chmod o+rx ./ ./static
# Recursively give give read to others on folder ./static Enable / Disable bridges
chmod o+r --recursive ./static ===
```
Nginx config: 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!
```nginx Find more information on the [Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitelisting)
# /etc/nginx/sites-enabled/rss-bridge.conf
server { **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!
listen 80;
# TODO: change to your own server name Deploy
server_name example.com; ===
access_log /var/log/nginx/rss-bridge.access.log; Thanks to the community, hosting your own instance of RSS-Bridge is as easy as clicking a button!
error_log /var/log/nginx/rss-bridge.error.log; *Note: External providers' applications are packaged by 3rd parties. Use at your own discretion.*
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
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge) [![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/sebsauvage/rss-bridge)
[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
[![Deploy to Cloudron](https://cloudron.io/img/button.svg)](https://www.cloudron.io/store/com.rssbridgeapp.cloudronapp.html) [![Deploy to Cloudron](https://cloudron.io/img/button.svg)](https://www.cloudron.io/store/com.rssbridgeapp.cloudronapp.html)
[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=rssbridge)
The Heroku quick deploy currently does not work. It might work if you fork this repo and Getting involved
modify the `repository` in `scalingo.json`. See https://github.com/RSS-Bridge/rss-bridge/issues/2688 ===
Learn more in There are many ways for you to getting involved with RSS-Bridge. Here are a few things:
[Installation](https://rss-bridge.github.io/rss-bridge/For_Hosts/Installation.html).
- Share RSS-Bridge with your friends (Twitter, Facebook, ..._you name it_...)
## How-to - 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))
### How to fix "Access denied." - Discuss bugs, features, ideas or [issues](https://github.com/RSS-Bridge/rss-bridge/issues)
- Add new bridges or improve the API
Output is from php-fpm. It is unable to read index.php. - 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:
chown rss-bridge:rss-bridge /var/www/rss-bridge/index.php
Authors
### How to password-protect the instance (token) ===
Modify `config.ini.php`: 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).
[authentication] **Contributors** (sorted alphabetically):
<!--
token = "hunter2" Use this script to generate the list automatically (using the GitHub API):
./contrib/prepare_release/fetch_contributors.php
### How to remove all cache items -->
As current user: * [16mhz](https://github.com/16mhz)
* [adamchainz](https://github.com/adamchainz)
bin/cache-clear * [Ahiles3005](https://github.com/Ahiles3005)
* [akirk](https://github.com/akirk)
As user rss-bridge: * [Albirew](https://github.com/Albirew)
* [aledeg](https://github.com/aledeg)
sudo -u rss-bridge bin/cache-clear * [alex73](https://github.com/alex73)
* [alexAubin](https://github.com/alexAubin)
As root: * [AmauryCarrade](https://github.com/AmauryCarrade)
* [AntoineTurmel](https://github.com/AntoineTurmel)
sudo bin/cache-clear * [arnd-s](https://github.com/arnd-s)
* [ArthurHoaro](https://github.com/ArthurHoaro)
### How to remove all expired cache items * [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
bin/cache-prune * [AxorPL](https://github.com/AxorPL)
* [ayacoo](https://github.com/ayacoo)
### How to fix "PHP Fatal error: Uncaught Exception: The FileCache path is not writable" * [az5he6ch](https://github.com/az5he6ch)
* [b1nj](https://github.com/b1nj)
```shell * [benasse](https://github.com/benasse)
# Give rss-bridge ownership * [Binnette](https://github.com/Binnette)
chown rss-bridge:rss-bridge -R /var/www/rss-bridge/cache * [Bockiii](https://github.com/Bockiii)
* [captn3m0](https://github.com/captn3m0)
# Or, give www-data ownership * [chemel](https://github.com/chemel)
chown www-data:www-data -R /var/www/rss-bridge/cache * [Chouchen](https://github.com/Chouchen)
* [ckiw](https://github.com/ckiw)
# Or, give everyone write permission * [cn-tools](https://github.com/cn-tools)
chmod 777 -R /var/www/rss-bridge/cache * [cnlpete](https://github.com/cnlpete)
* [corenting](https://github.com/corenting)
# Or last ditch effort (CAREFUL) * [couraudt](https://github.com/couraudt)
rm -rf /var/www/rss-bridge/cache/ && mkdir /var/www/rss-bridge/cache/ * [csisoap](https://github.com/csisoap)
``` * [da2x](https://github.com/da2x)
* [dabenzel](https://github.com/dabenzel)
### How to fix "attempt to write a readonly database" * [Daiyousei](https://github.com/Daiyousei)
* [dawidsowa](https://github.com/dawidsowa)
The sqlite files (db, wal and shm) are not writeable. * [DevonHess](https://github.com/DevonHess)
* [dhuschde](https://github.com/dhuschde)
chown -v rss-bridge:rss-bridge cache/* * [disk0x](https://github.com/disk0x)
* [DJCrashdummy](https://github.com/DJCrashdummy)
### How to fix "Unable to prepare statement: 1, no such table: storage" * [Djuuu](https://github.com/Djuuu)
* [DnAp](https://github.com/DnAp)
rm cache/* * [dominik-th](https://github.com/dominik-th)
* [Draeli](https://github.com/Draeli)
### How to create a new bridge from scratch * [Dreckiger-Dan](https://github.com/Dreckiger-Dan)
* [drego85](https://github.com/drego85)
Create the new bridge in e.g. `bridges/BearBlogBridge.php`: * [drklee3](https://github.com/drklee3)
* [dvikan](https://github.com/dvikan)
```php * [em92](https://github.com/em92)
<?php * [eMerzh](https://github.com/eMerzh)
* [EtienneM](https://github.com/EtienneM)
class BearBlogBridge extends BridgeAbstract * [f0086](https://github.com/f0086)
{ * [fanch317](https://github.com/fanch317)
const NAME = 'BearBlog (bearblog.dev)'; * [fatuuse](https://github.com/fatuuse)
* [fivefilters](https://github.com/fivefilters)
public function collectData() * [floviolleau](https://github.com/floviolleau)
{ * [fluffy-critter](https://github.com/fluffy-critter)
$dom = getSimpleHTMLDOM('https://herman.bearblog.dev/blog/'); * [fmachen](https://github.com/fmachen)
foreach ($dom->find('.blog-posts li') as $li) { * [Frenzie](https://github.com/Frenzie)
$a = $li->find('a', 0); * [fulmeek](https://github.com/fulmeek)
$this->items[] = [ * [ggiessen](https://github.com/ggiessen)
'title' => $a->plaintext, * [Ginko-Aloe](https://github.com/Ginko-Aloe)
'uri' => 'https://herman.bearblog.dev' . $a->href, * [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)
Learn more in [bridge api](https://rss-bridge.github.io/rss-bridge/Bridge_API/index.html). * [guigot](https://github.com/guigot)
* [hollowleviathan](https://github.com/hollowleviathan)
### How to enable all bridges * [hpacleb](https://github.com/hpacleb)
* [hunhejj](https://github.com/hunhejj)
enabled_bridges[] = * * [husim0](https://github.com/husim0)
* [IceWreck](https://github.com/IceWreck)
### How to enable some bridges * [imagoiq](https://github.com/imagoiq)
* [j0k3r](https://github.com/j0k3r)
``` * [JackNUMBER](https://github.com/JackNUMBER)
enabled_bridges[] = TwitchBridge * [jacquesh](https://github.com/jacquesh)
enabled_bridges[] = GettrBridge * [JasonGhent](https://github.com/JasonGhent)
``` * [jcgoette](https://github.com/jcgoette)
* [jdesgats](https://github.com/jdesgats)
### How to enable debug mode * [jdigilio](https://github.com/jdigilio)
* [JeremyRand](https://github.com/JeremyRand)
The * [JimDog546](https://github.com/JimDog546)
[debug mode](https://rss-bridge.github.io/rss-bridge/For_Developers/Debug_mode.html) * [jNullj](https://github.com/jNullj)
disables the majority of caching operations. * [Jocker666z](https://github.com/Jocker666z)
* [johnnygroovy](https://github.com/johnnygroovy)
enable_debug_mode = true * [johnpc](https://github.com/johnpc)
* [joni1993](https://github.com/joni1993)
### How to switch to memcached as cache backend * [KamaleiZestri](https://github.com/KamaleiZestri)
* [klimplant](https://github.com/klimplant)
``` * [kolarcz](https://github.com/kolarcz)
[cache] * [kranack](https://github.com/kranack)
* [kraoc](https://github.com/kraoc)
; Cache backend: file (default), sqlite, memcached, null * [l1n](https://github.com/l1n)
type = "memcached" * [laBecasse](https://github.com/laBecasse)
``` * [lagaisse](https://github.com/lagaisse)
* [lalannev](https://github.com/lalannev)
### How to switch to sqlite3 as cache backend * [ldidry](https://github.com/ldidry)
* [Leomaradan](https://github.com/Leomaradan)
type = "sqlite" * [leyrer](https://github.com/leyrer)
* [liamka](https://github.com/liamka)
### How to disable bridge errors (as feed items) * [Limero](https://github.com/Limero)
* [LogMANOriginal](https://github.com/LogMANOriginal)
When a bridge fails, RSS-Bridge will produce a feed with a single item describing the error. * [lorenzos](https://github.com/lorenzos)
* [lukasklinger](https://github.com/lukasklinger)
This way, feed readers pick it up and you are notified. * [m0zes](https://github.com/m0zes)
* [Mar-Koeh](https://github.com/Mar-Koeh)
If you don't want this behaviour, switch the error output to `http`: * [marcus-at-localhost](https://github.com/marcus-at-localhost)
* [marius8510000-bot](https://github.com/marius8510000-bot)
[error] * [matthewseal](https://github.com/matthewseal)
* [mcbyte-it](https://github.com/mcbyte-it)
; Defines how error messages are returned by RSS-Bridge * [mdemoss](https://github.com/mdemoss)
; * [melangue](https://github.com/melangue)
; "feed" = As part of the feed (default) * [metaMMA](https://github.com/metaMMA)
; "http" = As HTTP error message * [mibe](https://github.com/mibe)
; "none" = No errors are reported * [mightymt](https://github.com/mightymt)
output = "http" * [mitsukarenai](https://github.com/mitsukarenai)
* [Monocularity](https://github.com/Monocularity)
### How to accumulate errors before finally reporting it * [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
* [mr-flibble](https://github.com/mr-flibble)
Modify `report_limit` so that an error must occur 3 times before it is reported. * [mro](https://github.com/mro)
* [mschwld](https://github.com/mschwld)
; Defines how often an error must occur before it is reported to the user * [mxmehl](https://github.com/mxmehl)
report_limit = 3 * [Mynacol](https://github.com/Mynacol)
* [nel50n](https://github.com/nel50n)
The report count is reset to 0 each day. * [niawag](https://github.com/niawag)
* [Niehztog](https://github.com/Niehztog)
### How to password-protect the instance (HTTP Basic Auth) * [Nono-m0le](https://github.com/Nono-m0le)
* [obsiwitch](https://github.com/obsiwitch)
[authentication] * [Ololbu](https://github.com/Ololbu)
* [ORelio](https://github.com/ORelio)
enable = true * [otakuf](https://github.com/otakuf)
username = "alice" * [Park0](https://github.com/Park0)
password = "cat" * [Paroleen](https://github.com/Paroleen)
* [PaulVayssiere](https://github.com/PaulVayssiere)
Will typically require feed readers to be configured with the credentials. * [pellaeon](https://github.com/pellaeon)
* [PeterDaveHello](https://github.com/PeterDaveHello)
It may also be possible to manually include the credentials in the URL: * [Peterr-K](https://github.com/Peterr-K)
* [Piranhaplant](https://github.com/Piranhaplant)
https://alice:cat@rss-bridge.org/bridge01/?action=display&bridge=FabriceBellardBridge&format=Html * [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
* [pitchoule](https://github.com/pitchoule)
### How to create a new output format * [pmaziere](https://github.com/pmaziere)
* [Pofilo](https://github.com/Pofilo)
See `formats/PlaintextFormat.php` for an example. * [prysme01](https://github.com/prysme01)
* [Qluxzz](https://github.com/Qluxzz)
### How to run unit tests and linter * [quentinus95](https://github.com/quentinus95)
* [rakoo](https://github.com/rakoo)
These commands require that you have installed the dev dependencies in `composer.json`. * [RawkBob](https://github.com/RawkBob)
* [regisenguehard](https://github.com/regisenguehard)
Run all tests: * [Riduidel](https://github.com/Riduidel)
* [rogerdc](https://github.com/rogerdc)
./vendor/bin/phpunit * [Roliga](https://github.com/Roliga)
* [ronansalmon](https://github.com/ronansalmon)
Run a single test class: * [rremizov](https://github.com/rremizov)
* [sal0max](https://github.com/sal0max)
./vendor/bin/phpunit --filter UrlTest * [sebsauvage](https://github.com/sebsauvage)
* [shutosg](https://github.com/shutosg)
Run linter: * [simon816](https://github.com/simon816)
* [Simounet](https://github.com/Simounet)
./vendor/bin/phpcs --standard=phpcs.xml --warning-severity=0 --extensions=php -p ./ * [somini](https://github.com/somini)
* [SpangleLabs](https://github.com/SpangleLabs)
https://github.com/squizlabs/PHP_CodeSniffer/wiki * [squeek502](https://github.com/squeek502)
* [stjohnjohnson](https://github.com/stjohnjohnson)
### How to spawn a minimal development environment * [Stopka](https://github.com/Stopka)
* [Strubbl](https://github.com/Strubbl)
php -S 127.0.0.1:9001 * [sublimz](https://github.com/sublimz)
* [sunchaserinfo](https://github.com/sunchaserinfo)
http://127.0.0.1:9001/ * [SuperSandro2000](https://github.com/SuperSandro2000)
* [sysadminstory](https://github.com/sysadminstory)
## Explanation * [t0stiman](https://github.com/t0stiman)
* [tameroski](https://github.com/tameroski)
We are RSS-Bridge community, a group of developers continuing the project initiated by sebsauvage, * [teromene](https://github.com/teromene)
webmaster of * [tgkenney](https://github.com/tgkenney)
[sebsauvage.net](https://sebsauvage.net), author of * [thefranke](https://github.com/thefranke)
[Shaarli](https://sebsauvage.net/wiki/doku.php?id=php:shaarli) and * [ThePadawan](https://github.com/ThePadawan)
[ZeroBin](https://sebsauvage.net/wiki/doku.php?id=php:zerobin). * [TheRadialActive](https://github.com/TheRadialActive)
* [theScrabi](https://github.com/theScrabi)
See [CONTRIBUTORS.md](CONTRIBUTORS.md) * [thezeroalpha](https://github.com/thezeroalpha)
* [timendum](https://github.com/timendum)
RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds. * [TitiTestScalingo](https://github.com/TitiTestScalingo)
The specific cache duration can be different between bridges. * [tomaszkane](https://github.com/tomaszkane)
* [TReKiE](https://github.com/TReKiE)
RSS-Bridge allows you to take full control over which bridges are displayed to the user. * [triatic](https://github.com/triatic)
That way you can host your own RSS-Bridge service with your favorite collection of bridges! * [VerifiedJoseph](https://github.com/VerifiedJoseph)
* [WalterBarrett](https://github.com/WalterBarrett)
Current maintainers (as of 2024): @dvikan and @Mynacol #2519 * [wtuuju](https://github.com/wtuuju)
* [xurxof](https://github.com/xurxof)
## Reference * [yamanq](https://github.com/yamanq)
* [yardenac](https://github.com/yardenac)
### Feed item structure * [ymeister](https://github.com/ymeister)
* [yue-dongchen](https://github.com/yue-dongchen)
This is the feed item structure that bridges are expected to produce. * [ZeNairolf](https://github.com/ZeNairolf)
```php Licenses
$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
The source code for RSS-Bridge is [Public Domain](UNLICENSE). The source code for RSS-Bridge is [Public Domain](UNLICENSE).
RSS-Bridge uses third party libraries with their own license: 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) * [`Parsedown`](https://github.com/erusev/parsedown) licensed under the [MIT License](http://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 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](https://opensource.org/licenses/MIT) * [`php-urljoin`](https://github.com/fluffy-critter/php-urljoin) licensed under the [MIT License](http://opensource.org/licenses/MIT)
* [`Laravel framework`](https://github.com/laravel/framework/) licensed under the [MIT License](https://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.* *Dear so-called "social" websites.*

View File

@ -1,4 +1,15 @@
<?php <?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
/** /**
* Checks if the website for a given bridge is reachable. * Checks if the website for a given bridge is reachable.
@ -10,58 +21,116 @@
* - Returns a responsive web page that automatically checks all whitelisted * - Returns a responsive web page that automatically checks all whitelisted
* bridges (using JavaScript) if no bridge is specified. * bridges (using JavaScript) if no bridge is specified.
*/ */
class ConnectivityAction implements ActionInterface class ConnectivityAction extends ActionAbstract {
{ public function execute() {
private BridgeFactory $bridgeFactory;
public function __construct( if(!Debug::isEnabled()) {
BridgeFactory $bridgeFactory returnError('This action is only available in debug mode!');
) {
$this->bridgeFactory = $bridgeFactory;
} }
public function __invoke(Request $request): Response if(!isset($this->userData['bridge'])) {
{ $this->returnEntryPage();
if (!Debug::isEnabled()) { return;
return new Response('This action is only available in debug mode!', 403);
} }
$bridgeName = $request->get('bridge'); $bridgeName = $this->userData['bridge'];
if (!$bridgeName) {
return new Response(render_template('connectivity.html.php')); $this->reportBridgeConnectivity($bridgeName);
}
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
if (!$bridgeClassName) {
return new Response('Bridge not found', 404);
}
return $this->reportBridgeConnectivity($bridgeClassName);
} }
private function reportBridgeConnectivity($bridgeClassName) /**
{ * Generates a report about the bridge connectivity status and sends it back
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) { * to the user.
throw new \Exception('Bridge is not whitelisted!'); *
* The report is generated as Json-formatted string in the format
* {
* "bridge": "<bridge-name>",
* "successful": true/false
* }
*
* @param string $bridgeName Name of the bridge to generate the report for
* @return void
*/
private function reportBridgeConnectivity($bridgeName) {
$bridgeFac = new \BridgeFactory();
$bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
if(!$bridgeFac->isWhitelisted($bridgeName)) {
header('Content-Type: text/html');
returnServerError('Bridge is not whitelisted!');
} }
$bridge = $this->bridgeFactory->create($bridgeClassName); header('Content-Type: text/json');
$curl_opts = [
CURLOPT_CONNECTTIMEOUT => 5, $retVal = array(
CURLOPT_FOLLOWLOCATION => true, 'bridge' => $bridgeName,
];
$result = [
'bridge' => $bridgeClassName,
'successful' => false, 'successful' => false,
'http_code' => null, 'http_code' => 200,
]; );
try {
$response = getContents($bridge::URI, [], $curl_opts, true); $bridge = $bridgeFac->create($bridgeName);
$result['http_code'] = $response->getCode();
if (in_array($result['http_code'], [200])) { if($bridge === false) {
$result['successful'] = true; echo json_encode($retVal);
} return;
} catch (\Exception $e) {
} }
return new Response(Json::encode($result), 200, ['content-type' => 'text/json']); $curl_opts = array(
CURLOPT_CONNECTTIMEOUT => 5
);
try {
$reply = getContents($bridge::URI, array(), $curl_opts, true);
if($reply) {
$retVal['successful'] = true;
if (isset($reply['header'])) {
if (strpos($reply['header'], 'HTTP/1.1 301 Moved Permanently') !== false) {
$retVal['http_code'] = 301;
}
}
}
} catch(Exception $e) {
$retVal['successful'] = false;
}
echo json_encode($retVal);
}
private function returnEntryPage() {
echo <<<EOD
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="static/bootstrap.min.css">
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
crossorigin="anonymous">
<link rel="stylesheet" href="static/connectivity.css">
<script src="static/connectivity.js" type="text/javascript"></script>
</head>
<body>
<div id="main-content" class="container">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div id="status-message" class="sticky-top alert alert-primary alert-dismissible fade show" role="alert">
<i id="status-icon" class="fas fa-sync"></i>
<span>...</span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="stopConnectivityChecks()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<input type="text" class="form-control" id="search" onkeyup="search()" placeholder="Search for bridge..">
</div>
</body>
</html>
EOD;
} }
} }

View File

@ -1,51 +1,53 @@
<?php <?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DetectAction implements ActionInterface class DetectAction extends ActionAbstract {
{ public function execute() {
private BridgeFactory $bridgeFactory; $targetURL = $this->userData['url']
or returnClientError('You must specify a url!');
public function __construct( $format = $this->userData['format']
BridgeFactory $bridgeFactory or returnClientError('You must specify a format!');
) {
$this->bridgeFactory = $bridgeFactory;
}
public function __invoke(Request $request): Response $bridgeFac = new \BridgeFactory();
{ $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$url = $request->get('url');
$format = $request->get('format');
if (!$url) { foreach($bridgeFac->getBridgeNames() as $bridgeName) {
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(!$bridgeFac->isWhitelisted($bridgeName)) {
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
continue; continue;
} }
$bridge = $this->bridgeFactory->create($bridgeClassName); $bridge = $bridgeFac->create($bridgeName);
$bridgeParams = $bridge->detectParameters($url); if($bridge === false) {
if (!$bridgeParams) {
continue; continue;
} }
$query = [ $bridgeParams = $bridge->detectParameters($targetURL);
'action' => 'display',
'bridge' => $bridgeClassName, if(is_null($bridgeParams)) {
'format' => $format, continue;
];
$query = array_merge($query, $bridgeParams);
return new Response('', 301, ['location' => '?' . http_build_query($query)]);
} }
return new Response(render(__DIR__ . '/../templates/error.html.php', [ $bridgeParams['bridge'] = $bridgeName;
'message' => 'No bridge found for given URL: ' . $url, $bridgeParams['format'] = $format;
]));
header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
die();
}
returnClientError('No bridge found for given URL: ' . $targetURL);
} }
} }

View File

@ -1,231 +1,260 @@
<?php <?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class DisplayAction implements ActionInterface class DisplayAction extends ActionAbstract {
{ private function get_return_code($error) {
private CacheInterface $cache; $returnCode = $error->getCode();
private Logger $logger; if ($returnCode === 301 || $returnCode === 302) {
private BridgeFactory $bridgeFactory; # Don't pass redirect codes to the exterior
$returnCode = 508;
public function __construct( }
CacheInterface $cache, return $returnCode;
Logger $logger,
BridgeFactory $bridgeFactory
) {
$this->cache = $cache;
$this->logger = $logger;
$this->bridgeFactory = $bridgeFactory;
} }
public function __invoke(Request $request): Response public function execute() {
{ $bridge = array_key_exists('bridge', $this->userData) ? $this->userData['bridge'] : null;
$bridgeName = $request->get('bridge');
$format = $request->get('format');
$noproxy = $request->get('_noproxy');
if (!$bridgeName) { $format = $this->userData['format']
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge name parameter']), 400); or returnClientError('You must specify a format!');
}
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName); $bridgeFac = new \BridgeFactory();
if (!$bridgeClassName) { $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Bridge not found']), 404);
// whitelist control
if(!$bridgeFac->isWhitelisted($bridge)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
} }
if (!$format) { // Data retrieval
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']), 400); $bridge = $bridgeFac->create($bridge);
} $bridge->loadConfiguration();
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) $noproxy = array_key_exists('_noproxy', $this->userData)
if ( && filter_var($this->userData['_noproxy'], FILTER_VALIDATE_BOOLEAN);
Configuration::getConfig('proxy', 'url')
&& Configuration::getConfig('proxy', 'by_bridge') if(defined('PROXY_URL') && PROXY_BYBRIDGE && $noproxy) {
&& $noproxy
) {
// This const is only used once in getContents()
define('NOPROXY', true); define('NOPROXY', true);
} }
$cacheKey = 'http_' . json_encode($request->toArray()); // Cache timeout
$cache_timeout = -1;
if(array_key_exists('_cache_timeout', $this->userData)) {
$bridge = $this->bridgeFactory->create($bridgeClassName); if(!CUSTOM_CACHE_TIMEOUT) {
unset($this->userData['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($this->userData);
header('Location: ' . $uri, true, 301);
die();
}
$response = $this->createResponse($request, $bridge, $format); $cache_timeout = filter_var($this->userData['_cache_timeout'], FILTER_VALIDATE_INT);
if ($response->getCode() === 200) {
$ttl = $request->get('_cache_timeout');
if (Configuration::getConfig('cache', 'custom_timeout') && $ttl) {
$ttl = (int) $ttl;
} else { } else {
$ttl = $bridge->getCacheTimeout(); $cache_timeout = $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 parameters that don't concern bridges
$remove = [ $bridge_params = array_diff_key(
'token', $this->userData,
array_fill_keys(
array(
'action', 'action',
'bridge', 'bridge',
'format', 'format',
'_noproxy', '_noproxy',
'_cache_timeout', '_cache_timeout',
'_error_time', '_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, '')); // Remove parameters that don't concern caches
$bridge->setInput($input); $cache_params = array_diff_key(
$this->userData,
array_fill_keys(
array(
'action',
'format',
'_noproxy',
'_cache_timeout',
'_error_time'
), '')
);
// Initialize cache
$cacheFac = new CacheFactory();
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
$cache->setScope('');
$cache->purgeCache(86400); // 24 hours
$cache->setKey($cache_params);
$items = array();
$infos = array();
$mtime = $cache->getTime();
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
&& !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$stime = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($mtime <= $stime) { // Cached data is older or same
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $mtime) . 'GMT', true, 304);
die();
}
}
$cached = $cache->loadData();
if(isset($cached['items']) && isset($cached['extraInfos'])) {
foreach($cached['items'] as $item) {
$items[] = new \FeedItem($item);
}
$infos = $cached['extraInfos'];
}
} else { // Collect new data
try {
$bridge->setDatas($bridge_params);
$bridge->collectData(); $bridge->collectData();
$items = $bridge->getItems(); $items = $bridge->getItems();
} catch (\Throwable $e) {
if ($e instanceof RateLimitException) { // Transform "legacy" items to FeedItems if necessary.
// These are internally generated by bridges // Remove this code when support for "legacy" items ends!
$this->logger->info(sprintf('RateLimitException in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e))); if(isset($items[0]) && is_array($items[0])) {
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 429); $feedItems = array();
foreach($items as $item) {
$feedItems[] = new \FeedItem($item);
} }
if ($e instanceof HttpException) {
if (in_array($e->getCode(), [429, 503])) { $items = $feedItems;
// 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)
$infos = array(
'name' => $bridge->getName(),
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'icon' => $bridge->getIcon()
);
} catch(Error $e) {
error_log($e);
if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
// Error 0 is a special case (i.e. "trying to get property of non-object")
if($e->getCode() === 0) {
$item->setTitle(
'Bridge encountered an unexpected situation! ('
. $this->userData['_error_time']
. ')'
);
} else { } else {
// Log error if it's not an HttpException $item->setTitle(
$this->logger->error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]); 'Bridge returned error '
} . $e->getCode()
$errorOutput = Configuration::getConfig('error', 'output'); . '! ('
$reportLimit = Configuration::getConfig('error', 'report_limit'); . $this->userData['_error_time']
$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())
); );
} }
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
} catch(Exception $e) {
error_log($e);
if(logBridgeError($bridge::NAME, $e->getCode()) >= Configuration::getConfig('error', 'report_limit')) {
if(Configuration::getConfig('error', 'output') === 'feed') {
$item = new \FeedItem();
// Create "new" error message every 24 hours
$this->userData['_error_time'] = urlencode((int)(time() / 86400));
$item->setURI(
(isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
. '?'
. http_build_query($this->userData)
);
$item->setTitle(
'Bridge returned error '
. $e->getCode()
. '! ('
. $this->userData['_error_time']
. ')'
);
$item->setTimestamp(time());
$item->setContent(buildBridgeException($e, $bridge));
$items[] = $item;
} elseif(Configuration::getConfig('error', 'output') === 'http') {
header('Content-Type: text/html', true, $this->get_return_code($e));
die(buildTransformException($e, $bridge));
}
}
}
// Store data in cache
$cache->saveData(array(
'items' => array_map(function($i){ return $i->toArray(); }, $items),
'extraInfos' => $infos
));
}
// Data transformation
try {
$formatFac = new FormatFactory();
$formatFac->setWorkingDir(PATH_LIB_FORMATS);
$format = $formatFac->create($format);
$format->setItems($items);
$format->setExtraInfos($infos);
$format->setLastModified($cache->getTime());
$format->display();
} catch(Error $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
} catch(Exception $e) {
error_log($e);
header('Content-Type: text/html', true, $e->getCode());
die(buildTransformException($e, $bridge));
}
}
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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']);
}
}

View File

@ -1,26 +1,43 @@
<?php <?php
/**
* This file is part of RSS-Bridge, a PHP project capable of generating RSS and
* Atom feeds for websites that don't have one.
*
* For the full license information, please view the UNLICENSE file distributed
* with this source code.
*
* @package Core
* @license http://unlicense.org/ UNLICENSE
* @link https://github.com/rss-bridge/rss-bridge
*/
class ListAction implements ActionInterface class ListAction extends ActionAbstract {
{ public function execute() {
private BridgeFactory $bridgeFactory; $list = new StdClass();
$list->bridges = array();
public function __construct(
BridgeFactory $bridgeFactory
) {
$this->bridgeFactory = $bridgeFactory;
}
public function __invoke(Request $request): Response
{
$list = new \stdClass();
$list->bridges = [];
$list->total = 0; $list->total = 0;
foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) { $bridgeFac = new \BridgeFactory();
$bridge = $this->bridgeFactory->create($bridgeClassName); $bridgeFac->setWorkingDir(PATH_LIB_BRIDGES);
$list->bridges[$bridgeClassName] = [ foreach($bridgeFac->getBridgeNames() as $bridgeName) {
'status' => $this->bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
$bridge = $bridgeFac->create($bridgeName);
if($bridge === false) { // Broken bridge, show as inactive
$list->bridges[$bridgeName] = array(
'status' => 'inactive'
);
continue;
}
$status = $bridgeFac->isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
'uri' => $bridge->getURI(), 'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(), 'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(), 'name' => $bridge->getName(),
@ -28,9 +45,13 @@ class ListAction implements ActionInterface
'parameters' => $bridge->getParameters(), 'parameters' => $bridge->getParameters(),
'maintainer' => $bridge->getMaintainer(), 'maintainer' => $bridge->getMaintainer(),
'description' => $bridge->getDescription() 'description' => $bridge->getDescription()
]; );
} }
$list->total = count($list->bridges); $list->total = count($list->bridges);
return new Response(Json::encode($list), 200, ['content-type' => 'application/json']);
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
} }
} }

View File

@ -1,8 +1,8 @@
{ {
"service": "Heroku", "service": "Heroku",
"name": "rss-bridge-heroku", "name": "RSS-Bridge",
"description": "RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites which don't have one.", "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", "repository": "https://github.com/RSS-Bridge/rss-bridge",
"keywords": ["php", "rss-bridge", "rss"] "keywords": ["php", "rss-bridge", "rss"]
} }

View File

@ -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();

View File

@ -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();

View File

@ -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');

View File

@ -1,19 +1,17 @@
<?php <?php
class ABCNewsBridge extends BridgeAbstract {
class ABCNewsBridge extends BridgeAbstract
{
const NAME = 'ABC News Bridge'; const NAME = 'ABC News Bridge';
const URI = 'https://www.abc.net.au'; const URI = 'https://www.abc.net.au';
const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation'; const DESCRIPTION = 'Topics of the Australian Broadcasting Corporation';
const MAINTAINER = 'yue-dongchen'; const MAINTAINER = 'yue-dongchen';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'topic' => [ 'topic' => array(
'type' => 'list', 'type' => 'list',
'name' => 'Region', 'name' => 'Region',
'title' => 'Choose state', 'title' => 'Choose state',
'values' => [ 'values' => array(
'ACT' => 'act', 'ACT' => 'act',
'NSW' => 'nsw', 'NSW' => 'nsw',
'NT' => 'nt', 'NT' => 'nt',
@ -22,28 +20,26 @@ class ABCNewsBridge extends BridgeAbstract
'TAS' => 'tas', 'TAS' => 'tas',
'VIC' => 'vic', 'VIC' => 'vic',
'WA' => 'wa' 'WA' => 'wa'
], ),
] )
] )
]; );
public function collectData() public function collectData() {
{ $url = 'https://www.abc.net.au/news/' . $this->getInput('topic');
$url = sprintf('https://www.abc.net.au/news/%s', $this->getInput('topic')); $html = getSimpleHTMLDOM($url)->find('.YAJzu._2FvRw.ZWhbj._3BZxh', 0);
$dom = getSimpleHTMLDOM($url); $html = defaultLinkTo($html, $this->getURI());
$dom = $dom->find('div[data-component="PaginationList"]', 0);
if (!$dom) { foreach($html->find('._2H7Su') as $article) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url)); $item = array();
}
$dom = defaultLinkTo($dom, $this->getURI()); $title = $article->find('._3T9Id.fmhNa.nsZdE._2c2Zy._1tOey._3EOTW', 0);
foreach ($dom->find('article[data-component="DetailCard"]') as $article) { $item['title'] = $title->plaintext;
$a = $article->find('a', 0); $item['uri'] = $title->href;
$this->items[] = [ $item['content'] = $article->find('.rMkro._1cBaI._3PhF6._10YQT._1yL-m', 0)->plaintext;
'title' => $a->plaintext, $item['timestamp'] = strtotime($article->find('time', 0)->datetime);
'uri' => $a->href,
'content' => $article->find('p', 0)->plaintext, $this->items[] = $item;
'timestamp' => strtotime($article->find('time', 0)->datetime),
];
} }
} }
} }

View File

@ -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,
];
}
}
}

View File

@ -1,192 +1,78 @@
<?php <?php
class AO3Bridge extends BridgeAbstract class AO3Bridge extends BridgeAbstract {
{
const NAME = 'AO3'; const NAME = 'AO3';
const URI = 'https://archiveofourown.org/'; const URI = 'https://archiveofourown.org/';
const CACHE_TIMEOUT = 1800; const CACHE_TIMEOUT = 1800;
const DESCRIPTION = 'Returns works or chapters from Archive of Our Own'; const DESCRIPTION = 'Returns works or chapters from Archive of Our Own';
const MAINTAINER = 'Obsidienne'; const MAINTAINER = 'Obsidienne';
const PARAMETERS = [ const PARAMETERS = array(
'List' => [ 'List' => array(
'url' => [ 'url' => array(
'name' => 'url', 'name' => 'url',
'required' => true, 'required' => true,
// Example: F/F tag // Example: F/F tag, complete works only
'exampleValue' => 'https://archiveofourown.org/tags/F*s*F/works', 'exampleValue' => 'https://archiveofourown.org/works?work_search[complete]=T&tag_id=F*s*F',
], ),
'range' => [ ),
'name' => 'Chapter Content', 'Bookmarks' => array(
'title' => 'Chapter(s) to include in each work\'s feed entry', 'user' => array(
'defaultValue' => null,
'type' => 'list',
'values' => [
'None' => null,
'First' => 'first',
'Latest' => 'last',
'Entire work' => 'all',
],
],
'limit' => self::LIMIT,
],
'Bookmarks' => [
'user' => [
'name' => 'user', 'name' => 'user',
'required' => true, 'required' => true,
// Example: Nyaaru's bookmarks // Example: Nyaaru's bookmarks
'exampleValue' => 'Nyaaru', 'exampleValue' => 'Nyaaru',
], ),
], ),
'Work' => [ 'Work' => array(
'id' => [ 'id' => array(
'name' => 'id', 'name' => 'id',
'required' => true, 'required' => true,
// Example: latest chapters from A Better Past by LysSerris // Example: latest chapters from A Better Past by LysSerris
'exampleValue' => '18181853', 'exampleValue' => '18181853',
], ),
] )
]; );
private $title;
public function collectData() // Feed for lists of works (e.g. recent works, search results, filtered tags,
{ // bookmarks, series, collections).
switch ($this->queriedContext) { private function collectList($url) {
case 'Bookmarks': $html = getSimpleHTMLDOM($url);
$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); $html = defaultLinkTo($html, self::URI);
// Get list title. Will include page range + count in some cases foreach($html->find('.index.group > li') as $element) {
$heading = ($html->find('#main h2', 0)); $item = array();
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); $title = $element->find('div h4 a', 0);
if (!isset($title)) { if (!isset($title)) continue; // discard deleted works
continue; // discard deleted works
}
$item['title'] = $title->plaintext; $item['title'] = $title->plaintext;
$item['content'] = $element;
$item['uri'] = $title->href; $item['uri'] = $title->href;
$strdate = $element->find('div p.datetime', 0)->plaintext; $strdate = $element->find('div p.datetime', 0)->plaintext;
$item['timestamp'] = strtotime($strdate); $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); $chapters = $element->find('dl dd.chapters', 0);
// bookmarked series and external works do not have a chapters count // bookmarked series and external works do not have a chapters count
$chapters = (isset($chapters) ? $chapters->plaintext : 0); $chapters = (isset($chapters) ? $chapters->plaintext : 0);
$item['uid'] = $item['uri'] . "/$strdate/$chapters"; $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; $this->items[] = $item;
} }
} }
/** // Feed for recent chapters of a specific work.
* Feed for recent chapters of a specific work. private function collectWork($id) {
*/ $url = self::URI . "/works/$id/navigate";
private function collectWork($url) $html = getSimpleHTMLDOM($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); $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; $this->title = $html->find('h2 a', 0)->plaintext;
$nav = $html->find('ol.index.group > li'); foreach($html->find('ol.index.group > li') as $element) {
for ($i = 0; $i < count($nav); $i++) { $item = array();
$item = [];
$element = $nav[$i];
$item['title'] = $element->find('a', 0)->plaintext; $item['title'] = $element->find('a', 0)->plaintext;
$item['content'] = $workhtml->find('#chapter-' . ($i + 1), 0); $item['content'] = $element;
$item['uri'] = $element->find('a', 0)->href; $item['uri'] = $element->find('a', 0)->href;
$strdate = $element->find('span.datetime', 0)->plaintext; $strdate = $element->find('span.datetime', 0)->plaintext;
@ -202,37 +88,31 @@ class AO3Bridge extends BridgeAbstract
$this->items = array_reverse($this->items); $this->items = array_reverse($this->items);
} }
public function getName() public function collectData() {
{ switch($this->queriedContext) {
$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': case 'Bookmarks':
$user = $this->getInput('user'); $user = $this->getInput('user');
$this->title = $user;
$url = self::URI $url = self::URI
. '/users/' . $user . '/users/' . $user
. '/bookmarks?bookmark_search[sort_column]=bookmarkable_date'; . '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
break; return $this->collectList($url);
case 'List': case 'List': return $this->collectList(
$url = $this->getInput('url'); $this->getInput('url')
break; );
case 'Work': case 'Work': return $this->collectWork(
$url = self::URI . '/works/' . $this->getInput('id'); $this->getInput('id')
break; );
} }
return $url; }
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';
} }
} }

View File

@ -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();
}
}

View File

@ -1,7 +1,5 @@
<?php <?php
class ARDMediathekBridge extends BridgeAbstract {
class ARDMediathekBridge extends BridgeAbstract
{
const NAME = 'ARD-Mediathek Bridge'; const NAME = 'ARD-Mediathek Bridge';
const URI = 'https://www.ardmediathek.de'; const URI = 'https://www.ardmediathek.de';
const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path'; const DESCRIPTION = 'Feed of any series in the ARD-Mediathek, specified by its path';
@ -40,25 +38,19 @@ class ARDMediathekBridge extends BridgeAbstract
* @const IMAGEWIDTHPLACEHOLDER * @const IMAGEWIDTHPLACEHOLDER
*/ */
const IMAGEWIDTHPLACEHOLDER = '{width}'; const IMAGEWIDTHPLACEHOLDER = '{width}';
/**
* Title of the current show
* @var string
*/
private $title;
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'path' => [ 'path' => array(
'name' => 'Show Link or ID', 'name' => 'Show Link or ID',
'required' => true, 'required' => true,
'title' => 'Link to the show page or just its alphanumeric suffix', 'title' => 'Link to the show page or just its alphanumeric suffix',
'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/' 'defaultValue' => 'https://www.ardmediathek.de/sendung/45-min/Y3JpZDovL25kci5kZS8xMzkx/'
] )
] )
]; );
public function collectData() public function collectData() {
{
$oldTz = date_default_timezone_get(); $oldTz = date_default_timezone_get();
date_default_timezone_set('Europe/Berlin'); date_default_timezone_set('Europe/Berlin');
@ -77,20 +69,20 @@ class ARDMediathekBridge extends BridgeAbstract
} }
} }
$url = self::APIENDPOINT . $showID . '?pageSize=' . self::PAGESIZE; $url = SELF::APIENDPOINT . $showID . '/?pageSize=' . SELF::PAGESIZE;
$rawJSON = getContents($url); $rawJSON = getContents($url);
$processedJSON = json_decode($rawJSON); $processedJSON = json_decode($rawJSON);
foreach ($processedJSON->teasers as $video) { foreach($processedJSON->teasers as $video) {
$item = []; $item = array();
// there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId // there is also ->links->self->id, ->links->self->urlId, ->links->target->id, ->links->target->urlId
$item['uri'] = self::VIDEOLINKPREFIX . $video->id . '/'; $item['uri'] = SELF::VIDEOLINKPREFIX . $video->id . '/';
// there is also ->mediumTitle and ->shortTitle // there is also ->mediumTitle and ->shortTitle
$item['title'] = $video->longTitle; $item['title'] = $video->longTitle;
// in the test, aspect16x9 was the only child of images, not sure whether that is always true // in the test, aspect16x9 was the only child of images, not sure whether that is always true
$item['enclosures'] = [ $item['enclosures'] = array(
str_replace(self::IMAGEWIDTHPLACEHOLDER, self::IMAGEWIDTH, $video->images->aspect16x9->src) str_replace(SELF::IMAGEWIDTHPLACEHOLDER, SELF::IMAGEWIDTH, $video->images->aspect16x9->src)
]; );
$item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>'; $item['content'] = '<img src="' . $item['enclosures'][0] . '" /><p>';
$item['timestamp'] = $video->broadcastedOn; $item['timestamp'] = $video->broadcastedOn;
$item['uid'] = $video->id; $item['uid'] = $video->id;
@ -98,17 +90,6 @@ class ARDMediathekBridge extends BridgeAbstract
$this->items[] = $item; $this->items[] = $item;
} }
$this->title = $processedJSON->title;
date_default_timezone_set($oldTz); date_default_timezone_set($oldTz);
} }
/** {@inheritdoc} */
public function getName()
{
if (!empty($this->title)) {
return $this->title;
}
return parent::getName();
}
} }

View File

@ -1,23 +1,21 @@
<?php <?php
class ASRockNewsBridge extends BridgeAbstract {
class ASRockNewsBridge extends BridgeAbstract
{
const NAME = 'ASRock News Bridge'; const NAME = 'ASRock News Bridge';
const URI = 'https://www.asrock.com'; const URI = 'https://www.asrock.com';
const DESCRIPTION = 'Returns latest news articles'; const DESCRIPTION = 'Returns latest news articles';
const MAINTAINER = 'VerifiedJoseph'; const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = []; const PARAMETERS = array();
const CACHE_TIMEOUT = 3600; // 1 hour const CACHE_TIMEOUT = 3600; // 1 hour
public function collectData() public function collectData() {
{
$html = getSimpleHTMLDOM(self::URI . '/news/index.asp'); $html = getSimpleHTMLDOM(self::URI . '/news/index.asp');
$html = defaultLinkTo($html, self::URI . '/news/'); $html = defaultLinkTo($html, self::URI . '/news/');
foreach ($html->find('div.inner > a') as $index => $a) { foreach($html->find('div.inner > a') as $index => $a) {
$item = []; $item = array();
$articlePath = $a->href; $articlePath = $a->href;
@ -34,12 +32,7 @@ class ASRockNewsBridge extends BridgeAbstract
$item['content'] = $contents->innertext; $item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate($a->plaintext); $item['timestamp'] = $this->extractDate($a->plaintext);
$item['enclosures'][] = $a->find('img', 0)->src;
$img = $a->find('img', 0);
if ($img) {
$item['enclosures'][] = $img->src;
}
$this->items[] = $item; $this->items[] = $item;
if (count($this->items) >= 10) { if (count($this->items) >= 10) {
@ -48,8 +41,7 @@ class ASRockNewsBridge extends BridgeAbstract
} }
} }
private function extractDate($text) private function extractDate($text) {
{
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/'; $dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
$text = trim($text); $text = trim($text);

View File

@ -1,7 +1,6 @@
<?php <?php
class AcrimedBridge extends FeedExpander {
class AcrimedBridge extends FeedExpander
{
const MAINTAINER = 'qwertygc'; const MAINTAINER = 'qwertygc';
const NAME = 'Acrimed Bridge'; const NAME = 'Acrimed Bridge';
const URI = 'https://www.acrimed.org/'; const URI = 'https://www.acrimed.org/';
@ -18,16 +17,17 @@ class AcrimedBridge extends FeedExpander
] ]
]; ];
public function collectData() public function collectData(){
{ $this->collectExpandableDatas(
$url = 'https://www.acrimed.org/spip.php?page=backend'; static::URI . 'spip.php?page=backend',
$limit = $this->getInput('limit'); $this->getInput('limit')
$this->collectExpandableDatas($url, $limit); );
} }
protected function parseItem(array $item) protected function parseItem($newsItem){
{ $item = parent::parseItem($newsItem);
$articlePage = getSimpleHTMLDOM($item['uri']);
$articlePage = getSimpleHTMLDOM($newsItem->link);
$article = sanitize($articlePage->find('article.article1', 0)->innertext); $article = sanitize($articlePage->find('article.article1', 0)->innertext);
$article = defaultLinkTo($article, static::URI); $article = defaultLinkTo($article, static::URI);
$item['content'] = $article; $item['content'] = $article;

View File

@ -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),
];
}
}
}

View File

@ -1,17 +1,16 @@
<?php <?php
class AirBreizhBridge extends BridgeAbstract {
class AirBreizhBridge extends BridgeAbstract
{
const MAINTAINER = 'fanch317'; const MAINTAINER = 'fanch317';
const NAME = 'Air Breizh'; const NAME = 'Air Breizh';
const URI = 'https://www.airbreizh.asso.fr/'; const URI = 'https://www.airbreizh.asso.fr/';
const DESCRIPTION = 'Returns newests publications on Air Breizh'; const DESCRIPTION = 'Returns newests publications on Air Breizh';
const PARAMETERS = [ const PARAMETERS = array(
'Publications' => [ 'Publications' => array(
'theme' => [ 'theme' => array(
'name' => 'Thematique', 'name' => 'Thematique',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Tout' => '', 'Tout' => '',
'Rapport d\'activite' => 'rapport-dactivite', 'Rapport d\'activite' => 'rapport-dactivite',
'Etude' => 'etudes', 'Etude' => 'etudes',
@ -19,23 +18,22 @@ class AirBreizhBridge extends BridgeAbstract
'Autres documents' => 'autres-documents', 'Autres documents' => 'autres-documents',
'Plan Régional de Surveillance de la qualité de lair' => 'prsqa', 'Plan Régional de Surveillance de la qualité de lair' => 'prsqa',
'Transport' => 'transport' 'Transport' => 'transport'
] )
] )
] )
]; );
public function getIcon() public function getIcon() {
{
return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png'; return 'https://www.airbreizh.asso.fr/voy_content/uploads/2017/11/favicon.png';
} }
public function collectData() public function collectData(){
{
$html = ''; $html = '';
$html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme')); $html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'))
or returnClientError('No results for this query.');
foreach ($html->find('article') as $article) { foreach ($html->find('article') as $article) {
$item = []; $item = array();
// Title // Title
$item['title'] = $article->find('h2', 0)->plaintext; $item['title'] = $article->find('h2', 0)->plaintext;
// Author // Author

View File

@ -1,25 +1,24 @@
<?php <?php
class AlbionOnlineBridge extends BridgeAbstract {
class AlbionOnlineBridge extends BridgeAbstract
{
const NAME = 'Albion Online Changelog'; const NAME = 'Albion Online Changelog';
const MAINTAINER = 'otakuf'; const MAINTAINER = 'otakuf';
const URI = 'https://albiononline.com'; const URI = 'https://albiononline.com';
const DESCRIPTION = 'Returns the changes made to the Albion Online'; const DESCRIPTION = 'Returns the changes made to the Albion Online';
const CACHE_TIMEOUT = 3600; // 60min const CACHE_TIMEOUT = 3600; // 60min
const PARAMETERS = [ [ const PARAMETERS = array( array(
'postcount' => [ 'postcount' => array(
'name' => 'Limit', 'name' => 'Limit',
'type' => 'number', 'type' => 'number',
'required' => true, 'required' => true,
'title' => 'Maximum number of items to return', 'title' => 'Maximum number of items to return',
'defaultValue' => 5, 'defaultValue' => 5,
], ),
'language' => [ 'language' => array(
'name' => 'Language', 'name' => 'Language',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'English' => 'en', 'English' => 'en',
'Deutsch' => 'de', 'Deutsch' => 'de',
'Polski' => 'pl', 'Polski' => 'pl',
@ -27,20 +26,19 @@ class AlbionOnlineBridge extends BridgeAbstract
'Русский' => 'ru', 'Русский' => 'ru',
'Português' => 'pt', 'Português' => 'pt',
'Español' => 'es', 'Español' => 'es',
], ),
'title' => 'Language of changelog posts', 'title' => 'Language of changelog posts',
'defaultValue' => 'en', 'defaultValue' => 'en',
], ),
'full' => [ 'full' => array(
'name' => 'Full changelog', 'name' => 'Full changelog',
'type' => 'checkbox', 'type' => 'checkbox',
'required' => false, 'required' => false,
'title' => 'Enable to receive the full changelog post for each item' 'title' => 'Enable to receive the full changelog post for each item'
], ),
]]; ));
public function collectData() public function collectData() {
{
$api = 'https://albiononline.com/'; $api = 'https://albiononline.com/';
// Example: https://albiononline.com/en/changelog/1/5 // Example: https://albiononline.com/en/changelog/1/5
$url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount'); $url = $api . $this->getInput('language') . '/changelog/1/' . $this->getInput('postcount');
@ -48,14 +46,14 @@ class AlbionOnlineBridge extends BridgeAbstract
$html = getSimpleHTMLDOM($url); $html = getSimpleHTMLDOM($url);
foreach ($html->find('li') as $data) { foreach ($html->find('li') as $data) {
$item = []; $item = array();
$item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href'); $item['uri'] = self::URI . $data->find('a', 0)->getAttribute('href');
$item['title'] = trim(explode('|', $data->find('span', 0)->plaintext)[0]); $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 // 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') ); //print_r( date_parse_from_format( 'M j, Y' , 'Sep 9, 2020') );
//$item['timestamp'] = $this->extractDate($a->plaintext); //$item['timestamp'] = $this->extractDate($a->plaintext);
$item['author'] = 'albiononline.com'; $item['author'] = 'albiononline.com';
if ($this->getInput('full')) { if($this->getInput('full')) {
$item['content'] = $this->getFullChangelog($item['uri']); $item['content'] = $this->getFullChangelog($item['uri']);
} else { } else {
//$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext)); //$item['content'] = trim(preg_replace('/\s+/', ' ', $data->find('span', 0)->plaintext));
@ -67,8 +65,7 @@ class AlbionOnlineBridge extends BridgeAbstract
} }
} }
private function getFullChangelog($url) private function getFullChangelog($url) {
{
$html = getSimpleHTMLDOMCached($url); $html = getSimpleHTMLDOMCached($url);
$html = defaultLinkTo($html, self::URI); $html = defaultLinkTo($html, self::URI);
return $html->find('div.small-12.columns', 1)->innertext; return $html->find('div.small-12.columns', 1)->innertext;

View File

@ -1,48 +1,46 @@
<?php <?php
class AlfaBankByBridge extends BridgeAbstract {
class AlfaBankByBridge extends BridgeAbstract
{
const MAINTAINER = 'lassana'; const MAINTAINER = 'lassana';
const NAME = 'AlfaBank.by Новости'; const NAME = 'AlfaBank.by Новости';
const URI = 'https://www.alfabank.by'; const URI = 'https://www.alfabank.by';
const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка'; const DESCRIPTION = 'Уведомления Alfa-Now — новости от Альфа-Банка';
const CACHE_TIMEOUT = 3600; // 1 hour const CACHE_TIMEOUT = 3600; // 1 hour
const PARAMETERS = [ const PARAMETERS = array(
'News' => [ 'News' => array(
'business' => [ 'business' => array(
'name' => 'Альфа Бизнес', 'name' => 'Альфа Бизнес',
'type' => 'list', 'type' => 'list',
'title' => 'В зависимости от выбора, возращает уведомления для" . 'title' => 'В зависимости от выбора, возращает уведомления для" .
" клиентов физ. лиц либо для клиентов-юридических лиц и ИП', " клиентов физ. лиц либо для клиентов-юридических лиц и ИП',
'values' => [ 'values' => array(
'Новости' => 'news', 'Новости' => 'news',
'Новости бизнеса' => 'newsBusiness' 'Новости бизнеса' => 'newsBusiness'
], ),
'defaultValue' => 'news' 'defaultValue' => 'news'
], ),
'fullContent' => [ 'fullContent' => array(
'name' => 'Включать содержимое', 'name' => 'Включать содержимое',
'type' => 'checkbox', 'type' => 'checkbox',
'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)' 'title' => 'Если выбрано, содержимое уведомлений вставляется в поток (работает медленно)'
] )
] )
]; );
public function collectData() public function collectData() {
{
$business = $this->getInput('business') == 'newsBusiness'; $business = $this->getInput('business') == 'newsBusiness';
$fullContent = $this->getInput('fullContent') == 'on'; $fullContent = $this->getInput('fullContent') == 'on';
$mainPageUrl = self::URI . '/about/articles/uvedomleniya/'; $mainPageUrl = self::URI . '/about/articles/uvedomleniya/';
if ($business) { if($business) {
$mainPageUrl .= '?business=true'; $mainPageUrl .= '?business=true';
} }
$html = getSimpleHTMLDOM($mainPageUrl); $html = getSimpleHTMLDOM($mainPageUrl);
$limit = 0; $limit = 0;
foreach ($html->find('a.notifications__item') as $element) { foreach($html->find('a.notifications__item') as $element) {
if ($limit < 10) { if($limit < 10) {
$item = []; $item = array();
$item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id')); $item['uid'] = 'urn:sha1:' . hash('sha1', $element->getAttribute('data-notification-id'));
$item['title'] = $element->find('div.item-title', 0)->innertext; $item['title'] = $element->find('div.item-title', 0)->innertext;
$item['timestamp'] = DateTime::createFromFormat( $item['timestamp'] = DateTime::createFromFormat(
@ -51,14 +49,14 @@ class AlfaBankByBridge extends BridgeAbstract
)->getTimestamp(); )->getTimestamp();
$itemUrl = self::URI . $element->href; $itemUrl = self::URI . $element->href;
if ($business) { if($business) {
$itemUrl = str_replace('?business=true', '', $itemUrl); $itemUrl = str_replace('?business=true', '', $itemUrl);
} }
$item['uri'] = $itemUrl; $item['uri'] = $itemUrl;
if ($fullContent) { if($fullContent) {
$itemHtml = getSimpleHTMLDOM($itemUrl); $itemHtml = getSimpleHTMLDOM($itemUrl);
if ($itemHtml) { if($itemHtml) {
$item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext; $item['content'] = $itemHtml->find('div.now-p__content-text', 0)->innertext;
} }
} }
@ -69,19 +67,17 @@ class AlfaBankByBridge extends BridgeAbstract
} }
} }
public function getIcon() public function getIcon() {
{
return static::URI . '/local/images/favicon.ico'; return static::URI . '/local/images/favicon.ico';
} }
private function ruMonthsToEn($date) private function ruMonthsToEn($date) {
{ $ruMonths = array(
$ruMonths = [
'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня', 'Января', 'Февраля', 'Марта', 'Апреля', 'Мая', 'Июня',
'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' ]; 'Июля', 'Августа', 'Сентября', 'Октября', 'Ноября', 'Декабря' );
$enMonths = [ $enMonths = array(
'January', 'February', 'March', 'April', 'May', 'June', 'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December' ]; 'July', 'August', 'September', 'October', 'November', 'December' );
return str_replace($ruMonths, $enMonths, $date); return str_replace($ruMonths, $enMonths, $date);
} }
} }

View File

@ -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;
}
}

View File

@ -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>', '&emsp;</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;
}
}
}

View File

@ -1,18 +1,17 @@
<?php <?php
class AllocineFRBridge extends BridgeAbstract {
class AllocineFRBridge extends BridgeAbstract
{
const MAINTAINER = 'superbaillot.net'; const MAINTAINER = 'superbaillot.net';
const NAME = 'Allo Cine Bridge'; const NAME = 'Allo Cine Bridge';
const CACHE_TIMEOUT = 25200; // 7h const CACHE_TIMEOUT = 25200; // 7h
const URI = 'https://www.allocine.fr'; const URI = 'https://www.allocine.fr/';
const DESCRIPTION = 'Bridge for allocine.fr'; const DESCRIPTION = 'Bridge for allocine.fr';
const PARAMETERS = [ [ const PARAMETERS = array( array(
'category' => [ 'category' => array(
'name' => 'Emission', 'name' => 'Emission',
'type' => 'list', 'type' => 'list',
'title' => 'Sélectionner l\'emission', 'title' => 'Sélectionner l\'emission',
'values' => [ 'values' => array(
'Faux Raccord' => 'faux-raccord', 'Faux Raccord' => 'faux-raccord',
'Fanzone' => 'fanzone', 'Fanzone' => 'fanzone',
'Game In Ciné' => 'game-in-cine', 'Game In Ciné' => 'game-in-cine',
@ -28,34 +27,34 @@ class AllocineFRBridge extends BridgeAbstract
'Complètement...' => 'completement', 'Complètement...' => 'completement',
'#Fun Facts' => 'fun-facts', '#Fun Facts' => 'fun-facts',
'Origin Story' => 'origin-story', 'Origin Story' => 'origin-story',
] )
] )
]]; ));
public function getURI() public function getURI(){
{ if(!is_null($this->getInput('category'))) {
if (!is_null($this->getInput('category'))) {
$categories = [ $categories = array(
'faux-raccord' => '/video/programme-12284/', 'faux-raccord' => 'video/programme-12284/saison-37054/',
'fanzone' => '/video/programme-12298/', 'fanzone' => 'video/programme-12298/saison-37059/',
'game-in-cine' => '/video/programme-12288/', 'game-in-cine' => 'video/programme-12288/saison-22971/',
'pour-la-faire-courte' => '/video/programme-20960/', 'pour-la-faire-courte' => 'video/programme-20960/saison-29678/',
'home-cinema' => '/video/programme-12287/', 'home-cinema' => 'video/programme-12287/saison-34703/',
'pils-par-ici-les-sorties' => '/video/programme-25789/', 'pils-par-ici-les-sorties' => 'video/programme-25789/saison-37253/',
'allocine-lemission-sur-lestream' => '/video/programme-25123/', 'allocine-lemission-sur-lestream' => 'video/programme-25123/saison-36067/',
'give-me-five' => '/video/programme-21919/saison-34518/', 'give-me-five' => 'video/programme-21919/saison-34518/',
'aviez-vous-remarque' => '/video/programme-19518/', 'aviez-vous-remarque' => 'video/programme-19518/saison-37084/',
'et-paf-il-est-mort' => '/video/programme-25113/', 'et-paf-il-est-mort' => 'video/programme-25113/saison-36657/',
'the-big-fan-theory' => '/video/programme-20403/', 'the-big-fan-theory' => 'video/programme-20403/saison-37419/',
'cliches' => '/video/programme-24834/', 'cliches' => 'video/programme-24834/saison-35591/',
'completement' => '/video/programme-23859/', 'completement' => 'video/programme-23859/saison-34102/',
'fun-facts' => '/video/programme-23040/', 'fun-facts' => 'video/programme-23040/saison-32686/',
'origin-story' => '/video/programme-25667/' 'origin-story' => 'video/programme-25667/saison-37041/'
]; );
$category = $this->getInput('category'); $category = $this->getInput('category');
if (array_key_exists($category, $categories)) { if(array_key_exists($category, $categories)) {
return static::URI . $this->getLastSeasonURI($categories[$category]); return static::URI . $categories[$category];
} else { } else {
returnClientError('Emission inconnue'); returnClientError('Emission inconnue');
} }
@ -64,32 +63,32 @@ class AllocineFRBridge extends BridgeAbstract
return parent::getURI(); return parent::getURI();
} }
private function getLastSeasonURI($category) public function getName(){
{ if(!is_null($this->getInput('category'))) {
$html = getSimpleHTMLDOMCached(static::URI . $category, 86400); return self::NAME . ' : '
$seasonLink = $html->find('section[class=section-wrap section]', 0)->find('div[class=cf]', 0)->find('a', 0); . array_search(
$URI = $seasonLink->href; $this->getInput('category'),
return $URI; self::PARAMETERS[$this->queriedContext]['category']['values']
} );
public function getName()
{
if (!is_null($this->getInput('category'))) {
return self::NAME . ' : ' . $this->getKey('category');
} }
return parent::getName(); return parent::getName();
} }
public function collectData() public function collectData(){
{
$html = getSimpleHTMLDOM($this->getURI()); $html = getSimpleHTMLDOM($this->getURI());
foreach ($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) { $category = array_search(
$item = []; $this->getInput('category'),
self::PARAMETERS[$this->queriedContext]['category']['values']
);
foreach($html->find('div[class=gd-col-left]', 0)->find('div[class*=video-card]') as $element) {
$item = array();
$title = $element->find('a[class*=meta-title-link]', 0); $title = $element->find('a[class*=meta-title-link]', 0);
$content = trim(defaultLinkTo($element->outertext, static::URI)); $content = trim($element->outertext);
// Replace image 'src' with the one in 'data-src' // 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('@src="data:image/gif;base64,[A-Za-z0-9+\/]*"@', '', $content);
@ -100,7 +99,7 @@ class AllocineFRBridge extends BridgeAbstract
$item['content'] = $content; $item['content'] = $content;
$item['title'] = trim($title->innertext); $item['title'] = trim($title->innertext);
$item['uri'] = static::URI . '/' . substr($title->href, 1); $item['uri'] = static::URI . substr($title->href, 1);
$this->items[] = $item; $this->items[] = $item;
} }
} }

View File

@ -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'
]
)
);
}
}

View File

@ -1,35 +1,35 @@
<?php <?php
class AmazonBridge extends BridgeAbstract class AmazonBridge extends BridgeAbstract {
{
const MAINTAINER = 'Alexis CHEMEL'; const MAINTAINER = 'Alexis CHEMEL';
const NAME = 'Amazon'; const NAME = 'Amazon';
const URI = 'https://www.amazon.com/'; const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns products from Amazon search'; const DESCRIPTION = 'Returns products from Amazon search';
const PARAMETERS = [[ const PARAMETERS = array(array(
'q' => [ 'q' => array(
'name' => 'Keyword', 'name' => 'Keyword',
'required' => true, 'required' => true,
'exampleValue' => 'watch', 'exampleValue' => 'watch',
], ),
'sort' => [ 'sort' => array(
'name' => 'Sort by', 'name' => 'Sort by',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Relevance' => 'relevanceblender', 'Relevance' => 'relevanceblender',
'Price: Low to High' => 'price-asc-rank', 'Price: Low to High' => 'price-asc-rank',
'Price: High to Low' => 'price-desc-rank', 'Price: High to Low' => 'price-desc-rank',
'Average Customer Review' => 'review-rank', 'Average Customer Review' => 'review-rank',
'Newest Arrivals' => 'date-desc-rank', 'Newest Arrivals' => 'date-desc-rank',
], ),
'defaultValue' => 'relevanceblender', 'defaultValue' => 'relevanceblender',
], ),
'tld' => [ 'tld' => array(
'name' => 'Country', 'name' => 'Country',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Australia' => 'com.au', 'Australia' => 'com.au',
'Brazil' => 'com.br', 'Brazil' => 'com.br',
'Canada' => 'ca', 'Canada' => 'ca',
@ -41,65 +41,55 @@ class AmazonBridge extends BridgeAbstract
'Japan' => 'co.jp', 'Japan' => 'co.jp',
'Mexico' => 'com.mx', 'Mexico' => 'com.mx',
'Netherlands' => 'nl', 'Netherlands' => 'nl',
'Poland' => 'pl',
'Spain' => 'es', 'Spain' => 'es',
'Sweden' => 'se',
'Turkey' => 'com.tr',
'United Kingdom' => 'co.uk', 'United Kingdom' => 'co.uk',
'United States' => 'com', 'United States' => 'com',
], ),
'defaultValue' => 'com', 'defaultValue' => 'com',
], ),
]]; ));
public function collectData() public function getName(){
{ if(!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
$baseUrl = sprintf('https://www.amazon.%s', $this->getInput('tld'));
$url = sprintf(
'%s/s/?field-keywords=%s&sort=%s',
$baseUrl,
urlencode($this->getInput('q')),
$this->getInput('sort')
);
$dom = getSimpleHTMLDOM($url);
$elements = $dom->find('div.s-result-item');
foreach ($elements as $element) {
$item = [];
$title = $element->find('h2', 0);
if (!$title) {
continue;
}
$item['title'] = $title->innertext;
$itemUrl = $element->find('a', 0)->href;
$item['uri'] = urljoin($baseUrl, $itemUrl);
$image = $element->find('img', 0);
if ($image) {
$item['content'] = '<img src="' . $image->getAttribute('src') . '" /><br />';
}
$price = $element->find('span.a-price > .a-offscreen', 0);
if ($price) {
$item['content'] .= $price->innertext;
}
$this->items[] = $item;
}
}
public function getName()
{
if (!is_null($this->getInput('tld')) && !is_null($this->getInput('q'))) {
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q'); return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('q');
} }
return parent::getName(); return parent::getName();
} }
public function collectData() {
$uri = 'https://www.amazon.' . $this->getInput('tld') . '/';
$uri .= 's/?field-keywords=' . urlencode($this->getInput('q')) . '&sort=' . $this->getInput('sort');
$html = getSimpleHTMLDOM($uri);
foreach($html->find('li.s-result-item') as $element) {
$item = array();
// Title
$title = $element->find('h2', 0);
if (is_null($title)) {
continue;
}
$item['title'] = html_entity_decode($title->innertext, ENT_QUOTES);
// Url
$uri = $title->parent()->getAttribute('href');
$uri = substr($uri, 0, strrpos($uri, '/'));
$item['uri'] = substr($uri, 0, strrpos($uri, '/'));
// 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;
}
}
} }

View File

@ -1,26 +1,25 @@
<?php <?php
class AmazonPriceTrackerBridge extends BridgeAbstract class AmazonPriceTrackerBridge extends BridgeAbstract {
{
const MAINTAINER = 'captn3m0, sal0max'; const MAINTAINER = 'captn3m0, sal0max';
const NAME = 'Amazon Price Tracker'; const NAME = 'Amazon Price Tracker';
const URI = 'https://www.amazon.com/'; const URI = 'https://www.amazon.com/';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Tracks price for a single product on Amazon'; const DESCRIPTION = 'Tracks price for a single product on Amazon';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'asin' => [ 'asin' => array(
'name' => 'ASIN', 'name' => 'ASIN',
'required' => true, 'required' => true,
'exampleValue' => 'B071GB1VMQ', 'exampleValue' => 'B071GB1VMQ',
// https://stackoverflow.com/a/12827734 // https://stackoverflow.com/a/12827734
'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)', 'pattern' => 'B[\dA-Z]{9}|\d{9}(X|\d)',
], ),
'tld' => [ 'tld' => array(
'name' => 'Country', 'name' => 'Country',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Australia' => 'com.au', 'Australia' => 'com.au',
'Brazil' => 'com.br', 'Brazil' => 'com.br',
'Canada' => 'ca', 'Canada' => 'ca',
@ -32,25 +31,23 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
'Japan' => 'co.jp', 'Japan' => 'co.jp',
'Mexico' => 'com.mx', 'Mexico' => 'com.mx',
'Netherlands' => 'nl', 'Netherlands' => 'nl',
'Poland' => 'pl',
'Spain' => 'es', 'Spain' => 'es',
'Sweden' => 'se', 'Sweden' => 'se',
'Turkey' => 'com.tr',
'United Kingdom' => 'co.uk', 'United Kingdom' => 'co.uk',
'United States' => 'com', 'United States' => 'com',
], ),
'defaultValue' => 'com', 'defaultValue' => 'com',
], ),
]]; ));
const PRICE_SELECTORS = [ const PRICE_SELECTORS = array(
'#priceblock_ourprice', '#priceblock_ourprice',
'.priceBlockBuyingPriceString', '.priceBlockBuyingPriceString',
'#newBuyBoxPrice', '#newBuyBoxPrice',
'#tp_price_block_total_price_ww', '#tp_price_block_total_price_ww',
'span.offer-price', 'span.offer-price',
'.a-color-price', '.a-color-price',
]; );
const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0"; const WHITESPACE = " \t\n\r\0\x0B\xC2\xA0";
@ -59,16 +56,14 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
/** /**
* Generates domain name given a amazon TLD * Generates domain name given a amazon TLD
*/ */
private function getDomainName() private function getDomainName() {
{
return 'https://www.amazon.' . $this->getInput('tld'); return 'https://www.amazon.' . $this->getInput('tld');
} }
/** /**
* Generates URI for a Amazon product page * Generates URI for a Amazon product page
*/ */
public function getURI() public function getURI() {
{
if (!is_null($this->getInput('asin'))) { if (!is_null($this->getInput('asin'))) {
return $this->getDomainName() . '/dp/' . $this->getInput('asin'); return $this->getDomainName() . '/dp/' . $this->getInput('asin');
} }
@ -79,8 +74,7 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
* Scrapes the product title from the html page * Scrapes the product title from the html page
* returns the default title if scraping fails * returns the default title if scraping fails
*/ */
private function getTitle($html) private function getTitle($html) {
{
$titleTag = $html->find('#productTitle', 0); $titleTag = $html->find('#productTitle', 0);
if (!$titleTag) { if (!$titleTag) {
@ -93,8 +87,7 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
/** /**
* Title used by the feed if none could be found * Title used by the feed if none could be found
*/ */
private function getDefaultTitle() private function getDefaultTitle() {
{
return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin'); return 'Amazon.' . $this->getInput('tld') . ': ' . $this->getInput('asin');
} }
@ -102,8 +95,7 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
* Returns name for the feed * Returns name for the feed
* Uses title (already scraped) if it has one * Uses title (already scraped) if it has one
*/ */
public function getName() public function getName() {
{
if (isset($this->title)) { if (isset($this->title)) {
return $this->title; return $this->title;
} else { } else {
@ -111,8 +103,7 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
} }
} }
private function parseDynamicImage($attribute) private function parseDynamicImage($attribute) {
{
$json = json_decode(html_entity_decode($attribute), true); $json = json_decode(html_entity_decode($attribute), true);
if ($json and count($json) > 0) { if ($json and count($json) > 0) {
@ -123,15 +114,15 @@ class AmazonPriceTrackerBridge extends BridgeAbstract
/** /**
* Returns a generated image tag for the product * Returns a generated image tag for the product
*/ */
private function getImage($html) private function getImage($html) {
{
$image = 'https://placekitten.com/200/300';
$imageSrc = $html->find('#main-image-container img', 0); $imageSrc = $html->find('#main-image-container img', 0);
if ($imageSrc) { if ($imageSrc) {
$hiresImage = $imageSrc->getAttribute('data-old-hires'); $hiresImage = $imageSrc->getAttribute('data-old-hires');
$dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image'); $dynamicImageAttribute = $imageSrc->getAttribute('data-a-dynamic-image');
$image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute); $image = $hiresImage ?: $this->parseDynamicImage($dynamicImageAttribute);
} }
$image = $image ?: 'https://placekitten.com/200/300';
return <<<EOT return <<<EOT
<img width="300" style="max-width:300;max-height:300" src="$image" alt="{$this->title}" /> <img width="300" style="max-width:300;max-height:300" src="$image" alt="{$this->title}" />
@ -142,59 +133,49 @@ EOT;
* Return \simple_html_dom object * Return \simple_html_dom object
* for the entire html of the product page * for the entire html of the product page
*/ */
private function getHtml() private function getHtml() {
{
$uri = $this->getURI(); $uri = $this->getURI();
return getSimpleHTMLDOM($uri); return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
} }
private function scrapePriceFromMetrics($html) private function scrapePriceFromMetrics($html) {
{
$asinData = $html->find('#cerberus-data-metrics', 0); $asinData = $html->find('#cerberus-data-metrics', 0);
// <div id="cerberus-data-metrics" style="display: none;" // <div id="cerberus-data-metrics" style="display: none;"
// data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0" // data-asin="B00WTHJ5SU" data-asin-price="14.99" data-asin-shipping="0"
// data-asin-currency-code="USD" data-substitute-count="-1" ... /> // data-asin-currency-code="USD" data-substitute-count="-1" ... />
if ($asinData) { if ($asinData) {
return [ return array(
'price' => $asinData->getAttribute('data-asin-price'), 'price' => $asinData->getAttribute('data-asin-price'),
'currency' => $asinData->getAttribute('data-asin-currency-code'), 'currency' => $asinData->getAttribute('data-asin-currency-code'),
'shipping' => $asinData->getAttribute('data-asin-shipping') 'shipping' => $asinData->getAttribute('data-asin-shipping')
]; );
} }
return false; return false;
} }
private function scrapePriceTwister($html) private function scrapePriceTwister($html) {
{
$str = $html->find('.twister-plus-buying-options-price-data', 0); $str = $html->find('.twister-plus-buying-options-price-data', 0);
$data = json_decode($str->innertext, true); $data = json_decode($str->innertext, true);
if (count($data) === 1) { if(count($data) === 1) {
$data = $data[0]; $data = $data[0];
return [ return array(
'displayPrice' => $data['displayPrice'], 'displayPrice' => $data['displayPrice'],
'currency' => $data['currency'], 'currency' => $data['currency'],
'shipping' => '0', 'shipping' => '0',
]; );
} }
return false; return false;
} }
private function scrapePriceGeneric($html) private function scrapePriceGeneric($html) {
{
$default = [
'price' => null,
'displayPrice' => null,
'currency' => null,
'shipping' => null,
];
$priceDiv = null; $priceDiv = null;
foreach (self::PRICE_SELECTORS as $sel) { foreach(self::PRICE_SELECTORS as $sel) {
$priceDiv = $html->find($sel, 0); $priceDiv = $html->find($sel, 0);
if ($priceDiv) { if ($priceDiv) {
break; break;
@ -202,51 +183,59 @@ EOT;
} }
if (!$priceDiv) { if (!$priceDiv) {
return $default; return false;
} }
$priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext); $priceString = str_replace(str_split(self::WHITESPACE), '', $priceDiv->plaintext);
preg_match('/(\d+\.\d{0,2})/', $priceString, $matches); preg_match('/(\d+\.\d{0,2})/', $priceString, $matches);
$price = $matches[0] ?? null; $price = $matches[0];
$currency = str_replace($price, '', $priceString); $currency = str_replace($price, '', $priceString);
if ($price != null && $currency != null) { if ($price != null && $currency != null) {
return [ return array(
'price' => $price, 'price' => $price,
'displayPrice' => null,
'currency' => $currency, 'currency' => $currency,
'shipping' => '0' 'shipping' => '0'
]; );
}
return $default;
} }
public function collectData() return false;
{ }
$html = $this->getHtml();
$this->title = $this->getTitle($html);
$image = $this->getImage($html);
$data = $this->scrapePriceGeneric($html);
// render private function renderContent($image, $data) {
$content = '';
$price = $data['displayPrice']; $price = $data['displayPrice'];
if (!$price) { if (!$price) {
$price = sprintf('%s %s', $data['price'], $data['currency']); $price = "{$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 = [ $html = "$image<br>Price: $price";
if ($data['shipping'] !== '0') {
$html .= "<br>Shipping: {$data['shipping']} {$data['currency']}</br>";
}
return $html;
}
/**
* Scrape method for Amazon product page
* @return [type] [description]
*/
public function collectData() {
$html = $this->getHtml();
$this->title = $this->getTitle($html);
$imageTag = $this->getImage($html);
$data = $this->scrapePriceGeneric($html);
$item = array(
'title' => $this->title, 'title' => $this->title,
'uri' => $this->getURI(), 'uri' => $this->getURI(),
'content' => $content, 'content' => $this->renderContent($imageTag, $data),
// This is to ensure that feed readers notice the price change // This is to ensure that feed readers notice the price change
'uid' => md5($data['price']) 'uid' => md5($data['price'])
]; );
$this->items[] = $item; $this->items[] = $item;
} }

View File

@ -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,
];
}
}
}

View File

@ -1,19 +1,18 @@
<?php <?php
class AnidexBridge extends BridgeAbstract {
class AnidexBridge extends BridgeAbstract
{
const MAINTAINER = 'ORelio'; const MAINTAINER = 'ORelio';
const NAME = 'Anidex'; const NAME = 'Anidex';
const URI = 'http://anidex.info/'; // anidex.info has ddos-guard so we need to use anidex.moe 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_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 ALTERNATE_HOST = 'anidex.info'; // Correct host for requesting anidex.moe without 301 redirect
const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.'; const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'id' => [ 'id' => array(
'name' => 'Category', 'name' => 'Category',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'All categories' => '0', 'All categories' => '0',
'Anime' => '1,2,3', 'Anime' => '1,2,3',
'Anime - Sub' => '1', 'Anime - Sub' => '1',
@ -35,12 +34,12 @@ class AnidexBridge extends BridgeAbstract
'Pictures' => '14', 'Pictures' => '14',
'Adult Video' => '15', 'Adult Video' => '15',
'Other' => '16' 'Other' => '16'
] )
], ),
'lang_id' => [ 'lang_id' => array(
'name' => 'Language', 'name' => 'Language',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'All languages' => '0', 'All languages' => '0',
'English' => '1', 'English' => '1',
'Japanese' => '2', 'Japanese' => '2',
@ -73,52 +72,52 @@ class AnidexBridge extends BridgeAbstract
'Spanish (LATAM)' => '29', 'Spanish (LATAM)' => '29',
'Persian' => '30', 'Persian' => '30',
'Malaysian' => '31' 'Malaysian' => '31'
] )
], ),
'group_id' => [ 'group_id' => array(
'name' => 'Group ID', 'name' => 'Group ID',
'type' => 'number' 'type' => 'number'
], ),
'r' => [ 'r' => array(
'name' => 'Hide Remakes', 'name' => 'Hide Remakes',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'b' => [ 'b' => array(
'name' => 'Only Batches', 'name' => 'Only Batches',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'a' => [ 'a' => array(
'name' => 'Only Authorized', 'name' => 'Only Authorized',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'q' => [ 'q' => array(
'name' => 'Keyword', 'name' => 'Keyword',
'description' => 'Keyword(s)', 'description' => 'Keyword(s)',
'type' => 'text' 'type' => 'text'
], ),
'h' => [ 'h' => array(
'name' => 'Adult content', 'name' => 'Adult content',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'No filter' => '0', 'No filter' => '0',
'Hide +18' => '1', 'Hide +18' => '1',
'Only +18' => '2' 'Only +18' => '2'
] )
] )
] )
]; );
public function collectData() {
public function collectData()
{
// Build Search URL from user-provided parameters // Build Search URL from user-provided parameters
$search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc'; $search_url = self::ALTERNATE_URI . '?s=upload_timestamp&o=desc';
foreach (['id', 'lang_id', 'group_id'] as $param_name) { foreach (array('id', 'lang_id', 'group_id') as $param_name) {
$param = $this->getInput($param_name); $param = $this->getInput($param_name);
if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) { if (!empty($param) && intval($param) != 0 && ctype_digit(str_replace(',', '', $param))) {
$search_url .= '&' . $param_name . '=' . $param; $search_url .= '&' . $param_name . '=' . $param;
} }
} }
foreach (['r', 'b', 'a'] as $param_name) { foreach (array('r', 'b', 'a') as $param_name) {
$param = $this->getInput($param_name); $param = $this->getInput($param_name);
if (!empty($param) && boolval($param)) { if (!empty($param) && boolval($param)) {
$search_url .= '&' . $param_name . '=1'; $search_url .= '&' . $param_name . '=1';
@ -128,14 +127,14 @@ class AnidexBridge extends BridgeAbstract
if (!empty($query)) { if (!empty($query)) {
$search_url .= '&q=' . urlencode($query); $search_url .= '&q=' . urlencode($query);
} }
$opt = []; $opt = array();
$h = $this->getInput('h'); $h = $this->getInput('h');
if (!empty($h) && intval($h) != 0 && ctype_digit($h)) { if (!empty($h) && intval($h) != 0 && ctype_digit($h)) {
$opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h; $opt[CURLOPT_COOKIE] = 'anidex_h_toggle=' . $h;
} }
// We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI // We need to use a different Host HTTP header to reach the correct page on ALTERNATE_URI
$headers = ['Host: ' . self::ALTERNATE_HOST]; $headers = array('Host: ' . self::ALTERNATE_HOST);
// The HTTPS certificate presented by anidex.moe is for anidex.info. We need to ignore this. // 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:// // As a consequence, the bridge is intentionally marked as insecure by setting self::URI to http://
@ -145,20 +144,18 @@ class AnidexBridge extends BridgeAbstract
// Retrieve torrent listing from search results, which does not contain torrent description // Retrieve torrent listing from search results, which does not contain torrent description
$html = getSimpleHTMLDOM($search_url, $headers, $opt); $html = getSimpleHTMLDOM($search_url, $headers, $opt);
$links = $html->find('a'); $links = $html->find('a');
$results = []; $results = array();
foreach ($links as $link) { foreach ($links as $link)
if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results)) { if (strpos($link->href, '/torrent/') === 0 && !in_array($link->href, $results))
$results[] = $link->href; $results[] = $link->href;
} if (empty($results) && empty($this->getInput('q')))
}
if (empty($results) && empty($this->getInput('q'))) {
returnServerError('No results from Anidex: ' . $search_url); returnServerError('No results from Anidex: ' . $search_url);
}
//Process each item individually //Process each item individually
foreach ($results as $element) { foreach ($results as $element) {
//Limit total amount of requests //Limit total amount of requests
if (count($this->items) >= 20) { if(count($this->items) >= 20) {
break; break;
} }
@ -166,12 +163,14 @@ class AnidexBridge extends BridgeAbstract
//Ignore entries without valid torrent ID //Ignore entries without valid torrent ID
if ($torrent_id != 0 && ctype_digit($torrent_id)) { if ($torrent_id != 0 && ctype_digit($torrent_id)) {
//Retrieve data for this torrent ID //Retrieve data for this torrent ID
$item_browse_uri = self::URI . 'torrent/' . $torrent_id; $item_browse_uri = self::URI . 'torrent/' . $torrent_id;
$item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id; $item_fetch_uri = self::ALTERNATE_URI . 'torrent/' . $torrent_id;
//Retrieve full description from torrent page (cached for 24 hours: 86400 seconds) //Retrieve full description from torrent page (cached for 24 hours: 86400 seconds)
if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) { if ($item_html = getSimpleHTMLDOMCached($item_fetch_uri, 86400, $headers, $opt)) {
//Retrieve data from page contents //Retrieve data from page contents
$item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext); $item_title = str_replace(' (Torrent) - AniDex ', '', $item_html->find('title', 0)->plaintext);
$item_desc = $item_html->find('div.panel-body', 0); $item_desc = $item_html->find('div.panel-body', 0);
@ -201,12 +200,12 @@ class AnidexBridge extends BridgeAbstract
} }
//Build and add final item //Build and add final item
$item = []; $item = array();
$item['uri'] = $item_browse_uri; $item['uri'] = $item_browse_uri;
$item['title'] = $item_title; $item['title'] = $item_title;
$item['author'] = $item_author; $item['author'] = $item_author;
$item['timestamp'] = $item_date; $item['timestamp'] = $item_date;
$item['enclosures'] = [$item_image]; $item['enclosures'] = array($item_image);
$item['content'] = $item_desc; $item['content'] = $item_desc;
$this->items[] = $item; $this->items[] = $item;
} }

View File

@ -1,31 +1,33 @@
<?php <?php
class AnimeUltimeBridge extends BridgeAbstract {
class AnimeUltimeBridge extends BridgeAbstract
{
const MAINTAINER = 'ORelio'; const MAINTAINER = 'ORelio';
const NAME = 'Anime-Ultime'; const NAME = 'Anime-Ultime';
const URI = 'http://www.anime-ultime.net/'; const URI = 'http://www.anime-ultime.net/';
const CACHE_TIMEOUT = 10800; // 3h const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.'; const DESCRIPTION = 'Returns the newest releases posted on Anime-Ultime.';
const PARAMETERS = [ [ const PARAMETERS = array( array(
'type' => [ 'type' => array(
'name' => 'Type', 'name' => 'Type',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Everything' => '', 'Everything' => '',
'Anime' => 'A', 'Anime' => 'A',
'Drama' => 'D', 'Drama' => 'D',
'Tokusatsu' => 'T' 'Tokusatsu' => 'T'
] )
] )
]]; ));
private $filter = 'Releases'; private $filter = 'Releases';
public function collectData() public function collectData(){
{
//Add type filter if provided //Add type filter if provided
$typeFilter = $this->getKey('type'); $typeFilter = array_search(
$this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
//Build date and filters for making requests //Build date and filters for making requests
$thismonth = date('mY') . $typeFilter; $thismonth = date('mY') . $typeFilter;
@ -33,7 +35,8 @@ class AnimeUltimeBridge extends BridgeAbstract
//Process each HTML page until having 10 releases //Process each HTML page until having 10 releases
$processedOK = 0; $processedOK = 0;
foreach ([$thismonth, $lastmonth] as $requestFilter) { foreach (array($thismonth, $lastmonth) as $requestFilter) {
$url = self::URI . 'history-0-1/' . $requestFilter; $url = self::URI . 'history-0-1/' . $requestFilter;
$html = getContents($url); $html = getContents($url);
// Convert html from iso-8859-1 => utf8 // Convert html from iso-8859-1 => utf8
@ -41,7 +44,8 @@ class AnimeUltimeBridge extends BridgeAbstract
$html = str_get_html($html); $html = str_get_html($html);
//Relases are sorted by day : process each day individually //Relases are sorted by day : process each day individually
foreach ($html->find('div.history', 0)->find('h3') as $daySection) { foreach($html->find('div.history', 0)->find('h3') as $daySection) {
//Retrieve day and build date information //Retrieve day and build date information
$dateString = $daySection->plaintext; $dateString = $daySection->plaintext;
$day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2)); $day = intval(substr($dateString, strpos($dateString, ' ') + 1, 2));
@ -55,8 +59,9 @@ class AnimeUltimeBridge extends BridgeAbstract
$release = $daySection->next_sibling()->next_sibling()->first_child(); $release = $daySection->next_sibling()->next_sibling()->first_child();
//Process each release of that day, ignoring first table row: contains table headers //Process each release of that day, ignoring first table row: contains table headers
while (!is_null($release = $release->next_sibling())) { while(!is_null($release = $release->next_sibling())) {
if (count($release->find('td')) > 0) { if(count($release->find('td')) > 0) {
//Retrieve metadata from table columns //Retrieve metadata from table columns
$item_link_element = $release->find('td', 0)->find('a', 0); $item_link_element = $release->find('td', 0)->find('a', 0);
$item_uri = self::URI . $item_link_element->href; $item_uri = self::URI . $item_link_element->href;
@ -80,7 +85,8 @@ class AnimeUltimeBridge extends BridgeAbstract
$item_fansub = $release->find('td', 2)->plaintext; $item_fansub = $release->find('td', 2)->plaintext;
$item_type = $release->find('td', 4)->plaintext; $item_type = $release->find('td', 4)->plaintext;
if (!empty($item_uri)) { if(!empty($item_uri)) {
// Retrieve description from description page // Retrieve description from description page
$html_item = getContents($item_uri); $html_item = getContents($item_uri);
// Convert html from iso-8859-1 => utf8 // Convert html from iso-8859-1 => utf8
@ -89,8 +95,7 @@ class AnimeUltimeBridge extends BridgeAbstract
$html_item, $html_item,
strpos($html_item, 'class="principal_contain" align="center">') + 41 strpos($html_item, 'class="principal_contain" align="center">') + 41
); );
$item_description = substr( $item_description = substr($item_description,
$item_description,
0, 0,
strpos($item_description, '<div id="table">') strpos($item_description, '<div id="table">')
); );
@ -101,18 +106,18 @@ class AnimeUltimeBridge extends BridgeAbstract
$item_description = str_replace("\n", '', $item_description); $item_description = str_replace("\n", '', $item_description);
//Build and add final item //Build and add final item
$item = []; $item = array();
$item['uri'] = $item_uri; $item['uri'] = $item_uri;
$item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode; $item['title'] = $item_name . ' ' . $item_type . ' ' . $item_episode;
$item['author'] = $item_fansub; $item['author'] = $item_fansub;
$item['timestamp'] = $item_date; $item['timestamp'] = $item_date;
$item['enclosures'] = [$item_image]; $item['enclosures'] = array($item_image);
$item['content'] = $item_description; $item['content'] = $item_description;
$this->items[] = $item; $this->items[] = $item;
$processedOK++; $processedOK++;
//Stop processing once limit is reached //Stop processing once limit is reached
if ($processedOK >= 10) { if ($processedOK >= 10)
return; return;
} }
} }
@ -120,12 +125,15 @@ class AnimeUltimeBridge extends BridgeAbstract
} }
} }
} }
}
public function getName() public function getName() {
{ if(!is_null($this->getInput('type'))) {
if (!is_null($this->getInput('type'))) { $typeFilter = array_search(
return 'Latest ' . $this->getKey('type') . ' - Anime-Ultime Bridge'; $this->getInput('type'),
self::PARAMETERS[$this->queriedContext]['type']['values']
);
return 'Latest ' . $typeFilter . ' - Anime-Ultime Bridge';
} }
return parent::getName(); return parent::getName();

View File

@ -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
];
}
}
}

View File

@ -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 [zhHant]' => 'zhHant',
'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 (nonfiction)' => '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',
'SciHub' => 'scihub',
'ZLibrary' => '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;
}
}

View File

@ -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;
}
}

View File

@ -1,23 +1,23 @@
<?php <?php
class AppleAppStoreBridge extends BridgeAbstract class AppleAppStoreBridge extends BridgeAbstract {
{
const MAINTAINER = 'captn3m0'; const MAINTAINER = 'captn3m0';
const NAME = 'Apple App Store'; const NAME = 'Apple App Store';
const URI = 'https://apps.apple.com/'; const URI = 'https://apps.apple.com/';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns version updates for a specific application'; const DESCRIPTION = 'Returns version updates for a specific application';
const PARAMETERS = [[ const PARAMETERS = array(array(
'id' => [ 'id' => array(
'name' => 'Application ID', 'name' => 'Application ID',
'required' => true, 'required' => true,
'exampleValue' => '310633997' 'exampleValue' => '310633997'
], ),
'p' => [ 'p' => array(
'name' => 'Platform', 'name' => 'Platform',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'iPad' => 'ipad', 'iPad' => 'ipad',
'iPhone' => 'iphone', 'iPhone' => 'iphone',
'Mac' => 'mac', 'Mac' => 'mac',
@ -26,51 +26,36 @@ class AppleAppStoreBridge extends BridgeAbstract
// but not yet tested // but not yet tested
'Web' => 'web', 'Web' => 'web',
'Apple TV' => 'appletv', 'Apple TV' => 'appletv',
], ),
'defaultValue' => 'iphone', 'defaultValue' => 'iphone',
], ),
'country' => [ 'country' => array(
'name' => 'Store Country', 'name' => 'Store Country',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'US' => 'US', 'US' => 'US',
'India' => 'IN', 'India' => 'IN',
'Canada' => 'CA', 'Canada' => 'CA',
'Germany' => 'DE', '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', 'defaultValue' => 'US',
], ),
]]; ));
const PLATFORM_MAPPING = [ const PLATFORM_MAPPING = array(
'iphone' => 'ios', 'iphone' => 'ios',
'ipad' => 'ios', 'ipad' => 'ios',
]; );
private function makeHtmlUrl($id, $country) private function makeHtmlUrl($id, $country){
{
return 'https://apps.apple.com/' . $country . '/app/id' . $id; return 'https://apps.apple.com/' . $country . '/app/id' . $id;
} }
private function makeJsonUrl($id, $platform, $country) private function makeJsonUrl($id, $platform, $country){
{
return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory"; return "https://amp-api.apps.apple.com/v1/catalog/$country/apps/$id?platform=$platform&extend=versionHistory";
} }
public function getName() public function getName(){
{
if (isset($this->name)) { if (isset($this->name)) {
return $this->name . ' - AppStore Updates'; return $this->name . ' - AppStore Updates';
} }
@ -81,8 +66,7 @@ class AppleAppStoreBridge extends BridgeAbstract
/** /**
* In case of some platforms, the data is present in the initial response * In case of some platforms, the data is present in the initial response
*/ */
private function getDataFromShoebox($id, $platform, $country) private function getDataFromShoebox($id, $platform, $country){
{
$uri = $this->makeHtmlUrl($id, $country); $uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600); $html = getSimpleHTMLDOMCached($uri, 3600);
$script = $html->find('script[id="shoebox-ember-data-store"]', 0); $script = $html->find('script[id="shoebox-ember-data-store"]', 0);
@ -91,8 +75,7 @@ class AppleAppStoreBridge extends BridgeAbstract
return $json['data']; return $json['data'];
} }
private function getJWTToken($id, $platform, $country) private function getJWTToken($id, $platform, $country){
{
$uri = $this->makeHtmlUrl($id, $country); $uri = $this->makeHtmlUrl($id, $country);
$html = getSimpleHTMLDOMCached($uri, 3600); $html = getSimpleHTMLDOMCached($uri, 3600);
@ -106,14 +89,13 @@ class AppleAppStoreBridge extends BridgeAbstract
return $json->MEDIA_API->token; return $json->MEDIA_API->token;
} }
private function getAppData($id, $platform, $country, $token) private function getAppData($id, $platform, $country, $token){
{
$uri = $this->makeJsonUrl($id, $platform, $country); $uri = $this->makeJsonUrl($id, $platform, $country);
$headers = [ $headers = array(
"Authorization: Bearer $token", "Authorization: Bearer $token",
'Origin: https://apps.apple.com', 'Origin: https://apps.apple.com',
]; );
$json = json_decode(getContents($uri, $headers), true); $json = json_decode(getContents($uri, $headers), true);
@ -124,9 +106,8 @@ class AppleAppStoreBridge extends BridgeAbstract
* Parses the version history from the data received * Parses the version history from the data received
* @return array list of versions with details on each element * @return array list of versions with details on each element
*/ */
private function getVersionHistory($data, $platform) private function getVersionHistory($data, $platform){
{ switch($platform) {
switch ($platform) {
case 'mac': case 'mac':
return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory']; return $data['relationships']['platforms']['data'][0]['attributes']['versionHistory'];
default: default:
@ -135,8 +116,7 @@ class AppleAppStoreBridge extends BridgeAbstract
} }
} }
public function collectData() public function collectData() {
{
$id = $this->getInput('id'); $id = $this->getInput('id');
$country = $this->getInput('country'); $country = $this->getInput('country');
$platform = $this->getInput('p'); $platform = $this->getInput('p');
@ -156,7 +136,7 @@ class AppleAppStoreBridge extends BridgeAbstract
$author = $data['attributes']['artistName']; $author = $data['attributes']['artistName'];
foreach ($versionHistory as $row) { foreach ($versionHistory as $row) {
$item = []; $item = array();
$item['content'] = nl2br($row['releaseNotes']); $item['content'] = nl2br($row['releaseNotes']);
$item['title'] = $name . ' - ' . $row['versionDisplay']; $item['title'] = $name . ' - ' . $row['versionDisplay'];

View File

@ -1,63 +1,25 @@
<?php <?php
class AppleMusicBridge extends BridgeAbstract class AppleMusicBridge extends BridgeAbstract {
{
const NAME = 'Apple Music'; const NAME = 'Apple Music';
const URI = 'https://www.apple.com'; const URI = 'https://www.apple.com';
const DESCRIPTION = 'Fetches the latest releases from an artist'; const DESCRIPTION = 'Fetches the latest releases from an artist';
const MAINTAINER = 'bockiii'; const MAINTAINER = 'bockiii';
const PARAMETERS = [[ const PARAMETERS = array(array(
'artist' => [ 'artist' => array(
'name' => 'Artist ID', 'name' => 'Artist ID',
'exampleValue' => '909253', 'exampleValue' => '909253',
'required' => true, 'required' => true,
], ),
'limit' => [ 'limit' => array(
'name' => 'Latest X Releases (max 50)', 'name' => 'Latest X Releases (max 50)',
'defaultValue' => '10', 'defaultValue' => '10',
'required' => true, 'required' => true,
], ),
]]; ));
const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours const CACHE_TIMEOUT = 21600; // 6 hours
private $title; public function collectData() {
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 # Limit the amount of releases to 50
if ($this->getInput('limit') > 50) { if ($this->getInput('limit') > 50) {
$limit = 50; $limit = 50;
@ -65,53 +27,29 @@ class AppleMusicBridge extends BridgeAbstract
$limit = $this->getInput('limit'); $limit = $this->getInput('limit');
} }
$url = 'https://itunes.apple.com/lookup?id=' . $this->getInput('artist') . '&entity=album&limit=' . $limit . '&sort=recent'; $url = 'https://itunes.apple.com/lookup?id='
. $this->getInput('artist')
. '&entity=album&limit='
. $limit .
'&sort=recent';
$html = getSimpleHTMLDOM($url); $html = getSimpleHTMLDOM($url);
$json = json_decode($html); $json = json_decode($html);
$result = $json->results;
if (!is_array($result) || count($result) == 0) { foreach ($json->results as $obj) {
returnServerError('There is no artist with id "' . $this->getInput('artist') . '".'); if ($obj->wrapperType === 'collection') {
$this->items[] = array(
'title' => $obj->artistName . ' - ' . $obj->collectionName,
'uri' => $obj->collectionViewUrl,
'timestamp' => $obj->releaseDate,
'enclosures' => $obj->artworkUrl100,
'content' => '<a href=' . $obj->collectionViewUrl
. '><img src="' . $obj->artworkUrl100 . '" /></a><br><br>'
. $obj->artistName . ' - ' . $obj->collectionName
. '<br>'
. $obj->copyright,
);
} }
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;
} }
} }

View File

@ -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;
}
}

View File

@ -1,72 +1,65 @@
<?php <?php
class ArtStationBridge extends BridgeAbstract {
class ArtStationBridge extends BridgeAbstract
{
const NAME = 'ArtStation'; const NAME = 'ArtStation';
const URI = 'https://www.artstation.com'; const URI = 'https://www.artstation.com';
const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.'; const DESCRIPTION = 'Fetches the latest ten artworks from a search query on ArtStation.';
const MAINTAINER = 'thefranke'; const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
const PARAMETERS = [ const PARAMETERS = array(
'Search Query' => [ 'Search Query' => array(
'q' => [ 'q' => array(
'name' => 'Search term', 'name' => 'Search term',
'required' => true, 'required' => true,
'exampleValue' => 'bird' 'exampleValue' => 'bird'
] )
] )
]; );
public function getIcon() public function getIcon() {
{
return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico'; return 'https://www.artstation.com/assets/favicon-58653022bc38c1905ac7aa1b10bffa6b.ico';
} }
public function getName() public function getName() {
{
return self::NAME . ': ' . $this->getInput('q'); return self::NAME . ': ' . $this->getInput('q');
} }
private function fetchSearch($searchQuery) private function fetchSearch($searchQuery) {
{
$data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",'; $data = '{"query":"' . $searchQuery . '","page":1,"per_page":50,"sorting":"date",';
$data .= '"pro_first":"1","filters":[],"additional_fields":[]}'; $data .= '"pro_first":"1","filters":[],"additional_fields":[]}';
$header = [ $header = array(
'Content-Type: application/json', 'Content-Type: application/json',
'Accept: application/json' 'Accept: application/json'
]; );
$opts = [ $opts = array(
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data, CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true CURLOPT_RETURNTRANSFER => true
]; );
$jsonSearchURL = self::URI . '/api/v2/search/projects.json'; $jsonSearchURL = self::URI . '/api/v2/search/projects.json';
$jsonSearchStr = getContents($jsonSearchURL, $header, $opts); $jsonSearchStr = getContents($jsonSearchURL, $header, $opts);
return json_decode($jsonSearchStr); return json_decode($jsonSearchStr);
} }
private function fetchProject($hashID) private function fetchProject($hashID) {
{
$jsonProjectURL = self::URI . '/projects/' . $hashID . '.json'; $jsonProjectURL = self::URI . '/projects/' . $hashID . '.json';
$jsonProjectStr = getContents($jsonProjectURL); $jsonProjectStr = getContents($jsonProjectURL);
return json_decode($jsonProjectStr); return json_decode($jsonProjectStr);
} }
public function collectData() public function collectData() {
{
$searchTerm = $this->getInput('q'); $searchTerm = $this->getInput('q');
$jsonQuery = $this->fetchSearch($searchTerm); $jsonQuery = $this->fetchSearch($searchTerm);
foreach ($jsonQuery->data as $media) { foreach($jsonQuery->data as $media) {
// get detailed info about media item // get detailed info about media item
$jsonProject = $this->fetchProject($media->hash_id); $jsonProject = $this->fetchProject($media->hash_id);
// create item // create item
$item = []; $item = array();
$item['title'] = $media->title; $item['title'] = $media->title;
$item['uri'] = $media->url; $item['uri'] = $media->url;
$item['timestamp'] = strtotime($jsonProject->published_at); $item['timestamp'] = strtotime($jsonProject->published_at);
@ -83,19 +76,17 @@ class ArtStationBridge extends BridgeAbstract
$numAssets = count($jsonProject->assets); $numAssets = count($jsonProject->assets);
if ($numAssets > 1) { if ($numAssets > 1)
$item['content'] .= '<p><a href="' $item['content'] .= '<p><a href="'
. $media->url . $media->url
. '">Project contains ' . '">Project contains '
. ($numAssets - 1) . ($numAssets - 1)
. ' more item(s).</a></p>'; . ' more item(s).</a></p>';
}
$this->items[] = $item; $this->items[] = $item;
if (count($this->items) >= 10) { if (count($this->items) >= 10)
break; break;
} }
} }
}
} }

View File

@ -1,7 +1,7 @@
<?php <?php
class Arte7Bridge extends BridgeAbstract {
class Arte7Bridge extends BridgeAbstract // const MAINTAINER = 'mitsukarenai';
{
const NAME = 'Arte +7'; const NAME = 'Arte +7';
const URI = 'https://www.arte.tv/'; const URI = 'https://www.arte.tv/';
const CACHE_TIMEOUT = 1800; // 30min const CACHE_TIMEOUT = 1800; // 30min
@ -9,60 +9,34 @@ class Arte7Bridge extends BridgeAbstract
const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA'; const API_TOKEN = 'Nzc1Yjc1ZjJkYjk1NWFhN2I2MWEwMmRlMzAzNjI5NmU3NWU3ODg4ODJjOWMxNTMxYzEzZGRjYjg2ZGE4MmIwOA';
const PARAMETERS = [ const PARAMETERS = array(
'global' => [ 'global' => [
'sort_by' => [ 'video_duration_filter' => [
'type' => 'list', 'name' => 'Exclude short videos',
'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', 'type' => 'checkbox',
'required' => false, 'title' => 'Exclude videos that are shorter than 3 minutes',
'defaultValue' => false 'defaultValue' => false,
], ],
], ],
'Category' => [ 'Category' => array(
'lang' => [ 'lang' => array(
'type' => 'list', 'type' => 'list',
'name' => 'Language', 'name' => 'Language',
'values' => [ 'values' => array(
'Français' => 'fr', 'Français' => 'fr',
'Deutsch' => 'de', 'Deutsch' => 'de',
'English' => 'en', 'English' => 'en',
'Español' => 'es', 'Español' => 'es',
'Polski' => 'pl', 'Polski' => 'pl',
'Italiano' => 'it' 'Italiano' => 'it'
], ),
], 'title' => 'ex. RC-014095 pour https://www.arte.tv/fr/videos/RC-014095/blow-up/',
'cat' => [ 'exampleValue' => 'RC-014095'
),
'cat' => array(
'type' => 'list', 'type' => 'list',
'name' => 'Category', 'name' => 'Category',
'values' => [ 'values' => array(
'All videos' => null, 'All videos' => null,
'News & society' => 'ACT', 'News & society' => 'ACT',
'Series & fiction' => 'SER', 'Series & fiction' => 'SER',
@ -73,34 +47,34 @@ class Arte7Bridge extends BridgeAbstract
'History' => 'HIST', 'History' => 'HIST',
'Science' => 'SCI', 'Science' => 'SCI',
'Other' => 'AUT' 'Other' => 'AUT'
] )
], ),
], ),
'Collection' => [ 'Collection' => array(
'lang' => [ 'lang' => array(
'type' => 'list', 'type' => 'list',
'name' => 'Language', 'name' => 'Language',
'values' => [ 'values' => array(
'Français' => 'fr', 'Français' => 'fr',
'Deutsch' => 'de', 'Deutsch' => 'de',
'English' => 'en', 'English' => 'en',
'Español' => 'es', 'Español' => 'es',
'Polski' => 'pl', 'Polski' => 'pl',
'Italiano' => 'it' 'Italiano' => 'it'
] )
], ),
'col' => [ 'col' => array(
'name' => 'Collection id', 'name' => 'Collection id',
'required' => true, 'required' => true,
'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/', 'title' => 'ex. RC-014095 pour https://www.arte.tv/de/videos/RC-014095/blow-up/',
'exampleValue' => 'RC-014095' 'exampleValue' => 'RC-014095'
] )
] )
]; );
public function collectData() public function collectData(){
{ $lang = $this->getInput('lang');
switch ($this->queriedContext) { switch($this->queriedContext) {
case 'Category': case 'Category':
$category = $this->getInput('cat'); $category = $this->getInput('cat');
$collectionId = null; $collectionId = null;
@ -111,40 +85,34 @@ class Arte7Bridge extends BridgeAbstract
break; break;
} }
$lang = $this->getInput('lang'); $url = 'https://api.arte.tv/api/opa/v3/videos?sort=-lastModified&limit=15&language='
$sort_by = $this->getInput('sort_by');
$sort_direction = $this->getInput('sort_direction') == 'ASC' ? '' : '-';
$url = 'https://api.arte.tv/api/opa/v3/videos?limit=15&language='
. $lang . $lang
. ($sort_by != null ? '&sort=' . $sort_direction . $sort_by : '')
. ($category != null ? '&category.code=' . $category : '') . ($category != null ? '&category.code=' . $category : '')
. ($collectionId != null ? '&collections.collectionId=' . $collectionId : ''); . ($collectionId != null ? '&collections.collectionId=' . $collectionId : '');
$header = [ $header = array(
'Authorization: Bearer ' . self::API_TOKEN 'Authorization: Bearer ' . self::API_TOKEN
]; );
$input = getContents($url, $header); $input = getContents($url, $header);
$input_json = json_decode($input, true); $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') { $durationSeconds = $element['durationSeconds'];
if ($this->getInput('video_duration_filter') && $durationSeconds < 60 * 3) {
continue; continue;
} }
$durationSeconds = $element['durationSeconds']; $item = array();
$item = [];
$item['uri'] = $element['url']; $item['uri'] = $element['url'];
$item['id'] = $element['id']; $item['id'] = $element['id'];
$item['timestamp'] = strtotime($element['videoRightsBegin']); $item['timestamp'] = strtotime($element['videoRightsBegin']);
$item['title'] = $element['title']; $item['title'] = $element['title'];
if (!empty($element['subtitle'])) { if(!empty($element['subtitle']))
$item['title'] = $element['title'] . ' | ' . $element['subtitle']; $item['title'] = $element['title'] . ' | ' . $element['subtitle'];
}
$durationMinutes = round((int)$durationSeconds / 60); $durationMinutes = round((int)$durationSeconds / 60);
$item['content'] = $element['teaserText'] $item['content'] = $element['teaserText']
@ -156,10 +124,6 @@ class Arte7Bridge extends BridgeAbstract
. $element['mainImage']['url'] . $element['mainImage']['url']
. '" /></a>'; . '" /></a>';
$item['itunes'] = [
'duration' => $durationSeconds,
];
$this->items[] = $item; $this->items[] = $item;
} }
} }

View File

@ -1,18 +1,16 @@
<?php <?php
class AsahiShimbunAJWBridge extends BridgeAbstract {
class AsahiShimbunAJWBridge extends BridgeAbstract
{
const NAME = 'Asahi Shimbun AJW'; const NAME = 'Asahi Shimbun AJW';
const BASE_URI = 'http://www.asahi.com'; const BASE_URI = 'http://www.asahi.com';
const URI = self::BASE_URI . '/ajw/'; const URI = self::BASE_URI . '/ajw/';
const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch'; const DESCRIPTION = 'Asahi Shimbun - Asia & Japan Watch';
const MAINTAINER = 'somini'; const MAINTAINER = 'somini';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'section' => [ 'section' => array(
'type' => 'list', 'type' => 'list',
'name' => 'Section', 'name' => 'Section',
'values' => [ 'values' => array(
'Japan » Social Affairs' => 'japan/social', 'Japan » Social Affairs' => 'japan/social',
'Japan » People' => 'japan/people', 'Japan » People' => 'japan/people',
'Japan » 3/11 Disaster' => 'japan/0311disaster', 'Japan » 3/11 Disaster' => 'japan/0311disaster',
@ -22,32 +20,30 @@ class AsahiShimbunAJWBridge extends BridgeAbstract
'Culture » Style' => 'culture/style', 'Culture » Style' => 'culture/style',
'Culture » Movies' => 'culture/movies', 'Culture » Movies' => 'culture/movies',
'Culture » Manga & Anime' => 'culture/manga_anime', 'Culture » Manga & Anime' => 'culture/manga_anime',
'Asia » China' => 'asia_world/china', 'Asia » China' => 'asia/china',
'Asia » Korean Peninsula' => 'asia_world/korean_peninsula', 'Asia » Korean Peninsula' => 'asia/korean_peninsula',
'Asia » Around Asia' => 'asia_world/around_asia', 'Asia » Around Asia' => 'asia/around_asia',
'Asia » World' => 'asia_world/world',
'Opinion » Editorial' => 'opinion/editorial', 'Opinion » Editorial' => 'opinion/editorial',
'Opinion » Vox Populi' => 'opinion/vox', 'Opinion » Vox Populi' => 'opinion/vox',
], ),
'defaultValue' => 'politics', 'defaultValue' => 'politics',
] )
] )
]; );
private function getSectionURI($section) private function getSectionURI($section) {
{ return self::getURI() . $section . '/';
return $this->getURI() . $section . '/';
} }
public function collectData() public function collectData() {
{
$html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section'))); $html = getSimpleHTMLDOM($this->getSectionURI($this->getInput('section')));
foreach ($html->find('#MainInner li a') as $element) { foreach($html->find('#MainInner li a') as $element) {
if ($element->parent()->class == 'HeadlineTopImage-S') { if ($element->parent()->class == 'HeadlineTopImage-S') {
Debug::log('Skip Headline, it is repeated below');
continue; continue;
} }
$item = []; $item = array();
$item['uri'] = self::BASE_URI . $element->href; $item['uri'] = self::BASE_URI . $element->href;
$e_lead = $element->find('span.Lead', 0); $e_lead = $element->find('span.Lead', 0);
@ -67,12 +63,7 @@ class AsahiShimbunAJWBridge extends BridgeAbstract
$e_video->outertext = ''; $e_video->outertext = '';
$element->innertext = "VIDEO: $element->innertext"; $element->innertext = "VIDEO: $element->innertext";
} }
$e_title = $element->find('.title', 0);
if ($e_title) {
$item['title'] = $e_title->innertext;
} else {
$item['title'] = $element->innertext; $item['title'] = $element->innertext;
}
$this->items[] = $item; $this->items[] = $item;
} }

View File

@ -1,53 +1,49 @@
<?php <?php
class AskfmBridge extends BridgeAbstract {
class AskfmBridge extends BridgeAbstract
{
const MAINTAINER = 'az5he6ch, logmanoriginal'; const MAINTAINER = 'az5he6ch, logmanoriginal';
const NAME = 'Ask.fm Answers'; const NAME = 'Ask.fm Answers';
const URI = 'https://ask.fm/'; const URI = 'https://ask.fm/';
const CACHE_TIMEOUT = 300; //5 min const CACHE_TIMEOUT = 300; //5 min
const DESCRIPTION = 'Returns answers from an Ask.fm user'; const DESCRIPTION = 'Returns answers from an Ask.fm user';
const PARAMETERS = [ const PARAMETERS = array(
'Ask.fm username' => [ 'Ask.fm username' => array(
'u' => [ 'u' => array(
'name' => 'Username', 'name' => 'Username',
'required' => true, 'required' => true,
'exampleValue' => 'ApprovedAndReal' 'exampleValue' => 'ApprovedAndReal'
] )
] )
]; );
public function collectData() public function collectData(){
{
$html = getSimpleHTMLDOM($this->getURI()); $html = getSimpleHTMLDOM($this->getURI());
$html = defaultLinkTo($html, self::URI); $html = defaultLinkTo($html, self::URI);
foreach ($html->find('article.streamItem-answer') as $element) { foreach($html->find('article.streamItem-answer') as $element) {
$item = []; $item = array();
$item['uri'] = $element->find('a.streamItem_meta', 0)->href; $item['uri'] = $element->find('a.streamItem_meta', 0)->href;
$question = trim($element->find('header.streamItem_header', 0)->innertext); $question = trim($element->find('header.streamItem_header', 0)->innertext);
$item['title'] = trim( $item['title'] = trim(
htmlspecialchars_decode( htmlspecialchars_decode($element->find('header.streamItem_header', 0)->plaintext,
$element->find('header.streamItem_header', 0)->plaintext,
ENT_QUOTES 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($element->find('div.streamItem_content', 0)->innertext);
$answer = trim($var->innertext ?? '');
// This probably should be cleaned up, especially for YouTube embeds // This probably should be cleaned up, especially for YouTube embeds
if ($visual = $element->find('div.streamItem_visual', 0)) { if($visual = $element->find('div.streamItem_visual', 0)) {
$visual = $visual->innertext; $visual = $visual->innertext;
} }
// Fix tracking links, also doesn't work // Fix tracking links, also doesn't work
foreach ($element->find('a') as $link) { foreach($element->find('a') as $link) {
if (strpos($link->href, 'l.ask.fm') !== false) { if(strpos($link->href, 'l.ask.fm') !== false) {
$link->href = $link->plaintext; $link->href = $link->plaintext;
} }
} }
@ -60,18 +56,16 @@ class AskfmBridge extends BridgeAbstract
} }
} }
public function getName() public function getName(){
{ if(!is_null($this->getInput('u'))) {
if (!is_null($this->getInput('u'))) {
return self::NAME . ' : ' . $this->getInput('u'); return self::NAME . ' : ' . $this->getInput('u');
} }
return parent::getName(); return parent::getName();
} }
public function getURI() public function getURI(){
{ if(!is_null($this->getInput('u'))) {
if (!is_null($this->getInput('u'))) {
return self::URI . urlencode($this->getInput('u')); return self::URI . urlencode($this->getInput('u'));
} }

View File

@ -1,17 +1,15 @@
<?php <?php
class AssociatedPressNewsBridge extends BridgeAbstract {
class AssociatedPressNewsBridge extends BridgeAbstract
{
const NAME = 'Associated Press News Bridge'; const NAME = 'Associated Press News Bridge';
const URI = 'https://apnews.com/'; const URI = 'https://apnews.com/';
const DESCRIPTION = 'Returns newest articles by topic'; const DESCRIPTION = 'Returns newest articles by topic';
const MAINTAINER = 'VerifiedJoseph'; const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [ const PARAMETERS = array(
'Standard Topics' => [ 'Standard Topics' => array(
'topic' => [ 'topic' => array(
'name' => 'Topic', 'name' => 'Topic',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'AP Top News' => 'apf-topnews', 'AP Top News' => 'apf-topnews',
'Sports' => 'apf-sports', 'Sports' => 'apf-sports',
'Entertainment' => 'apf-entertainment', 'Entertainment' => 'apf-entertainment',
@ -29,19 +27,19 @@ class AssociatedPressNewsBridge extends BridgeAbstract
'Photo Galleries' => 'PhotoGalleries', 'Photo Galleries' => 'PhotoGalleries',
'Fact Checks' => 'APFactCheck', 'Fact Checks' => 'APFactCheck',
'Videos' => 'apf-videos', 'Videos' => 'apf-videos',
], ),
'defaultValue' => 'apf-topnews', 'defaultValue' => 'apf-topnews',
], ),
], ),
'Custom Topic' => [ 'Custom Topic' => array(
'topic' => [ 'topic' => array(
'name' => 'Topic', 'name' => 'Topic',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'exampleValue' => 'europe' 'exampleValue' => 'europe'
], ),
] )
]; );
const CACHE_TIMEOUT = 900; // 15 mins const CACHE_TIMEOUT = 900; // 15 mins
@ -49,11 +47,10 @@ class AssociatedPressNewsBridge extends BridgeAbstract
private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags='; private $tagEndpoint = 'https://afs-prod.appspot.com/api/v2/feed/tag?tags=';
private $feedName = ''; private $feedName = '';
public function detectParameters($url) public function detectParameters($url) {
{ $params = array();
$params = [];
if (preg_match($this->detectParamRegex, $url, $matches) > 0) { if(preg_match($this->detectParamRegex, $url, $matches) > 0) {
$params['topic'] = $matches[1]; $params['topic'] = $matches[1];
$params['context'] = 'Custom Topic'; $params['context'] = 'Custom Topic';
return $params; return $params;
@ -62,9 +59,8 @@ class AssociatedPressNewsBridge extends BridgeAbstract
return null; return null;
} }
public function collectData() public function collectData() {
{ switch($this->getInput('topic')) {
switch ($this->getInput('topic')) {
case 'Podcasts': case 'Podcasts':
returnClientError('Podcasts topic feed is not supported'); returnClientError('Podcasts topic feed is not supported');
break; break;
@ -76,8 +72,7 @@ class AssociatedPressNewsBridge extends BridgeAbstract
} }
} }
public function getURI() public function getURI() {
{
if (!is_null($this->getInput('topic'))) { if (!is_null($this->getInput('topic'))) {
return self::URI . $this->getInput('topic'); return self::URI . $this->getInput('topic');
} }
@ -85,8 +80,7 @@ class AssociatedPressNewsBridge extends BridgeAbstract
return parent::getURI(); return parent::getURI();
} }
public function getName() public function getName() {
{
if (!empty($this->feedName)) { if (!empty($this->feedName)) {
return $this->feedName . ' - Associated Press'; return $this->feedName . ' - Associated Press';
} }
@ -94,8 +88,7 @@ class AssociatedPressNewsBridge extends BridgeAbstract
return parent::getName(); return parent::getName();
} }
private function getTagURI() private function getTagURI() {
{
if (!is_null($this->getInput('topic'))) { if (!is_null($this->getInput('topic'))) {
return $this->tagEndpoint . $this->getInput('topic'); return $this->tagEndpoint . $this->getInput('topic');
} }
@ -103,9 +96,9 @@ class AssociatedPressNewsBridge extends BridgeAbstract
return parent::getURI(); return parent::getURI();
} }
private function collectCardData() private function collectCardData() {
{ $json = getContents($this->getTagURI())
$json = getContents($this->getTagURI()); or returnServerError('Could not request: ' . $this->getTagURI());
$tagContents = json_decode($json, true); $tagContents = json_decode($json, true);
@ -116,7 +109,7 @@ class AssociatedPressNewsBridge extends BridgeAbstract
$this->feedName = $tagContents['tagObjs'][0]['name']; $this->feedName = $tagContents['tagObjs'][0]['name'];
foreach ($tagContents['cards'] as $card) { foreach ($tagContents['cards'] as $card) {
$item = []; $item = array();
// skip hub peeks & Notifications // skip hub peeks & Notifications
if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') { if ($card['cardType'] == 'Hub Peek' || $card['cardType'] == 'Notification') {
@ -125,7 +118,7 @@ class AssociatedPressNewsBridge extends BridgeAbstract
$storyContent = $card['contents'][0]; $storyContent = $card['contents'][0];
switch ($storyContent['contentType']) { switch($storyContent['contentType']) {
case 'web': // Skip link only content case 'web': // Skip link only content
continue 2; continue 2;
@ -148,11 +141,8 @@ class AssociatedPressNewsBridge extends BridgeAbstract
$this->processIframes($html); $this->processIframes($html);
if (!is_null($storyContent['leadPhotoId'])) { if (!is_null($storyContent['leadPhotoId'])) {
$leadPhotoUrl = sprintf('https://storage.googleapis.com/afs-prod/media/%s/800.jpeg', $storyContent['leadPhotoId']); $item['enclosures'][] = 'https://storage.googleapis.com/afs-prod/media/'
$leadPhotoImageTag = sprintf('<img src="%s">', $leadPhotoUrl); . $storyContent['leadPhotoId'] . '/800.jpeg';
// Move the image to the beginning of the content
$html = $leadPhotoImageTag . $html;
// Explicitly not adding it to the item's enclosures!
} }
} }
@ -188,8 +178,8 @@ class AssociatedPressNewsBridge extends BridgeAbstract
} }
} }
private function processMediaPlaceholders($html, $id) private function processMediaPlaceholders($html, $id) {
{
if ($html->find('div.media-placeholder', 0)) { if ($html->find('div.media-placeholder', 0)) {
// Fetch page content // Fetch page content
$json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id); $json = getContents('https://afs-prod.appspot.com/api/v2/content/' . $id);
@ -226,10 +216,11 @@ EOD;
/* /*
Create full coverage links (HubLinks) Create full coverage links (HubLinks)
*/ */
private function processHubLinks($html, $storyContent) private function processHubLinks($html, $storyContent) {
{
if (!empty($storyContent['richEmbeds'])) { if (!empty($storyContent['richEmbeds'])) {
foreach ($storyContent['richEmbeds'] as $embed) { foreach ($storyContent['richEmbeds'] as $embed) {
if ($embed['type'] === 'Hub Link') { if ($embed['type'] === 'Hub Link') {
$url = self::URI . $embed['tag']['id']; $url = self::URI . $embed['tag']['id'];
$div = $html->find('div[id=' . $embed['id'] . ']', 0); $div = $html->find('div[id=' . $embed['id'] . ']', 0);
@ -244,8 +235,7 @@ EOD;
} }
} }
private function processVideo($storyContent) private function processVideo($storyContent) {
{
$video = $storyContent['media'][0]; $video = $storyContent['media'][0];
if ($video['type'] === 'YouTube') { if ($video['type'] === 'YouTube') {
@ -265,8 +255,8 @@ EOD;
} }
// Remove datawrapper.dwcdn.net iframes and related javaScript // Remove datawrapper.dwcdn.net iframes and related javaScript
private function processIframes($html) private function processIframes($html) {
{
foreach ($html->find('iframe') as $index => $iframe) { foreach ($html->find('iframe') as $index => $iframe) {
if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) { if (preg_match('/datawrapper\.dwcdn\.net/', $iframe->src)) {
$iframe->outertext = ''; $iframe->outertext = '';

View File

@ -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;
}
}
}

View File

@ -1,24 +1,22 @@
<?php <?php
class AtmoNouvelleAquitaineBridge extends BridgeAbstract {
class AtmoNouvelleAquitaineBridge extends BridgeAbstract
{
const NAME = 'Atmo Nouvelle Aquitaine'; const NAME = 'Atmo Nouvelle Aquitaine';
const URI = 'https://www.atmo-nouvelleaquitaine.org'; const URI = 'https://www.atmo-nouvelleaquitaine.org';
const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo'; const DESCRIPTION = 'Fetches the latest air polution of cities in Nouvelle Aquitaine from Atmo';
const MAINTAINER = 'floviolleau'; const MAINTAINER = 'floviolleau';
const PARAMETERS = [[ const PARAMETERS = array(array(
'cities' => [ 'cities' => array(
'name' => 'Choisir une ville', 'name' => 'Choisir une ville',
'type' => 'list', 'type' => 'list',
'values' => self::CITIES 'values' => self::CITIES
] )
]]; ));
const CACHE_TIMEOUT = 7200; const CACHE_TIMEOUT = 7200;
private $dom; private $dom;
private function getClosest($search, $arr) private function getClosest($search, $arr) {
{
$closest = null; $closest = null;
foreach ($arr as $key => $value) { foreach ($arr as $key => $value) {
if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) { if ($closest === null || abs((int)$search - $closest) > abs((int)$key - (int)$search)) {
@ -28,11 +26,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return $arr[$closest]; return $arr[$closest];
} }
public function collectData() public function collectData() {
{
// this bridge is broken and unmaintained
return;
$uri = self::URI . '/monair/commune/' . $this->getInput('cities'); $uri = self::URI . '/monair/commune/' . $this->getInput('cities');
$html = getSimpleHTMLDOM($uri); $html = getSimpleHTMLDOM($uri);
@ -53,8 +47,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
$this->items[] = $item; $this->items[] = $item;
} }
private function getIndex() private function getIndex() {
{
$index = $this->dom->find('.indice', 0)->innertext; $index = $this->dom->find('.indice', 0)->innertext;
if ($index == 'XX') { if ($index == 'XX') {
@ -64,14 +57,12 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return $index; return $index;
} }
private function getMaxIndexText() private function getMaxIndexText() {
{
// will return '/100' // will return '/100'
return $this->dom->find('.pourcent', 0)->innertext; return $this->dom->find('.pourcent', 0)->innertext;
} }
private function getQualityText($index, $indexes) private function getQualityText($index, $indexes) {
{
if ($index == -1) { if ($index == -1) {
if (array_key_exists('no-available', $indexes)) { if (array_key_exists('no-available', $indexes)) {
return $indexes['no-available']; return $indexes['no-available'];
@ -83,10 +74,9 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return $this->getClosest($index, $indexes); return $this->getClosest($index, $indexes);
} }
private function getLegendIndexes() private function getLegendIndexes() {
{
$rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label'); $rawIndexes = $this->dom->find('.prevision-legend .prevision-legend-label');
$indexes = []; $indexes = array();
for ($i = 0; $i < count($rawIndexes); $i++) { for ($i = 0; $i < count($rawIndexes); $i++) {
if ($rawIndexes[$i]->hasAttribute('data-color')) { if ($rawIndexes[$i]->hasAttribute('data-color')) {
$indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext; $indexes[$rawIndexes[$i]->getAttribute('data-color')] = $rawIndexes[$i]->innertext;
@ -96,8 +86,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return $indexes; return $indexes;
} }
private function getTomorrowTrendIndex() private function getTomorrowTrendIndex() {
{
$tomorrowTrendDomNode = $this->dom $tomorrowTrendDomNode = $this->dom
->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2); ->find('.day-controls.raster-controls .list-raster-controls .raster-control', 2);
$tomorrowTrendIndexNode = null; $tomorrowTrendIndexNode = null;
@ -115,8 +104,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return $tomorrowTrendIndex; return $tomorrowTrendIndex;
} }
private function getTomorrowTrendQualityText($trendIndex, $indexes) private function getTomorrowTrendQualityText($trendIndex, $indexes) {
{
if ($trendIndex == -1) { if ($trendIndex == -1) {
if (array_key_exists('no-available', $indexes)) { if (array_key_exists('no-available', $indexes)) {
return $indexes['no-available']; return $indexes['no-available'];
@ -128,8 +116,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return $this->getClosest($trendIndex, $indexes); return $this->getClosest($trendIndex, $indexes);
} }
private function getIndexMessage() private function getIndexMessage() {
{
$index = $this->getIndex(); $index = $this->getIndex();
$maxIndexText = $this->getMaxIndexText(); $maxIndexText = $this->getMaxIndexText();
@ -140,8 +127,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return "L'indice d'aujourd'hui est $index$maxIndexText."; return "L'indice d'aujourd'hui est $index$maxIndexText.";
} }
private function getQualityMessage() private function getQualityMessage() {
{
$index = $index = $this->getIndex(); $index = $index = $this->getIndex();
$indexes = $this->getLegendIndexes(); $indexes = $this->getLegendIndexes();
$quality = $this->getQualityText($index, $indexes); $quality = $this->getQualityText($index, $indexes);
@ -153,8 +139,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return "La qualité de l'air est $quality."; return "La qualité de l'air est $quality.";
} }
private function getTomorrowTrendIndexMessage() private function getTomorrowTrendIndexMessage() {
{
$trendIndex = $this->getTomorrowTrendIndex(); $trendIndex = $this->getTomorrowTrendIndex();
$maxIndexText = $this->getMaxIndexText(); $maxIndexText = $this->getMaxIndexText();
@ -165,8 +150,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return "L'indice prévu pour demain est $trendIndex$maxIndexText."; return "L'indice prévu pour demain est $trendIndex$maxIndexText.";
} }
private function getTomorrowTrendQualityMessage() private function getTomorrowTrendQualityMessage() {
{
$trendIndex = $this->getTomorrowTrendIndex(); $trendIndex = $this->getTomorrowTrendIndex();
$indexes = $this->getLegendIndexes(); $indexes = $this->getLegendIndexes();
$trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes); $trendQuality = $this->getTomorrowTrendQualityText($trendIndex, $indexes);
@ -177,7 +161,7 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
return "La qualite de l'air pour demain sera $trendQuality."; return "La qualite de l'air pour demain sera $trendQuality.";
} }
const CITIES = [ const CITIES = array(
'Aast (64460)' => '64001', 'Aast (64460)' => '64001',
'Abère (64160)' => '64002', 'Abère (64160)' => '64002',
'Abidos (64150)' => '64003', 'Abidos (64150)' => '64003',
@ -4649,5 +4633,5 @@ class AtmoNouvelleAquitaineBridge extends BridgeAbstract
'Yvrac (33370)' => '33554', 'Yvrac (33370)' => '33554',
'Yvrac-et-Malleyrand (16110)' => '16425', 'Yvrac-et-Malleyrand (16110)' => '16425',
'Yzosse (40180)' => '40334' 'Yzosse (40180)' => '40334'
]; );
} }

View File

@ -1,22 +1,20 @@
<?php <?php
class AtmoOccitanieBridge extends BridgeAbstract {
class AtmoOccitanieBridge extends BridgeAbstract
{
const NAME = 'Atmo Occitanie'; const NAME = 'Atmo Occitanie';
const URI = 'https://www.atmo-occitanie.org/'; const URI = 'https://www.atmo-occitanie.org/';
const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo'; const DESCRIPTION = 'Fetches the latest air polution of cities in Occitanie from Atmo';
const MAINTAINER = 'floviolleau'; const MAINTAINER = 'floviolleau';
const PARAMETERS = [[ const PARAMETERS = array(array(
'city' => [ 'city' => array(
'name' => 'Ville', 'name' => 'Ville',
'required' => true, 'required' => true,
'exampleValue' => 'cahors' 'exampleValue' => 'cahors'
] )
]]; ));
const CACHE_TIMEOUT = 7200; const CACHE_TIMEOUT = 7200;
public function collectData() public function collectData() {
{
$uri = self::URI . $this->getInput('city'); $uri = self::URI . $this->getInput('city');
$html = getSimpleHTMLDOM($uri); $html = getSimpleHTMLDOM($uri);

View File

@ -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;
}
}

View File

@ -1,68 +1,48 @@
<?php <?php
class AutoJMBridge extends BridgeAbstract class AutoJMBridge extends BridgeAbstract {
{
const NAME = 'AutoJM'; const NAME = 'AutoJM';
const URI = 'https://www.autojm.fr/'; 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 DESCRIPTION = 'Suivre les offres de véhicules proposés par AutoJM en fonction des critères de filtrages';
const MAINTAINER = 'sysadminstory'; const MAINTAINER = 'sysadminstory';
const PARAMETERS = [ const PARAMETERS = array(
'Afficher les offres de véhicules disponible sur la recheche AutoJM' => [ 'Afficher les offres de véhicules disponible sur la recheche AutoJM' => array(
'url' => [ 'url' => array(
'name' => 'URL de la page de recherche', 'name' => 'URL de la page de recherche',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/', 'title' => 'URL d\'une recherche avec filtre de véhicules sans le http://www.autojm.fr/',
'exampleValue' => 'recherche?brands[]=PEUGEOT&ranges[]=PEUGEOT 308' 'exampleValue' => 'recherche?brands[]=peugeot&ranges[]=peugeot-nouvelle-308-2021-5p'
], ),
] )
]; );
const CACHE_TIMEOUT = 3600; const CACHE_TIMEOUT = 3600;
const TEST_DETECT_PARAMETERS = [ public function getIcon() {
'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 . 'favicon.ico'; return self::URI . 'favicon.ico';
} }
public function getName() public function getName() {
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'Afficher les offres de véhicules disponible sur la recheche AutoJM': case 'Afficher les offres de véhicules disponible sur la recheche AutoJM':
return 'AutoJM | Recherche de véhicules'; return 'AutoJM | Recherche de véhicules';
break; break;
default: default:
return parent::getName(); return parent::getName();
} }
} }
public function getURI() public function collectData() {
{
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;
}
}
public function collectData()
{
// Get the number of result for this search // Get the number of result for this search
$search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false'; $search_url = self::URI . $this->getInput('url') . '&open=energy&onlyFilters=false';
// Set the header 'X-Requested-With' like the website does it // Set the header 'X-Requested-With' like the website does it
$header = [ $header = array(
'X-Requested-With: XMLHttpRequest' 'X-Requested-With: XMLHttpRequest'
]; );
// Get the JSON content of the form // Get the JSON content of the form
$json = getContents($search_url, $header); $json = getContents($search_url, $header);
@ -71,22 +51,23 @@ class AutoJMBridge extends BridgeAbstract
$data = json_decode($json); $data = json_decode($json);
$nb_results = $data->nbResults; $nb_results = $data->nbResults;
$total_pages = ceil($nb_results / 14); $total_pages = ceil($nb_results / 15);
// Limit the number of page to analyse to 10 // Limit the number of page to analyse to 10
for ($page = 1; $page <= $total_pages && $page <= 10; $page++) { for($page = 1; $page <= $total_pages && $page <= 10; $page++) {
// Get the result the next page // Get the result the next page
$html = $this->getResults($page); $html = $this->getResults($page);
// Go through every car of the search // Go through every car of the search
$list = $html->find('div[class*=card-car card-car--listing]'); $list = $html->find('div[class*=card-car card-car--listing]');
foreach ($list as $car) { foreach ($list as $car) {
// Get the info about the car offer // Get the info about the car offer
$image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src; $image = $car->find('div[class=card-car__header__img]', 0)->find('img', 0)->src;
// Decode HTML attribute JSON data // Decode HTML attribute JSON data
$car_data = json_decode(html_entity_decode($car->{'data-layer'})); $car_data = json_decode(html_entity_decode($car->{'data-layer'}));
$car_model = $car_data->title; $car_model = $car->{'data-title'} . ' ' . $car->{'data-suptitle'};
$availability = $car->find('div[class*=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext; $availability = $car->find('div[class=card-car__modalites]', 0)->find('div[class=col]', 0)->plaintext;
$warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext; $warranty = $car->find('div[data-type=WarrantyCard]', 0)->plaintext;
$discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0); $discount_html = $car->find('div[class=subtext vehicle_reference_element]', 0);
// Check if there is any discount info displayed // Check if there is any discount info displayed
@ -108,7 +89,7 @@ class AutoJMBridge extends BridgeAbstract
$transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext; $transmission = $car->find('span[data-cfg=vehicle__transmission]', 0)->plaintext;
$loa_html = $car->find('span[data-cfg=vehicle__loa]', 0); $loa_html = $car->find('span[data-cfg=vehicle__loa]', 0);
// Check if any LOA price is displayed // Check if any LOA price is displayed
if ($loa_html != null) { if($loa_html != null) {
$loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext; $loa_value = $car->find('span[data-cfg=vehicle__loa]', 0)->plaintext;
$loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>'; $loa = '<li>LOA : à partir de ' . $loa_value . ' / mois </li>';
} else { } else {
@ -116,7 +97,7 @@ class AutoJMBridge extends BridgeAbstract
} }
// Construct the new item // Construct the new item
$item = []; $item = array();
$item['title'] = $car_model; $item['title'] = $car_model;
$item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />' $item['content'] = '<p><img style="vertical-align:middle ; padding: 10px" src="' . $image . '" />'
. $car_model . '</p>'; . $car_model . '</p>';
@ -151,18 +132,4 @@ class AutoJMBridge extends BridgeAbstract
return $html; 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;
}
}
} }

View File

@ -1,7 +1,5 @@
<?php <?php
class AwwwardsBridge extends BridgeAbstract {
class AwwwardsBridge extends BridgeAbstract
{
const NAME = 'Awwwards'; const NAME = 'Awwwards';
const URI = 'https://www.awwwards.com/'; const URI = 'https://www.awwwards.com/';
const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards'; const DESCRIPTION = 'Fetches the latest ten sites of the day from Awwwards';
@ -12,14 +10,31 @@ class AwwwardsBridge extends BridgeAbstract
const SITEURI = 'https://www.awwwards.com/sites/'; const SITEURI = 'https://www.awwwards.com/sites/';
const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/'; const ASSETSURI = 'https://assets.awwwards.com/awards/media/cache/thumb_417_299/';
private $sites = []; private $sites = array();
public function collectData() public function getIcon() {
{ return 'https://www.awwwards.com/favicon.ico';
}
private function fetchSites() {
Debug::log('Fetching all sites');
$sites = getSimpleHTMLDOM(self::SITESURI);
Debug::log('Parsing all JSON data');
foreach($sites->find('li[data-model]') as $site) {
$decode = html_entity_decode($site->attr['data-model'],
ENT_QUOTES, 'utf-8');
$decode = json_decode($decode, true);
$this->sites[] = $decode;
}
}
public function collectData() {
$this->fetchSites(); $this->fetchSites();
foreach ($this->sites as $site) { Debug::log('Building RSS feed');
$item = []; foreach($this->sites as $site) {
$item = array();
$item['title'] = $site['title']; $item['title'] = $site['title'];
$item['timestamp'] = $site['createdAt']; $item['timestamp'] = $site['createdAt'];
$item['categories'] = $site['tags']; $item['categories'] = $site['tags'];
@ -32,28 +47,8 @@ class AwwwardsBridge extends BridgeAbstract
$this->items[] = $item; $this->items[] = $item;
if (count($this->items) >= 10) { if(count($this->items) >= 10)
break; 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;
}
}
} }

View File

@ -1,56 +1,52 @@
<?php <?php
class BAEBridge extends BridgeAbstract {
class BAEBridge extends BridgeAbstract
{
const MAINTAINER = 'couraudt'; const MAINTAINER = 'couraudt';
const NAME = 'Bourse Aux Equipiers Bridge'; const NAME = 'Bourse Aux Equipiers Bridge';
const URI = 'https://www.bourse-aux-equipiers.com'; const URI = 'https://www.bourse-aux-equipiers.com';
const DESCRIPTION = 'Returns the newest sailing offers.'; const DESCRIPTION = 'Returns the newest sailing offers.';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'keyword' => [ 'keyword' => array(
'name' => 'Filtrer par mots clés', 'name' => 'Filtrer par mots clés',
'title' => 'Entrez le mot clé à filtrer ici' 'title' => 'Entrez le mot clé à filtrer ici'
], ),
'type' => [ 'type' => array(
'name' => 'Type de recherche', 'name' => 'Type de recherche',
'title' => 'Afficher seuleument un certain type d\'annonce', 'title' => 'Afficher seuleument un certain type d\'annonce',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Toutes les annonces' => false, 'Toutes les annonces' => false,
'Les embarquements' => 'boat', 'Les embarquements' => 'boat',
'Les skippers' => 'skipper', 'Les skippers' => 'skipper',
'Les équipiers' => 'crew' 'Les équipiers' => 'crew'
] )
] )
] )
]; );
public function collectData() public function collectData() {
{
$url = $this->getURI(); $url = $this->getURI();
$html = getSimpleHTMLDOM($url); $html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
$annonces = $html->find('main article'); $annonces = $html->find('main article');
foreach ($annonces as $annonce) { foreach ($annonces as $annonce) {
$detail = $annonce->find('footer a', 0); $detail = $annonce->find('footer a', 0);
$htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href); $htmlDetail = getSimpleHTMLDOMCached(parent::getURI() . $detail->href);
if (!$htmlDetail) { if (!$htmlDetail)
continue; continue;
}
$item = []; $item = array();
$item['title'] = $annonce->find('header h2', 0)->plaintext; $item['title'] = $annonce->find('header h2', 0)->plaintext;
$item['uri'] = parent::getURI() . $detail->href; $item['uri'] = parent::getURI() . $detail->href;
$content = $htmlDetail->find('article p', 0)->innertext; $content = $htmlDetail->find('article p', 0)->innertext;
if (!empty($this->getInput('keyword'))) { if (!empty($this->getInput('keyword'))) {
$keyword = $this->removeAccents(strtolower($this->getInput('keyword'))); $keyword = $this->remove_accents(strtolower($this->getInput('keyword')));
$cleanTitle = $this->removeAccents(strtolower($item['title'])); $cleanTitle = $this->remove_accents(strtolower($item['title']));
if (strpos($cleanTitle, $keyword) === false) { if (strpos($cleanTitle, $keyword) === false) {
$cleanContent = $this->removeAccents(strtolower($content)); $cleanContent = $this->remove_accents(strtolower($content));
if (strpos($cleanContent, $keyword) === false) { if (strpos($cleanContent, $keyword) === false) {
continue; continue;
} }
@ -62,14 +58,13 @@ class BAEBridge extends BridgeAbstract
$item['content'] = defaultLinkTo($content, parent::getURI()); $item['content'] = defaultLinkTo($content, parent::getURI());
$image = $htmlDetail->find('#zoom', 0); $image = $htmlDetail->find('#zoom', 0);
if ($image) { if ($image) {
$item['enclosures'] = [parent::getURI() . $image->getAttribute('src')]; $item['enclosures'] = array(parent::getURI() . $image->getAttribute('src'));
} }
$this->items[] = $item; $this->items[] = $item;
} }
} }
public function getURI() public function getURI() {
{
$uri = parent::getURI(); $uri = parent::getURI();
if (!empty($this->getInput('type'))) { if (!empty($this->getInput('type'))) {
if ($this->getInput('type') == 'boat') { if ($this->getInput('type') == 'boat') {
@ -84,9 +79,8 @@ class BAEBridge extends BridgeAbstract
return $uri; return $uri;
} }
private function removeAccents($string) private function remove_accents($string) {
{ $chars = array(
$chars = [
// Decompositions for Latin-1 Supplement // Decompositions for Latin-1 Supplement
'ª' => 'a', 'º' => 'o', 'ª' => 'a', 'º' => 'o',
'À' => 'A', 'Á' => 'A', 'À' => 'A', 'Á' => 'A',
@ -260,7 +254,7 @@ class BAEBridge extends BridgeAbstract
'Ǚ' => 'U', 'ǚ' => 'u', 'Ǚ' => 'U', 'ǚ' => 'u',
// grave accent // grave accent
'Ǜ' => 'U', 'ǜ' => 'u', 'Ǜ' => 'U', 'ǜ' => 'u',
]; );
$string = strtr($string, $chars); $string = strtr($string, $chars);

View File

@ -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 '';
}
}
}

View File

@ -1,57 +1,55 @@
<?php <?php
class BadDragonBridge extends BridgeAbstract {
class BadDragonBridge extends BridgeAbstract
{
const NAME = 'Bad Dragon Bridge'; const NAME = 'Bad Dragon Bridge';
const URI = 'https://bad-dragon.com/'; const URI = 'https://bad-dragon.com/';
const CACHE_TIMEOUT = 300; // 5min const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Returns sales or new clearance items'; const DESCRIPTION = 'Returns sales or new clearance items';
const MAINTAINER = 'Roliga'; const MAINTAINER = 'Roliga';
const PARAMETERS = [ const PARAMETERS = array(
'Sales' => [ 'Sales' => array(
], ),
'Clearance' => [ 'Clearance' => array(
'ready_made' => [ 'ready_made' => array(
'name' => 'Ready Made', 'name' => 'Ready Made',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'flop' => [ 'flop' => array(
'name' => 'Flops', 'name' => 'Flops',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'skus' => [ 'skus' => array(
'name' => 'Products', 'name' => 'Products',
'exampleValue' => 'chanceflared, crackers', 'exampleValue' => 'chanceflared, crackers',
'title' => 'Comma separated list of product SKUs' 'title' => 'Comma separated list of product SKUs'
], ),
'onesize' => [ 'onesize' => array(
'name' => 'One-Size', 'name' => 'One-Size',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'mini' => [ 'mini' => array(
'name' => 'Mini', 'name' => 'Mini',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'small' => [ 'small' => array(
'name' => 'Small', 'name' => 'Small',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'medium' => [ 'medium' => array(
'name' => 'Medium', 'name' => 'Medium',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'large' => [ 'large' => array(
'name' => 'Large', 'name' => 'Large',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'extralarge' => [ 'extralarge' => array(
'name' => 'Extra Large', 'name' => 'Extra Large',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'category' => [ 'category' => array(
'name' => 'Category', 'name' => 'Category',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'All' => 'all', 'All' => 'all',
'Accessories' => 'accessories', 'Accessories' => 'accessories',
'Merchandise' => 'merchandise', 'Merchandise' => 'merchandise',
@ -61,50 +59,50 @@ class BadDragonBridge extends BridgeAbstract
'Lil\' Squirts' => 'shooter', 'Lil\' Squirts' => 'shooter',
'Lil\' Vibes' => 'vibrator', 'Lil\' Vibes' => 'vibrator',
'Wearables' => 'wearable' 'Wearables' => 'wearable'
], ),
'defaultValue' => 'all', 'defaultValue' => 'all',
], ),
'soft' => [ 'soft' => array(
'name' => 'Soft Firmness', 'name' => 'Soft Firmness',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'med_firm' => [ 'med_firm' => array(
'name' => 'Medium Firmness', 'name' => 'Medium Firmness',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'firm' => [ 'firm' => array(
'name' => 'Firm', 'name' => 'Firm',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'split' => [ 'split' => array(
'name' => 'Split Firmness', 'name' => 'Split Firmness',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'maxprice' => [ 'maxprice' => array(
'name' => 'Max Price', 'name' => 'Max Price',
'type' => 'number', 'type' => 'number',
'required' => true, 'required' => true,
'defaultValue' => 300 'defaultValue' => 300
], ),
'minprice' => [ 'minprice' => array(
'name' => 'Min Price', 'name' => 'Min Price',
'type' => 'number', 'type' => 'number',
'defaultValue' => 0 'defaultValue' => 0
], ),
'cumtube' => [ 'cumtube' => array(
'name' => 'Cumtube', 'name' => 'Cumtube',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'suctionCup' => [ 'suctionCup' => array(
'name' => 'Suction Cup', 'name' => 'Suction Cup',
'type' => 'checkbox' 'type' => 'checkbox'
], ),
'noAccessories' => [ 'noAccessories' => array(
'name' => 'No Accessories', 'name' => 'No Accessories',
'type' => 'checkbox' 'type' => 'checkbox'
] )
] )
]; );
/* /*
* This sets index $strFrom (or $strTo if set) in $outArr to 'on' if * This sets index $strFrom (or $strTo if set) in $outArr to 'on' if
@ -124,35 +122,32 @@ class BadDragonBridge extends BridgeAbstract
* [flop] => on * [flop] => on
* ) * )
* */ * */
private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) private function setParam($inArr, &$outArr, $param, $strFrom, $strTo = null) {
{ if(isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
if (isset($inArr[$param]) && in_array($strFrom, $inArr[$param])) {
$outArr[($strTo ?: $strFrom)] = 'on'; $outArr[($strTo ?: $strFrom)] = 'on';
} }
} }
public function detectParameters($url) public function detectParameters($url) {
{ $params = array();
$params = [];
// Sale // Sale
$regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/'; $regex = '/^(https?:\/\/)?bad-dragon\.com\/sales/';
if (preg_match($regex, $url, $matches) > 0) { if(preg_match($regex, $url, $matches) > 0) {
$params['context'] = 'Sales';
return $params; return $params;
} }
// Clearance // Clearance
$regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/'; $regex = '/^(https?:\/\/)?bad-dragon\.com\/shop\/clearance/';
if (preg_match($regex, $url, $matches) > 0) { if(preg_match($regex, $url, $matches) > 0) {
parse_str(parse_url($url, PHP_URL_QUERY), $urlParams); parse_str(parse_url($url, PHP_URL_QUERY), $urlParams);
$this->setParam($urlParams, $params, 'type', 'ready_made'); $this->setParam($urlParams, $params, 'type', 'ready_made');
$this->setParam($urlParams, $params, 'type', 'flop'); $this->setParam($urlParams, $params, 'type', 'flop');
if (isset($urlParams['skus'])) { if(isset($urlParams['skus'])) {
$skus = []; $skus = array();
foreach ($urlParams['skus'] as $sku) { foreach($urlParams['skus'] as $sku) {
is_string($sku) && $skus[] = $sku; is_string($sku) && $skus[] = $sku;
is_array($sku) && $skus[] = $sku[0]; is_array($sku) && $skus[] = $sku[0];
} }
@ -166,9 +161,9 @@ class BadDragonBridge extends BridgeAbstract
$this->setParam($urlParams, $params, 'sizes', 'large'); $this->setParam($urlParams, $params, 'sizes', 'large');
$this->setParam($urlParams, $params, 'sizes', 'extralarge'); $this->setParam($urlParams, $params, 'sizes', 'extralarge');
if (isset($urlParams['category'])) { if(isset($urlParams['category'])) {
$params['category'] = strtolower($urlParams['category']); $params['category'] = strtolower($urlParams['category']);
} else { } else{
$params['category'] = 'all'; $params['category'] = 'all';
} }
@ -177,7 +172,7 @@ class BadDragonBridge extends BridgeAbstract
$this->setParam($urlParams, $params, 'firmnessValues', 'firm'); $this->setParam($urlParams, $params, 'firmnessValues', 'firm');
$this->setParam($urlParams, $params, 'firmnessValues', 'split'); $this->setParam($urlParams, $params, 'firmnessValues', 'split');
if (isset($urlParams['price'])) { if(isset($urlParams['price'])) {
isset($urlParams['price']['max']) isset($urlParams['price']['max'])
&& $params['maxprice'] = $urlParams['price']['max']; && $params['maxprice'] = $urlParams['price']['max'];
isset($urlParams['price']['min']) isset($urlParams['price']['min'])
@ -193,7 +188,6 @@ class BadDragonBridge extends BridgeAbstract
isset($urlParams['noAccessories']) isset($urlParams['noAccessories'])
&& $urlParams['noAccessories'] === '1' && $urlParams['noAccessories'] === '1'
&& $params['noAccessories'] = 'on'; && $params['noAccessories'] = 'on';
$params['context'] = 'Clearance';
return $params; return $params;
} }
@ -201,9 +195,8 @@ class BadDragonBridge extends BridgeAbstract
return null; return null;
} }
public function getName() public function getName() {
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'Sales': case 'Sales':
return 'Bad Dragon Sales'; return 'Bad Dragon Sales';
case 'Clearance': case 'Clearance':
@ -213,9 +206,8 @@ class BadDragonBridge extends BridgeAbstract
} }
} }
public function getURI() public function getURI() {
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'Sales': case 'Sales':
return self::URI . 'sales'; return self::URI . 'sales';
case 'Clearance': case 'Clearance':
@ -225,14 +217,13 @@ class BadDragonBridge extends BridgeAbstract
} }
} }
public function collectData() public function collectData() {
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'Sales': case 'Sales':
$sales = json_decode(getContents(self::URI . 'api/sales')); $sales = json_decode(getContents(self::URI . 'api/sales'));
foreach ($sales as $sale) { foreach($sales as $sale) {
$item = []; $item = array();
$item['title'] = $sale->title; $item['title'] = $sale->title;
$item['timestamp'] = strtotime($sale->startDate); $item['timestamp'] = strtotime($sale->startDate);
@ -240,7 +231,7 @@ class BadDragonBridge extends BridgeAbstract
$item['uri'] = $this->getURI() . '/' . $sale->slug; $item['uri'] = $this->getURI() . '/' . $sale->slug;
$contentHTML = '<p><img src="' . $sale->image->url . '"></p>'; $contentHTML = '<p><img src="' . $sale->image->url . '"></p>';
if (isset($sale->endDate)) { if(isset($sale->endDate)) {
$contentHTML .= '<p><b>This promotion ends on ' $contentHTML .= '<p><b>This promotion ends on '
. gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate)) . gmdate('M j, Y \a\t g:i A T', strtotime($sale->endDate))
. '</b></p>'; . '</b></p>';
@ -249,8 +240,8 @@ class BadDragonBridge extends BridgeAbstract
} }
$ul = false; $ul = false;
$content = json_decode($sale->content); $content = json_decode($sale->content);
foreach ($content->blocks as $block) { foreach($content->blocks as $block) {
switch ($block->type) { switch($block->type) {
case 'header-one': case 'header-one':
$contentHTML .= '<h1>' . $block->text . '</h1>'; $contentHTML .= '<h1>' . $block->text . '</h1>';
break; break;
@ -261,14 +252,14 @@ class BadDragonBridge extends BridgeAbstract
$contentHTML .= '<h3>' . $block->text . '</h3>'; $contentHTML .= '<h3>' . $block->text . '</h3>';
break; break;
case 'unordered-list-item': case 'unordered-list-item':
if (!$ul) { if(!$ul) {
$contentHTML .= '<ul>'; $contentHTML .= '<ul>';
$ul = true; $ul = true;
} }
$contentHTML .= '<li>' . $block->text . '</li>'; $contentHTML .= '<li>' . $block->text . '</li>';
break; break;
default: default:
if ($ul) { if($ul) {
$contentHTML .= '</ul>'; $contentHTML .= '</ul>';
$ul = false; $ul = false;
} }
@ -284,18 +275,19 @@ class BadDragonBridge extends BridgeAbstract
case 'Clearance': case 'Clearance':
$toyData = json_decode(getContents($this->inputToURL(true))); $toyData = json_decode(getContents($this->inputToURL(true)));
$productList = json_decode(getContents(self::URI . 'api/inventory-toy/product-list')); $productList = json_decode(getContents(self::URI
. 'api/inventory-toy/product-list'));
foreach ($toyData->toys as $toy) { foreach($toyData->toys as $toy) {
$item = []; $item = array();
$item['uri'] = $this->getURI() $item['uri'] = $this->getURI()
. '#' . '#'
. $toy->id; . $toy->id;
$item['timestamp'] = strtotime($toy->created); $item['timestamp'] = strtotime($toy->created);
foreach ($productList as $product) { foreach($productList as $product) {
if ($product->sku == $toy->sku) { if($product->sku == $toy->sku) {
$item['title'] = $product->name; $item['title'] = $product->name;
break; break;
} }
@ -303,7 +295,7 @@ class BadDragonBridge extends BridgeAbstract
// images // images
$content = '<p>'; $content = '<p>';
foreach ($toy->images as $image) { foreach($toy->images as $image) {
$content .= '<a href="' $content .= '<a href="'
. $image->fullFilename . $image->fullFilename
. '"><img src="' . '"><img src="'
@ -326,44 +318,44 @@ class BadDragonBridge extends BridgeAbstract
. ($toy->cumtube ? 'Cumtube' : '') . ($toy->cumtube ? 'Cumtube' : '')
. ($toy->suction_cup || $toy->cumtube ? '' : 'None'); . ($toy->suction_cup || $toy->cumtube ? '' : 'None');
// firmness // firmness
$firmnessTexts = [ $firmnessTexts = array(
'2' => 'Extra soft', '2' => 'Extra soft',
'3' => 'Soft', '3' => 'Soft',
'5' => 'Medium', '5' => 'Medium',
'8' => 'Firm' '8' => 'Firm'
]; );
$firmnesses = explode('/', $toy->firmness); $firmnesses = explode('/', $toy->firmness);
if (count($firmnesses) === 2) { if(count($firmnesses) === 2) {
$content .= '<br /><b>Firmness:</b> ' $content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]] . $firmnessTexts[$firmnesses[0]]
. ', ' . ', '
. $firmnessTexts[$firmnesses[1]]; . $firmnessTexts[$firmnesses[1]];
} else { } else{
$content .= '<br /><b>Firmness:</b> ' $content .= '<br /><b>Firmness:</b> '
. $firmnessTexts[$firmnesses[0]]; . $firmnessTexts[$firmnesses[0]];
} }
// flop // flop
if ($toy->type === 'flop') { if($toy->type === 'flop') {
$content .= '<br /><b>Flop reason:</b> ' $content .= '<br /><b>Flop reason:</b> '
. $toy->flop_reason; . $toy->flop_reason;
} }
$content .= '</p>'; $content .= '</p>';
$item['content'] = $content; $item['content'] = $content;
$enclosures = []; $enclosures = array();
foreach ($toy->images as $image) { foreach($toy->images as $image) {
$enclosures[] = $image->fullFilename; $enclosures[] = $image->fullFilename;
} }
$item['enclosures'] = $enclosures; $item['enclosures'] = $enclosures;
$categories = []; $categories = array();
$categories[] = $toy->sku; $categories[] = $toy->sku;
$categories[] = $toy->type; $categories[] = $toy->type;
$categories[] = $toy->size; $categories[] = $toy->size;
if ($toy->cumtube) { if($toy->cumtube) {
$categories[] = 'cumtube'; $categories[] = 'cumtube';
} }
if ($toy->suction_cup) { if($toy->suction_cup) {
$categories[] = 'suction_cup'; $categories[] = 'suction_cup';
} }
$item['categories'] = $categories; $item['categories'] = $categories;
@ -374,8 +366,7 @@ class BadDragonBridge extends BridgeAbstract
} }
} }
private function inputToURL($api = false) private function inputToURL($api = false) {
{
$url = self::URI; $url = self::URI;
$url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?'); $url .= ($api ? 'api/inventory-toys?' : 'shop/clearance?');
@ -390,7 +381,7 @@ class BadDragonBridge extends BridgeAbstract
$url .= ($this->getInput('flop') ? '&type[]=flop' : ''); $url .= ($this->getInput('flop') ? '&type[]=flop' : '');
// Product names // Product names
foreach (array_filter(explode(',', $this->getInput('skus'))) as $sku) { foreach(array_filter(explode(',', $this->getInput('skus'))) as $sku) {
$url .= '&skus[]=' . urlencode(trim($sku)); $url .= '&skus[]=' . urlencode(trim($sku));
} }
@ -407,18 +398,18 @@ class BadDragonBridge extends BridgeAbstract
. urlencode($this->getInput('category')) : ''); . urlencode($this->getInput('category')) : '');
// Firmness // Firmness
if ($api) { if($api) {
$url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : ''); $url .= ($this->getInput('soft') ? '&firmnessValues[]=3' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : ''); $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=5' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : ''); $url .= ($this->getInput('firm') ? '&firmnessValues[]=8' : '');
if ($this->getInput('split')) { if($this->getInput('split')) {
$url .= '&firmnessValues[]=3/5'; $url .= '&firmnessValues[]=3/5';
$url .= '&firmnessValues[]=3/8'; $url .= '&firmnessValues[]=3/8';
$url .= '&firmnessValues[]=8/3'; $url .= '&firmnessValues[]=8/3';
$url .= '&firmnessValues[]=5/8'; $url .= '&firmnessValues[]=5/8';
$url .= '&firmnessValues[]=8/5'; $url .= '&firmnessValues[]=8/5';
} }
} else { } else{
$url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : ''); $url .= ($this->getInput('soft') ? '&firmnessValues[]=soft' : '');
$url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : ''); $url .= ($this->getInput('med_firm') ? '&firmnessValues[]=medium' : '');
$url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : ''); $url .= ($this->getInput('firm') ? '&firmnessValues[]=firm' : '');

View File

@ -1,126 +1,107 @@
<?php <?php
class BakaUpdatesMangaReleasesBridge extends BridgeAbstract {
class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
{
const NAME = 'Baka Updates Manga Releases'; const NAME = 'Baka Updates Manga Releases';
const URI = 'https://www.mangaupdates.com/'; const URI = 'https://www.mangaupdates.com/';
const DESCRIPTION = 'Get the latest series releases'; const DESCRIPTION = 'Get the latest series releases';
const MAINTAINER = 'fulmeek, KamaleiZestri'; const MAINTAINER = 'fulmeek, KamaleiZestri';
const PARAMETERS = [ const PARAMETERS = array(
'By series' => [ 'By series' => array(
'series_id' => [ 'series_id' => array(
'name' => 'Series ID', 'name' => 'Series ID',
'type' => 'number', 'type' => 'number',
'required' => true, 'required' => true,
'exampleValue' => '188066' 'exampleValue' => '188066'
] )
], ),
'By list' => [ 'By list' => array(
'list_id' => [ 'list_id' => array(
'name' => 'List ID and Type', 'name' => 'List ID and Type',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'exampleValue' => '4395&list=read' 'exampleValue' => '4395&list=read'
] )
] )
]; );
const LIMIT_COLS = 5; const LIMIT_COLS = 5;
const LIMIT_ITEMS = 10; const LIMIT_ITEMS = 10;
const RELEASES_URL = 'https://www.mangaupdates.com/releases.html'; const RELEASES_URL = 'https://www.mangaupdates.com/releases.html';
private $feedName = ''; private $feedName = '';
public function collectData() public function collectData() {
{ if($this -> queriedContext == 'By series')
if ($this -> queriedContext == 'By series') {
$this -> collectDataBySeries(); $this -> collectDataBySeries();
} else { //queriedContext == 'By list' else //queriedContext == 'By list'
$this -> collectDataByList(); $this -> collectDataByList();
} }
}
public function getURI() public function getURI(){
{ if($this -> queriedContext == 'By series') {
if ($this -> queriedContext == 'By series') {
$series_id = $this->getInput('series_id'); $series_id = $this->getInput('series_id');
if (!empty($series_id)) { if (!empty($series_id)) {
return self::URI . 'releases.html?search=' . $series_id . '&stype=series'; return self::URI . 'releases.html?search=' . $series_id . '&stype=series';
} }
} else { //queriedContext == 'By list' } else //queriedContext == 'By list'
return self::RELEASES_URL; return self::RELEASES_URL;
}
return self::URI; return self::URI;
} }
public function getName() public function getName(){
{ if(!empty($this->feedName)) {
if (!empty($this->feedName)) {
return $this->feedName . ' - ' . self::NAME; return $this->feedName . ' - ' . self::NAME;
} }
return parent::getName(); return parent::getName();
} }
private function getSanitizedHash($string) private function getSanitizedHash($string) {
{
return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string)))); return hash('sha1', preg_replace('/[^a-zA-Z0-9\-\.]/', '', ucwords(strtolower($string))));
} }
private function filterText($text) private function filterText($text) {
{
return rtrim($text, '* '); return rtrim($text, '* ');
} }
private function filterHTML($text) private function filterHTML($text) {
{
return $this->filterText(html_entity_decode($text)); return $this->filterText(html_entity_decode($text));
} }
private function findID($manga) private function findID($manga) {
{
// sometimes new series are on the release list that have no ID. just drop them. // sometimes new series are on the release list that have no ID. just drop them.
if (@$this -> filterHTML($manga -> find('a', 0) -> href) != null) { if(@$this -> filterHTML($manga -> find('a', 0) -> href) != null) {
preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match); preg_match('/id=([0-9]*)/', $this -> filterHTML($manga -> find('a', 0) -> href), $match);
return $match[1]; return $match[1];
} else { } else
return 0; return 0;
} }
}
private function collectDataBySeries() private function collectDataBySeries() {
{
$html = getSimpleHTMLDOM($this->getURI()); $html = getSimpleHTMLDOM($this->getURI());
// content is an unstructured pile of divs, ugly to parse // content is an unstructured pile of divs, ugly to parse
$cols = $html->find('div#main_content div.row > div.text'); $cols = $html->find('div#main_content div.row > div.text');
if (!$cols) { if (!$cols)
returnServerError('No releases'); returnServerError('No releases');
}
$rows = array_slice( $rows = array_slice(
array_chunk($cols, self::LIMIT_COLS), array_chunk($cols, self::LIMIT_COLS), 0, self::LIMIT_ITEMS
0,
self::LIMIT_ITEMS
); );
if (isset($rows[0][1])) { if (isset($rows[0][1])) {
$this->feedName = $this->filterHTML($rows[0][1]->plaintext); $this->feedName = $this->filterHTML($rows[0][1]->plaintext);
} }
foreach ($rows as $cols) { foreach($rows as $cols) {
if (count($cols) < self::LIMIT_COLS) { if (count($cols) < self::LIMIT_COLS) continue;
continue;
}
$item = []; $item = array();
$title = []; $title = array();
$item['content'] = ''; $item['content'] = '';
$objDate = $cols[0]; $objDate = $cols[0];
if ($objDate) { if ($objDate)
$item['timestamp'] = strtotime($objDate->plaintext); $item['timestamp'] = strtotime($objDate->plaintext);
}
$objTitle = $cols[1]; $objTitle = $cols[1];
if ($objTitle) { if ($objTitle) {
@ -129,14 +110,12 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
} }
$objVolume = $cols[2]; $objVolume = $cols[2];
if ($objVolume && !empty($objVolume->plaintext)) { if ($objVolume && !empty($objVolume->plaintext))
$title[] = 'Vol.' . $objVolume->plaintext; $title[] = 'Vol.' . $objVolume->plaintext;
}
$objChapter = $cols[3]; $objChapter = $cols[3];
if ($objChapter && !empty($objChapter->plaintext)) { if ($objChapter && !empty($objChapter->plaintext))
$title[] = 'Chp.' . $objChapter->plaintext; $title[] = 'Chp.' . $objChapter->plaintext;
}
$objAuthor = $cols[4]; $objAuthor = $cols[4];
if ($objAuthor && !empty($objAuthor->plaintext)) { if ($objAuthor && !empty($objAuthor->plaintext)) {
@ -152,10 +131,9 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
} }
} }
private function collectDataByList() private function collectDataByList() {
{
$this -> feedName = 'Releases'; $this -> feedName = 'Releases';
$list = []; $list = array();
$releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL); $releasesHTML = getSimpleHTMLDOM(self::RELEASES_URL);
@ -164,7 +142,7 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
//get ids of the manga that the user follows, //get ids of the manga that the user follows,
$parts = $listHTML -> find('table#ptable tr > td.pl'); $parts = $listHTML -> find('table#ptable tr > td.pl');
foreach ($parts as $part) { foreach($parts as $part) {
$list[] = $this -> findID($part); $list[] = $this -> findID($part);
} }
@ -172,15 +150,13 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
$cols = $releasesHTML -> find('div#main_content div.row > div.pbreak'); $cols = $releasesHTML -> find('div#main_content div.row > div.pbreak');
$rows = array_slice(array_chunk($cols, 3), 0); $rows = array_slice(array_chunk($cols, 3), 0);
foreach ($rows as $cols) { foreach($rows as $cols) {
//check if current manga is in user's list. //check if current manga is in user's list.
$id = $this -> findId($cols[0]); $id = $this -> findId($cols[0]);
if (!array_search($id, $list)) { if(!array_search($id, $list)) continue;
continue;
}
$item = []; $item = array();
$title = []; $title = array();
$item['content'] = ''; $item['content'] = '';
@ -191,9 +167,8 @@ class BakaUpdatesMangaReleasesBridge extends BridgeAbstract
} }
$objVolChap = $cols[1]; $objVolChap = $cols[1];
if ($objVolChap && !empty($objVolChap->plaintext)) { if ($objVolChap && !empty($objVolChap->plaintext))
$title[] = $this -> filterHTML($objVolChap -> innertext); $title[] = $this -> filterHTML($objVolChap -> innertext);
}
$objAuthor = $cols[2]; $objAuthor = $cols[2];
if ($objAuthor && !empty($objAuthor->plaintext)) { if ($objAuthor && !empty($objAuthor->plaintext)) {

View File

@ -1,123 +1,120 @@
<?php <?php
class BandcampBridge extends BridgeAbstract {
class BandcampBridge extends BridgeAbstract
{
const MAINTAINER = 'sebsauvage, Roliga'; const MAINTAINER = 'sebsauvage, Roliga';
const NAME = 'Bandcamp Bridge'; const NAME = 'Bandcamp Bridge';
const URI = 'https://bandcamp.com/'; const URI = 'https://bandcamp.com/';
const CACHE_TIMEOUT = 600; // 10min const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'New bandcamp releases by tag, band or album'; const DESCRIPTION = 'New bandcamp releases by tag, band or album';
const PARAMETERS = [ const PARAMETERS = array(
'By tag' => [ 'By tag' => array(
'tag' => [ 'tag' => array(
'name' => 'tag', 'name' => 'tag',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'exampleValue' => 'hip-hop-rap' 'exampleValue' => 'hip-hop-rap'
] )
], ),
'By band' => [ 'By band' => array(
'band' => [ 'band' => array(
'name' => 'band', 'name' => 'band',
'type' => 'text', 'type' => 'text',
'title' => 'Band name as seen in the band page URL', 'title' => 'Band name as seen in the band page URL',
'required' => true, 'required' => true,
'exampleValue' => 'aesoprock' 'exampleValue' => 'aesoprock'
], ),
'type' => [ 'type' => array(
'name' => 'Articles are', 'name' => 'Articles are',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Releases' => 'releases', 'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes', 'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks' 'Individual tracks' => 'tracks'
], ),
'defaultValue' => 'changes' 'defaultValue' => 'changes'
], ),
'limit' => [ 'limit' => array(
'name' => 'limit', 'name' => 'limit',
'type' => 'number', 'type' => 'number',
'required' => true, 'required' => true,
'title' => 'Number of releases to return', 'title' => 'Number of releases to return',
'defaultValue' => 5 'defaultValue' => 5
] )
], ),
'By label' => [ 'By label' => array(
'label' => [ 'label' => array(
'name' => 'label', 'name' => 'label',
'type' => 'text', 'type' => 'text',
'title' => 'label name as seen in the label page URL', 'title' => 'label name as seen in the label page URL',
'required' => true 'required' => true
], ),
'type' => [ 'type' => array(
'name' => 'Articles are', 'name' => 'Articles are',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Releases' => 'releases', 'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes', 'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks' 'Individual tracks' => 'tracks'
], ),
'defaultValue' => 'changes' 'defaultValue' => 'changes'
], ),
'limit' => [ 'limit' => array(
'name' => 'limit', 'name' => 'limit',
'type' => 'number', 'type' => 'number',
'title' => 'Number of releases to return', 'title' => 'Number of releases to return',
'defaultValue' => 5 'defaultValue' => 5
] )
], ),
'By album' => [ 'By album' => array(
'band' => [ 'band' => array(
'name' => 'band', 'name' => 'band',
'type' => 'text', 'type' => 'text',
'title' => 'Band name as seen in the album page URL', 'title' => 'Band name as seen in the album page URL',
'required' => true, 'required' => true,
'exampleValue' => 'aesoprock' 'exampleValue' => 'aesoprock'
], ),
'album' => [ 'album' => array(
'name' => 'album', 'name' => 'album',
'type' => 'text', 'type' => 'text',
'title' => 'Album name as seen in the album page URL', 'title' => 'Album name as seen in the album page URL',
'required' => true, 'required' => true,
'exampleValue' => 'appleseed' 'exampleValue' => 'appleseed'
], ),
'type' => [ 'type' => array(
'name' => 'Articles are', 'name' => 'Articles are',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Releases' => 'releases', 'Releases' => 'releases',
'Releases, new one when track list changes' => 'changes', 'Releases, new one when track list changes' => 'changes',
'Individual tracks' => 'tracks' 'Individual tracks' => 'tracks'
], ),
'defaultValue' => 'tracks' 'defaultValue' => 'tracks'
] )
] )
]; );
const IMGURI = 'https://f4.bcbits.com/'; const IMGURI = 'https://f4.bcbits.com/';
const IMGSIZE_300PX = 23; const IMGSIZE_300PX = 23;
const IMGSIZE_700PX = 16; const IMGSIZE_700PX = 16;
private $feedName; private $feedName;
public function getIcon() public function getIcon() {
{
return 'https://s4.bcbits.com/img/bc_favicon.ico'; return 'https://s4.bcbits.com/img/bc_favicon.ico';
} }
public function collectData() public function collectData(){
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'By tag': case 'By tag':
$url = self::URI . 'api/hub/1/dig_deeper'; $url = self::URI . 'api/hub/1/dig_deeper';
$data = $this->buildRequestJson(); $data = $this->buildRequestJson();
$header = [ $header = array(
'Content-Type: application/json', 'Content-Type: application/json',
'Content-Length: ' . strlen($data), 'Content-Length: ' . strlen($data)
]; );
$opts = [ $opts = array(
CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $data, CURLOPT_POSTFIELDS => $data
]; );
$content = getContents($url, $header, $opts); $content = getContents($url, $header, $opts);
$json = json_decode($content); $json = json_decode($content);
@ -142,13 +139,13 @@ class BandcampBridge extends BridgeAbstract
$small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX); $small_img = $this->getImageUrl($entry->art_id, self::IMGSIZE_300PX);
$img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX); $img = $this->getImageUrl($entry->art_id, self::IMGSIZE_700PX);
$item = [ $item = array(
'uri' => $url, 'uri' => $url,
'author' => $full_artist, 'author' => $full_artist,
'title' => $full_title 'title' => $full_title
]; );
$item['content'] = "<img src='$small_img' /><br/>$full_title"; $item['content'] = "<img src='$small_img' /><br/>$full_title";
$item['enclosures'] = [$img]; $item['enclosures'] = array($img);
$this->items[] = $item; $this->items[] = $item;
} }
break; break;
@ -164,47 +161,45 @@ class BandcampBridge extends BridgeAbstract
} }
$regex = '/band_id=(\d+)/'; $regex = '/band_id=(\d+)/';
if (preg_match($regex, $html, $matches) == false) { if(preg_match($regex, $html, $matches) == false)
returnServerError('Unable to find band ID on: ' . $this->getURI()); returnServerError('Unable to find band ID on: ' . $this->getURI());
}
$band_id = $matches[1]; $band_id = $matches[1];
$tralbums = []; $tralbums = array();
switch ($this->queriedContext) { switch($this->queriedContext) {
case 'By band': case 'By band':
case 'By label': case 'By label':
$query_data = [ $query_data = array(
'band_id' => $band_id 'band_id' => $band_id
]; );
$band_data = $this->apiGet('mobile/22/band_details', $query_data); $band_data = $this->apiGet('mobile/22/band_details', $query_data);
$num_albums = min(count($band_data->discography), $this->getInput('limit')); $num_albums = min(count($band_data->discography), $this->getInput('limit'));
for ($i = 0; $i < $num_albums; $i++) { for($i = 0; $i < $num_albums; $i++) {
$album_basic_data = $band_data->discography[$i]; $album_basic_data = $band_data->discography[$i];
// 'a' or 't' for albums and individual tracks respectively // 'a' or 't' for albums and individual tracks respectively
$tralbum_type = substr($album_basic_data->item_type, 0, 1); $tralbum_type = substr($album_basic_data->item_type, 0, 1);
$query_data = [ $query_data = array(
'band_id' => $band_id, 'band_id' => $band_id,
'tralbum_type' => $tralbum_type, 'tralbum_type' => $tralbum_type,
'tralbum_id' => $album_basic_data->item_id 'tralbum_id' => $album_basic_data->item_id
]; );
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
} }
break; break;
case 'By album': case 'By album':
$regex = '/album=(\d+)/'; $regex = '/album=(\d+)/';
if (preg_match($regex, $html, $matches) == false) { if(preg_match($regex, $html, $matches) == false)
returnServerError('Unable to find album ID on: ' . $this->getURI()); returnServerError('Unable to find album ID on: ' . $this->getURI());
}
$album_id = $matches[1]; $album_id = $matches[1];
$query_data = [ $query_data = array(
'band_id' => $band_id, 'band_id' => $band_id,
'tralbum_type' => 'a', 'tralbum_type' => 'a',
'tralbum_id' => $album_id 'tralbum_id' => $album_id
]; );
$tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data); $tralbums[] = $this->apiGet('mobile/22/tralbum_details', $query_data);
break; break;
@ -213,11 +208,11 @@ class BandcampBridge extends BridgeAbstract
foreach ($tralbums as $tralbum_data) { foreach ($tralbums as $tralbum_data) {
if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') { if ($tralbum_data->type === 'a' && $this->getInput('type') === 'tracks') {
foreach ($tralbum_data->tracks as $track) { foreach ($tralbum_data->tracks as $track) {
$query_data = [ $query_data = array(
'band_id' => $band_id, 'band_id' => $band_id,
'tralbum_type' => 't', 'tralbum_type' => 't',
'tralbum_id' => $track->track_id 'tralbum_id' => $track->track_id
]; );
$track_data = $this->apiGet('mobile/22/tralbum_details', $query_data); $track_data = $this->apiGet('mobile/22/tralbum_details', $query_data);
$this->items[] = $this->buildTralbumItem($track_data); $this->items[] = $this->buildTralbumItem($track_data);
@ -230,8 +225,7 @@ class BandcampBridge extends BridgeAbstract
} }
} }
private function buildTralbumItem($tralbum_data) private function buildTralbumItem($tralbum_data){
{
$band_data = $tralbum_data->band; $band_data = $tralbum_data->band;
// Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER) // Format title like: ARTIST - ALBUM/TRACK (OPTIONAL RELEASER)
@ -256,15 +250,15 @@ class BandcampBridge extends BridgeAbstract
$small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX); $small_img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_300PX);
$img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX); $img = $this->getImageUrl($tralbum_data->art_id, self::IMGSIZE_700PX);
$item = [ $item = array(
'uri' => $tralbum_data->bandcamp_url, 'uri' => $tralbum_data->bandcamp_url,
'author' => $full_artist, 'author' => $full_artist,
'title' => $full_title, 'title' => $full_title,
'enclosures' => [$img], 'enclosures' => array($img),
'timestamp' => $tralbum_data->release_date 'timestamp' => $tralbum_data->release_date
]; );
$item['categories'] = []; $item['categories'] = array();
foreach ($tralbum_data->tags as $tag) { foreach ($tralbum_data->tags as $tag) {
$item['categories'][] = $tag->norm_name; $item['categories'][] = $tag->norm_name;
} }
@ -295,35 +289,29 @@ class BandcampBridge extends BridgeAbstract
return $item; return $item;
} }
private function buildRequestJson() private function buildRequestJson(){
{ $requestJson = array(
$requestJson = [
'tag' => $this->getInput('tag'), 'tag' => $this->getInput('tag'),
'page' => 1, 'page' => 1,
'sort' => 'date' 'sort' => 'date'
]; );
return json_encode($requestJson); return json_encode($requestJson);
} }
private function getImageUrl($id, $size) private function getImageUrl($id, $size){
{
return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg'; return self::IMGURI . 'img/a' . $id . '_' . $size . '.jpg';
} }
private function apiGet($endpoint, $query_data) private function apiGet($endpoint, $query_data) {
{
$url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data); $url = self::URI . 'api/' . $endpoint . '?' . http_build_query($query_data);
// todo: 429 Too Many Requests happens a lot $data = json_decode(getContents($url));
$response = getContents($url);
$data = json_decode($response);
return $data; return $data;
} }
public function getURI() public function getURI(){
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'By tag': case 'By tag':
if (!is_null($this->getInput('tag'))) { if(!is_null($this->getInput('tag'))) {
return self::URI return self::URI
. 'tag/' . 'tag/'
. urlencode($this->getInput('tag')) . urlencode($this->getInput('tag'))
@ -331,21 +319,21 @@ class BandcampBridge extends BridgeAbstract
} }
break; break;
case 'By label': case 'By label':
if (!is_null($this->getInput('label'))) { if(!is_null($this->getInput('label'))) {
return 'https://' return 'https://'
. $this->getInput('label') . $this->getInput('label')
. '.bandcamp.com/music'; . '.bandcamp.com/music';
} }
break; break;
case 'By band': case 'By band':
if (!is_null($this->getInput('band'))) { if(!is_null($this->getInput('band'))) {
return 'https://' return 'https://'
. $this->getInput('band') . $this->getInput('band')
. '.bandcamp.com/music'; . '.bandcamp.com/music';
} }
break; break;
case 'By album': case 'By album':
if (!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) { if(!is_null($this->getInput('band')) && !is_null($this->getInput('album'))) {
return 'https://' return 'https://'
. $this->getInput('band') . $this->getInput('band')
. '.bandcamp.com/album/' . '.bandcamp.com/album/'
@ -357,32 +345,31 @@ class BandcampBridge extends BridgeAbstract
return parent::getURI(); return parent::getURI();
} }
public function getName() public function getName(){
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'By tag': case 'By tag':
if (!is_null($this->getInput('tag'))) { if(!is_null($this->getInput('tag'))) {
return $this->getInput('tag') . ' - Bandcamp Tag'; return $this->getInput('tag') . ' - Bandcamp Tag';
} }
break; break;
case 'By band': case 'By band':
if (isset($this->feedName)) { if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Band'; return $this->feedName . ' - Bandcamp Band';
} elseif (!is_null($this->getInput('band'))) { } elseif(!is_null($this->getInput('band'))) {
return $this->getInput('band') . ' - Bandcamp Band'; return $this->getInput('band') . ' - Bandcamp Band';
} }
break; break;
case 'By label': case 'By label':
if (isset($this->feedName)) { if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Label'; return $this->feedName . ' - Bandcamp Label';
} elseif (!is_null($this->getInput('label'))) { } elseif(!is_null($this->getInput('label'))) {
return $this->getInput('label') . ' - Bandcamp Label'; return $this->getInput('label') . ' - Bandcamp Label';
} }
break; break;
case 'By album': case 'By album':
if (isset($this->feedName)) { if(isset($this->feedName)) {
return $this->feedName . ' - Bandcamp Album'; return $this->feedName . ' - Bandcamp Album';
} elseif (!is_null($this->getInput('album'))) { } elseif(!is_null($this->getInput('album'))) {
return $this->getInput('album') . ' - Bandcamp Album'; return $this->getInput('album') . ' - Bandcamp Album';
} }
break; break;
@ -391,30 +378,26 @@ class BandcampBridge extends BridgeAbstract
return parent::getName(); return parent::getName();
} }
public function detectParameters($url) public function detectParameters($url) {
{ $params = array();
$params = [];
// By tag // By tag
$regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/'; $regex = '/^(https?:\/\/)?bandcamp\.com\/tag\/([^\/.&?\n]+)/';
if (preg_match($regex, $url, $matches) > 0) { if(preg_match($regex, $url, $matches) > 0) {
$params['context'] = 'By tag';
$params['tag'] = urldecode($matches[2]); $params['tag'] = urldecode($matches[2]);
return $params; return $params;
} }
// By band // By band
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/'; $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com/';
if (preg_match($regex, $url, $matches) > 0) { if(preg_match($regex, $url, $matches) > 0) {
$params['context'] = 'By band';
$params['band'] = urldecode($matches[2]); $params['band'] = urldecode($matches[2]);
return $params; return $params;
} }
// By album // By album
$regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/'; $regex = '/^(https?:\/\/)?([^\/.&?\n]+?)\.bandcamp\.com\/album\/([^\/.&?\n]+)/';
if (preg_match($regex, $url, $matches) > 0) { if(preg_match($regex, $url, $matches) > 0) {
$params['context'] = 'By album';
$params['band'] = urldecode($matches[2]); $params['band'] = urldecode($matches[2]);
$params['album'] = urldecode($matches[3]); $params['album'] = urldecode($matches[3]);
return $params; return $params;

View File

@ -1,18 +1,16 @@
<?php <?php
class BandcampDailyBridge extends BridgeAbstract {
class BandcampDailyBridge extends BridgeAbstract
{
const NAME = 'Bandcamp Daily Bridge'; const NAME = 'Bandcamp Daily Bridge';
const URI = 'https://daily.bandcamp.com'; const URI = 'https://daily.bandcamp.com';
const DESCRIPTION = 'Returns newest articles'; const DESCRIPTION = 'Returns newest articles';
const MAINTAINER = 'VerifiedJoseph'; const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [ const PARAMETERS = array(
'Latest articles' => [], 'Latest articles' => array(),
'Best of' => [ 'Best of' => array(
'best-content' => [ 'content' => array(
'name' => 'content', 'name' => 'content',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Best Ambient' => 'best-ambient', 'Best Ambient' => 'best-ambient',
'Best Beat Tapes' => 'best-beat-tapes', 'Best Beat Tapes' => 'best-beat-tapes',
'Best Dance 12\'s' => 'best-dance-12s', 'Best Dance 12\'s' => 'best-dance-12s',
@ -25,15 +23,15 @@ class BandcampDailyBridge extends BridgeAbstract
'Best Punk' => 'best-punk', 'Best Punk' => 'best-punk',
'Best Reissues' => 'best-reissues', 'Best Reissues' => 'best-reissues',
'Best Soul' => 'best-soul', 'Best Soul' => 'best-soul',
], ),
'defaultValue' => 'best-ambient', 'defaultValue' => 'best-ambient',
], ),
], ),
'Genres' => [ 'Genres' => array(
'genres-content' => [ 'content' => array(
'name' => 'content', 'name' => 'content',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Acoustic' => 'genres/acoustic', 'Acoustic' => 'genres/acoustic',
'Alternative' => 'genres/alternative', 'Alternative' => 'genres/alternative',
'Ambient' => 'genres/ambient', 'Ambient' => 'genres/ambient',
@ -59,15 +57,15 @@ class BandcampDailyBridge extends BridgeAbstract
'Soundtrack' => 'genres/soundtrack', 'Soundtrack' => 'genres/soundtrack',
'Spoken Word' => 'genres/spoken-word', 'Spoken Word' => 'genres/spoken-word',
'World' => 'genres/world', 'World' => 'genres/world',
], ),
'defaultValue' => 'genres/acoustic', 'defaultValue' => 'genres/acoustic',
], ),
], ),
'Franchises' => [ 'Franchises' => array(
'franchises-content' => [ 'content' => array(
'name' => 'content', 'name' => 'content',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Lists' => 'lists', 'Lists' => 'lists',
'Features' => 'features', 'Features' => 'features',
'Album of the Day' => 'album-of-the-day', 'Album of the Day' => 'album-of-the-day',
@ -83,28 +81,29 @@ class BandcampDailyBridge extends BridgeAbstract
'Scene Report' => 'scene-report', 'Scene Report' => 'scene-report',
'Seven Essential Releases' => 'seven-essential-releases', 'Seven Essential Releases' => 'seven-essential-releases',
'The Merch Table' => 'the-merch-table', 'The Merch Table' => 'the-merch-table',
], ),
'defaultValue' => 'lists', 'defaultValue' => 'lists',
], ),
] )
]; );
const CACHE_TIMEOUT = 3600; // 1 hour const CACHE_TIMEOUT = 3600; // 1 hour
public function collectData() public function collectData() {
{ $html = getSimpleHTMLDOM($this->getURI())
$html = getSimpleHTMLDOM($this->getURI()); or returnServerError('Could not request: ' . $this->getURI());
$html = defaultLinkTo($html, self::URI); $html = defaultLinkTo($html, self::URI);
$articles = $html->find('articles-list', 0); $articles = $html->find('articles-list', 0);
foreach ($articles->find('div.list-article') as $index => $article) { foreach($articles->find('div.list-article') as $index => $article) {
$item = []; $item = array();
$articlePath = $article->find('a.title', 0)->href; $articlePath = $article->find('a.title', 0)->href;
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600); $articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600)
or returnServerError('Could not request: ' . $articlePath);
$item['uri'] = $articlePath; $item['uri'] = $articlePath;
$item['title'] = $articlePageHtml->find('article-title', 0)->innertext; $item['title'] = $articlePageHtml->find('article-title', 0)->innertext;
@ -127,36 +126,30 @@ class BandcampDailyBridge extends BridgeAbstract
} }
} }
public function getURI() public function getURI() {
{ switch($this->queriedContext) {
switch ($this->queriedContext) {
case 'Latest articles': case 'Latest articles':
return self::URI . '/latest'; return self::URI . '/latest';
case 'Best of': case 'Best of':
case 'Genres': case 'Genres':
case 'Franchises': case 'Franchises':
// TODO Switch to array_key_first once php >= 7.3 return self::URI . '/' . $this->getInput('content');
$contentKey = key(self::PARAMETERS[$this->queriedContext]);
return self::URI . '/' . $this->getInput($contentKey);
default: default:
return parent::getURI(); return parent::getURI();
} }
} }
public function getName() public function getName() {
{ if ($this->queriedContext === 'Latest articles') {
switch ($this->queriedContext) {
case 'Latest articles':
return $this->queriedContext . ' - Bandcamp Daily'; return $this->queriedContext . ' - Bandcamp Daily';
case 'Best of': }
case 'Genres':
case 'Franchises': if (!is_null($this->getInput('content'))) {
$contentKey = array_key_first(self::PARAMETERS[$this->queriedContext]); $contentValues = array_flip(self::PARAMETERS[$this->queriedContext]['content']['values']);
$contentValues = array_flip(self::PARAMETERS[$this->queriedContext][$contentKey]['values']);
return $contentValues[$this->getInput('content')] . ' - Bandcamp Daily';
}
return $contentValues[$this->getInput($contentKey)] . ' - Bandcamp Daily';
default:
return parent::getName(); return parent::getName();
} }
}
} }

View File

@ -1,22 +1,20 @@
<?php <?php
class BastaBridge extends BridgeAbstract {
class BastaBridge extends BridgeAbstract
{
const MAINTAINER = 'qwertygc'; const MAINTAINER = 'qwertygc';
const NAME = 'Bastamag Bridge'; const NAME = 'Bastamag Bridge';
const URI = 'https://www.bastamag.net/'; const URI = 'https://www.bastamag.net/';
const CACHE_TIMEOUT = 7200; // 2h const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Returns the newest articles.'; const DESCRIPTION = 'Returns the newest articles.';
public function collectData() public function collectData(){
{
$html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend'); $html = getSimpleHTMLDOM(self::URI . 'spip.php?page=backend');
$limit = 0; $limit = 0;
foreach ($html->find('item') as $element) { foreach($html->find('item') as $element) {
if ($limit < 10) { if($limit < 10) {
$item = []; $item = array();
$item['title'] = $element->find('title', 0)->innertext; $item['title'] = $element->find('title', 0)->innertext;
$item['uri'] = $element->find('guid', 0)->plaintext; $item['uri'] = $element->find('guid', 0)->plaintext;
$item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext); $item['timestamp'] = strtotime($element->find('dc:date', 0)->plaintext);

View File

@ -1,34 +1,41 @@
<?php <?php
class BinanceBridge extends BridgeAbstract {
class BinanceBridge extends BridgeAbstract
{
const NAME = 'Binance Blog'; const NAME = 'Binance Blog';
const URI = 'https://www.binance.com/en/blog'; const URI = 'https://www.binance.com/en/blog';
const DESCRIPTION = 'Subscribe to the Binance blog.'; const DESCRIPTION = 'Subscribe to the Binance blog.';
const MAINTAINER = 'thefranke'; const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
public function collectData() public function getIcon() {
{
$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'; return 'https://bin.bnbstatic.com/static/images/common/favicon.ico';
} }
public function collectData() {
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not fetch Binance blog data.');
$appData = $html->find('script[id="__APP_DATA"]');
$appDataJson = json_decode($appData[0]->innertext);
foreach($appDataJson->pageData->redux->blogList->blogList as $element) {
$date = $element->postTime;
$abstract = $element->brief;
$uri = self::URI . '/' . $element->lang . '/blog/' . $element->idStr;
$title = $element->title;
$content = $element->content;
$item = array();
$item['title'] = $title;
$item['uri'] = $uri;
$item['timestamp'] = substr($date, 0, -3);
$item['author'] = 'Binance';
$item['content'] = $content;
$this->items[] = $item;
if (count($this->items) >= 10)
break;
}
}
} }

View File

@ -1,24 +1,23 @@
<?php <?php
class BlaguesDeMerdeBridge extends BridgeAbstract {
class BlaguesDeMerdeBridge extends BridgeAbstract
{
const MAINTAINER = 'superbaillot.net, logmanoriginal'; const MAINTAINER = 'superbaillot.net, logmanoriginal';
const NAME = 'Blagues De Merde'; const NAME = 'Blagues De Merde';
const URI = 'http://www.blaguesdemerde.fr/'; const URI = 'http://www.blaguesdemerde.fr/';
const CACHE_TIMEOUT = 7200; // 2h const CACHE_TIMEOUT = 7200; // 2h
const DESCRIPTION = 'Blagues De Merde'; const DESCRIPTION = 'Blagues De Merde';
public function getIcon() public function getIcon() {
{
return self::URI . 'assets/img/favicon.ico'; return self::URI . 'assets/img/favicon.ico';
} }
public function collectData() public function collectData(){
{
$html = getSimpleHTMLDOM(self::URI); $html = getSimpleHTMLDOM(self::URI);
foreach ($html->find('div.blague') as $element) { foreach($html->find('div.blague') as $element) {
$item = [];
$item = array();
$item['uri'] = static::URI . '#' . $element->id; $item['uri'] = static::URI . '#' . $element->id;
$item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext; $item['author'] = $element->find('div[class="blague-footer"] p strong', 0)->plaintext;
@ -39,6 +38,8 @@ class BlaguesDeMerdeBridge extends BridgeAbstract
$item['timestamp'] = strtotime($matches[1]); $item['timestamp'] = strtotime($matches[1]);
$this->items[] = $item; $this->items[] = $item;
} }
} }
} }

View File

@ -1,22 +1,16 @@
<?php <?php
class BleepingComputerBridge extends FeedExpander {
class BleepingComputerBridge extends FeedExpander
{
const MAINTAINER = 'csisoap'; const MAINTAINER = 'csisoap';
const NAME = 'Bleeping Computer'; const NAME = 'Bleeping Computer';
const URI = 'https://www.bleepingcomputer.com/'; const URI = 'https://www.bleepingcomputer.com/';
const DESCRIPTION = 'Returns the newest articles.'; const DESCRIPTION = 'Returns the newest articles.';
public function collectData() protected function parseItem($item){
{ $item = parent::parseItem($item);
$feed = static::URI . 'feed/';
$this->collectExpandableDatas($feed);
}
protected function parseItem(array $item)
{
$article_html = getSimpleHTMLDOMCached($item['uri']); $article_html = getSimpleHTMLDOMCached($item['uri']);
if (!$article_html) { if(!$article_html) {
$item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>'; $item['content'] .= '<p><em>Could not request ' . $this->getName() . ': ' . $item['uri'] . '</em></p>';
return $item; return $item;
} }
@ -27,4 +21,9 @@ class BleepingComputerBridge extends FeedExpander
return $item; return $item;
} }
public function collectData(){
$feed = static::URI . 'feed/';
$this->collectExpandableDatas($feed);
}
} }

View File

@ -1,17 +1,17 @@
<?php <?php
class BlizzardNewsBridge extends BridgeAbstract class BlizzardNewsBridge extends XPathAbstract {
{
const NAME = 'Blizzard News'; const NAME = 'Blizzard News';
const URI = 'https://news.blizzard.com'; const URI = 'https://news.blizzard.com';
const DESCRIPTION = 'Blizzard (game company) newsfeed'; const DESCRIPTION = 'Blizzard (game company) newsfeed';
const MAINTAINER = 'Niehztog'; const MAINTAINER = 'Niehztog';
const PARAMETERS = [ const PARAMETERS = array(
'' => [ '' => array(
'locale' => [ 'locale' => array(
'name' => 'Language', 'name' => 'Language',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Deutsch' => 'de-de', 'Deutsch' => 'de-de',
'English (EU)' => 'en-gb', 'English (EU)' => 'en-gb',
'English (US)' => 'en-us', 'English (US)' => 'en-us',
@ -27,81 +27,34 @@ class BlizzardNewsBridge extends BridgeAbstract
'ภาษาไทย' => 'th-th', 'ภาษาไทย' => 'th-th',
'简体中文' => 'zh-cn', '简体中文' => 'zh-cn',
'繁體中文' => 'zh-tw' '繁體中文' => 'zh-tw'
], ),
'defaultValue' => 'en-us', 'defaultValue' => 'en-us',
'title' => 'Select your language' 'title' => 'Select your language'
] )
] )
]; );
const CACHE_TIMEOUT = 3600; const CACHE_TIMEOUT = 3600;
private const PRODUCT_IDS = [ const XPATH_EXPRESSION_ITEM = '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article';
'blt525c436e4a1b0a97', const XPATH_EXPRESSION_ITEM_TITLE = './/div/div[2]/h2';
'blt54fbd3787a705054', const XPATH_EXPRESSION_ITEM_CONTENT = './/div[@class="ArticleListItem-description"]/div[@class="h6"]';
'blt2031aef34200656d', const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ArticleLink ArticleLink"]/@href';
'blt795c314400d7ded9', const XPATH_EXPRESSION_ITEM_AUTHOR = '';
'blt5cfc6affa3ca0638', const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp';
'blt2e50e1521bb84dc6', const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/div[@class="ArticleListItem-image"]/@style';
'blt376fb94931906b6f', const XPATH_EXPRESSION_ITEM_CATEGORIES = './/div[@class="ArticleListItem-label"]';
'blt81d46fcb05ab8811', const SETTING_FIX_ENCODING = true;
'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) * Source Web page URL (should provide either HTML or XML content)
* @return string * @return string
*/ */
private function getSourceUrl(): string protected function getSourceUrl(){
{
$locale = $this->getInput('locale'); $locale = $this->getInput('locale');
if ('zh-cn' === $locale) { if('zh-cn' === $locale) {
$baseUrl = 'https://cn.news.blizzard.com' . self::API_PATH; return 'https://cn.news.blizzard.com';
} else {
$baseUrl = 'https://news.blizzard.com/' . $locale . self::API_PATH;
} }
return $baseUrl .= http_build_query([ return 'https://news.blizzard.com/' . $locale;
'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;
} }
} }

View File

@ -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;
}
}

View File

@ -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

View File

@ -1,74 +1,69 @@
<?php <?php
require_once('DanbooruBridge.php');
class BooruprojectBridge extends DanbooruBridge {
class BooruprojectBridge extends DanbooruBridge
{
const MAINTAINER = 'mitsukarenai'; const MAINTAINER = 'mitsukarenai';
const NAME = 'Booruproject'; const NAME = 'Booruproject';
const URI = 'https://booru.org/'; const URI = 'https://booru.org/';
const DESCRIPTION = 'Returns images from given page of booruproject'; const DESCRIPTION = 'Returns images from given page of booruproject';
const PARAMETERS = [ const PARAMETERS = array(
'global' => [ 'global' => array(
'p' => [ 'p' => array(
'name' => 'page', 'name' => 'page',
'defaultValue' => 0, 'defaultValue' => 0,
'type' => 'number' 'type' => 'number'
], ),
't' => [ 't' => array(
'name' => 'tags', 'name' => 'tags',
'required' => true, 'required' => true,
'exampleValue' => 'tagme', 'exampleValue' => 'tagme',
'title' => 'Use "all" to get all posts' 'title' => 'Use "all" to get all posts'
] )
], ),
'Booru subdomain (subdomain.booru.org)' => [ 'Booru subdomain (subdomain.booru.org)' => array(
'i' => [ 'i' => array(
'name' => 'Subdomain', 'name' => 'Subdomain',
'required' => true, 'required' => true,
'exampleValue' => 'rm' 'exampleValue' => 'rm'
] )
] )
]; );
const PATHTODATA = '.thumb'; const PATHTODATA = '.thumb';
const IDATTRIBUTE = 'id'; const IDATTRIBUTE = 'id';
const TAGATTRIBUTE = 'title'; const TAGATTRIBUTE = 'title';
const PIDBYPAGE = 20; const PIDBYPAGE = 20;
protected function getFullURI() protected function getFullURI(){
{
return $this->getURI() return $this->getURI()
. 'index.php?page=post&s=list&pid=' . 'index.php?page=post&s=list&pid='
. ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '') . ($this->getInput('p') ? ($this->getInput('p') - 1) * static::PIDBYPAGE : '')
. '&tags=' . urlencode($this->getInput('t')); . '&tags=' . urlencode($this->getInput('t'));
} }
protected function getTags($element) protected function getTags($element){
{
$tags = parent::getTags($element); $tags = parent::getTags($element);
$tags = explode(' ', $tags); $tags = explode(' ', $tags);
// Remove statistics from the tags list (identified by colon) // Remove statistics from the tags list (identified by colon)
foreach ($tags as $key => $tag) { foreach($tags as $key => $tag) {
if (strpos($tag, ':') !== false) { if(strpos($tag, ':') !== false) unset($tags[$key]);
unset($tags[$key]);
}
} }
return implode(' ', $tags); return implode(' ', $tags);
} }
public function getURI() public function getURI(){
{ if(!is_null($this->getInput('i'))) {
if (!is_null($this->getInput('i'))) {
return 'https://' . $this->getInput('i') . '.booru.org/'; return 'https://' . $this->getInput('i') . '.booru.org/';
} }
return parent::getURI(); return parent::getURI();
} }
public function getName() public function getName(){
{ if(!is_null($this->getInput('i'))) {
if (!is_null($this->getInput('i'))) {
return static::NAME . ' ' . $this->getInput('i'); return static::NAME . ' ' . $this->getInput('i');
} }

View File

@ -1,16 +1,14 @@
<?php <?php
class BrutBridge extends BridgeAbstract {
class BrutBridge extends BridgeAbstract
{
const NAME = 'Brut Bridge'; const NAME = 'Brut Bridge';
const URI = 'https://www.brut.media'; const URI = 'https://www.brut.media';
const DESCRIPTION = 'Returns 10 newest videos by category and edition'; const DESCRIPTION = 'Returns 5 newest videos by category and edition';
const MAINTAINER = 'VerifiedJoseph'; const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = [[ const PARAMETERS = array(array(
'category' => [ 'category' => array(
'name' => 'Category', 'name' => 'Category',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'News' => 'news', 'News' => 'news',
'International' => 'international', 'International' => 'international',
'Economy' => 'economy', 'Economy' => 'economy',
@ -19,55 +17,141 @@ class BrutBridge extends BridgeAbstract
'Sports' => 'sport', 'Sports' => 'sport',
'Nature' => 'nature', 'Nature' => 'nature',
'Health' => 'health', 'Health' => 'health',
], ),
'defaultValue' => 'news', 'defaultValue' => 'news',
], ),
'edition' => [ 'edition' => array(
'name' => ' Edition', 'name' => ' Edition',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'United States' => 'us', 'United States' => 'us',
'United Kingdom' => 'uk', 'United Kingdom' => 'uk',
'France' => 'fr', 'France' => 'fr',
'Spain' => 'es', 'Spain' => 'es',
'India' => 'in', 'India' => 'in',
'Mexico' => 'mx', 'Mexico' => 'mx',
], ),
'defaultValue' => 'us', 'defaultValue' => 'us',
] )
] )
]; );
public function collectData() const CACHE_TIMEOUT = 1800; // 30 mins
{
$url = $this->getURI(); private $videoId = '';
$html = getSimpleHTMLDOM($url); private $videoType = '';
$regex = '/window.__PRELOADED_STATE__ = (.*);/'; private $videoImage = '';
preg_match($regex, $html, $parts);
$data = Json::decode($parts[1], false); public function collectData() {
foreach ($data->medias->index as $uid => $media) {
$this->items[] = [ $html = getSimpleHTMLDOM($this->getURI());
'uid' => $uid,
'title' => $media->metadata->slug, $results = $html->find('div.results', 0);
'uri' => $media->share_url,
'timestamp' => $media->published_at, foreach($results->find('li.col-6.col-sm-4.col-md-3.col-lg-2.px-2.pb-4') as $index => $li) {
]; $item = array();
$videoPath = self::URI . $li->children(0)->href;
$videoPageHtml = getSimpleHTMLDOMCached($videoPath, 3600);
$this->videoImage = $videoPageHtml->find('meta[name="twitter:image"]', 0)->content;
$this->processTwitterImage();
$description = $videoPageHtml->find('div.description', 0);
$item['uri'] = $videoPath;
$item['title'] = $description->find('h1', 0)->plaintext;
if ($description->find('div.date', 0)->children(0)) {
$description->find('div.date', 0)->children(0)->outertext = '';
}
$item['content'] = $this->processContent(
$description
);
$item['timestamp'] = $this->processDate($description);
$item['enclosures'][] = $this->videoImage;
$this->items[] = $item;
if (count($this->items) >= 5) {
break;
}
} }
} }
public function getURI() public function getURI() {
{
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category'); return self::URI . '/' . $this->getInput('edition') . '/' . $this->getInput('category');
} }
return parent::getURI(); return parent::getURI();
} }
public function getName() public function getName() {
{
if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) { if (!is_null($this->getInput('edition')) && !is_null($this->getInput('category'))) {
return $this->getKey('category') . ' - ' . $this->getKey('edition') . ' - Brut.'; $parameters = $this->getParameters();
$editionValues = array_flip($parameters[0]['edition']['values']);
$categoryValues = array_flip($parameters[0]['category']['values']);
return $categoryValues[$this->getInput('category')] . ' - ' .
$editionValues[$this->getInput('edition')] . ' - Brut.';
} }
return parent::getName(); return parent::getName();
} }
private function processDate($description) {
if ($this->getInput('edition') === 'uk') {
$date = DateTime::createFromFormat('d/m/Y H:i', $description->find('div.date', 0)->innertext);
return strtotime($date->format('Y-m-d H:i:s'));
}
return strtotime($description->find('div.date', 0)->innertext);
}
private function processContent($description) {
$content = '<video controls poster="' . $this->videoImage . '" preload="none">
<source src="https://content.brut.media/video/' . $this->videoId . '-' . $this->videoType . '-web.mp4"
type="video/mp4">
</video>';
$content .= '<p>' . $description->find('h2.mb-1', 0)->innertext . '</p>';
if ($description->find('div.text.pb-3', 0)->children(1)->class != 'date') {
$content .= '<p>' . $description->find('div.text.pb-3', 0)->children(1)->innertext . '</p>';
}
return $content;
}
private function processTwitterImage() {
/**
* Extract video ID + type from twitter image
*
* Example (wrapped):
* https://img.brut.media/thumbnail/
* the-life-of-rita-moreno-2cce75b5-d448-44d2-a97c-ca50d6470dd4-square.jpg
* ?ts=1559337892
*/
$fpath = parse_url($this->videoImage, PHP_URL_PATH);
$fname = basename($fpath);
$fname = substr($fname, 0, strrpos($fname, '.'));
$parts = explode('-', $fname);
if (end($parts) === 'auto') {
$key = array_search('auto', $parts);
unset($parts[$key]);
}
$this->videoId = implode('-', array_splice($parts, -6, 5));
$this->videoType = end($parts);
}
} }

View File

@ -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);
}
}

View File

@ -6,13 +6,13 @@ class BukowskisBridge extends BridgeAbstract
const URI = 'https://www.bukowskis.com'; const URI = 'https://www.bukowskis.com';
const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house'; const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house';
const MAINTAINER = 'Qluxzz'; const MAINTAINER = 'Qluxzz';
const PARAMETERS = [[ const PARAMETERS = array(array(
'category' => [ 'category' => array(
'name' => 'Category', 'name' => 'Category',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'All categories' => '', 'All categories' => '',
'Art' => [ 'Art' => array(
'All' => 'art', 'All' => 'art',
'Classic Art' => 'art.classic-art', 'Classic Art' => 'art.classic-art',
'Classic Finnish Art' => 'art.classic-finnish-art', 'Classic Finnish Art' => 'art.classic-finnish-art',
@ -27,31 +27,31 @@ class BukowskisBridge extends BridgeAbstract
'Prints' => 'art.prints', 'Prints' => 'art.prints',
'Sculpture' => 'art.sculpture', 'Sculpture' => 'art.sculpture',
'Swedish Old Masters' => 'art.swedish-old-masters', 'Swedish Old Masters' => 'art.swedish-old-masters',
], ),
'Asian Ceramics & Works of Art' => [ 'Asian Ceramics & Works of Art' => array(
'All' => 'asian-ceramics-works-of-art', 'All' => 'asian-ceramics-works-of-art',
'Other' => 'asian-ceramics-works-of-art.other', 'Other' => 'asian-ceramics-works-of-art.other',
'Porcelain' => 'asian-ceramics-works-of-art.porcelain', 'Porcelain' => 'asian-ceramics-works-of-art.porcelain',
], ),
'Books & Manuscripts' => [ 'Books & Manuscripts' => array(
'All' => 'books-manuscripts', 'All' => 'books-manuscripts',
'Books' => 'books-manuscripts.books', 'Books' => 'books-manuscripts.books',
], ),
'Carpets, rugs & textiles' => [ 'Carpets, rugs & textiles' => array(
'All' => 'carpets-rugs-textiles', 'All' => 'carpets-rugs-textiles',
'European' => 'carpets-rugs-textiles.european', 'European' => 'carpets-rugs-textiles.european',
'Oriental' => 'carpets-rugs-textiles.oriental', 'Oriental' => 'carpets-rugs-textiles.oriental',
'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world', 'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world',
'Scandinavian' => 'carpets-rugs-textiles.scandinavian', 'Scandinavian' => 'carpets-rugs-textiles.scandinavian',
], ),
'Ceramics & porcelain' => [ 'Ceramics & porcelain' => array(
'All' => 'ceramics-porcelain', 'All' => 'ceramics-porcelain',
'Ceramic ware' => 'ceramics-porcelain.ceramic-ware', 'Ceramic ware' => 'ceramics-porcelain.ceramic-ware',
'European' => 'ceramics-porcelain.european', 'European' => 'ceramics-porcelain.european',
'Rest of the world' => 'ceramics-porcelain.rest-of-the-world', 'Rest of the world' => 'ceramics-porcelain.rest-of-the-world',
'Scandinavian' => 'ceramics-porcelain.scandinavian', 'Scandinavian' => 'ceramics-porcelain.scandinavian',
], ),
'Collectibles' => [ 'Collectibles' => array(
'All' => 'collectibles', 'All' => 'collectibles',
'Advertising & Retail' => 'collectibles.advertising-retail', 'Advertising & Retail' => 'collectibles.advertising-retail',
'Memorabilia' => 'collectibles.memorabilia', 'Memorabilia' => 'collectibles.memorabilia',
@ -60,18 +60,18 @@ class BukowskisBridge extends BridgeAbstract
'Retro & Popular Culture' => 'collectibles.retro-popular-culture', 'Retro & Popular Culture' => 'collectibles.retro-popular-culture',
'Technica & Nautica' => 'collectibles.technica-nautica', 'Technica & Nautica' => 'collectibles.technica-nautica',
'Toys' => 'collectibles.toys', 'Toys' => 'collectibles.toys',
], ),
'Design' => [ 'Design' => array(
'All' => 'design', 'All' => 'design',
'Art glass' => 'design.art-glass', 'Art glass' => 'design.art-glass',
'Furniture' => 'design.furniture', 'Furniture' => 'design.furniture',
'Other' => 'design.other', 'Other' => 'design.other',
], ),
'Folk art' => [ 'Folk art' => array(
'All' => 'folk-art', 'All' => 'folk-art',
'All categories' => 'lots', 'All categories' => 'lots',
], ),
'Furniture' => [ 'Furniture' => array(
'All' => 'furniture', 'All' => 'furniture',
'Armchairs & Sofas' => 'furniture.armchairs-sofas', 'Armchairs & Sofas' => 'furniture.armchairs-sofas',
'Cabinets & Bureaus' => 'furniture.cabinets-bureaus', 'Cabinets & Bureaus' => 'furniture.cabinets-bureaus',
@ -81,13 +81,13 @@ class BukowskisBridge extends BridgeAbstract
'Other' => 'furniture.other', 'Other' => 'furniture.other',
'Shelves & Book cases' => 'furniture.shelves-book-cases', 'Shelves & Book cases' => 'furniture.shelves-book-cases',
'Tables' => 'furniture.tables', 'Tables' => 'furniture.tables',
], ),
'Glassware' => [ 'Glassware' => array(
'All' => 'glassware', 'All' => 'glassware',
'Glassware' => 'glassware.glassware', 'Glassware' => 'glassware.glassware',
'Other' => 'glassware.other', 'Other' => 'glassware.other',
], ),
'Jewellery' => [ 'Jewellery' => array(
'All' => 'jewellery', 'All' => 'jewellery',
'Bracelets' => 'jewellery.bracelets', 'Bracelets' => 'jewellery.bracelets',
'Brooches' => 'jewellery.brooches', 'Brooches' => 'jewellery.brooches',
@ -95,8 +95,8 @@ class BukowskisBridge extends BridgeAbstract
'Necklaces & Pendants' => 'jewellery.necklaces-pendants', 'Necklaces & Pendants' => 'jewellery.necklaces-pendants',
'Other' => 'jewellery.other', 'Other' => 'jewellery.other',
'Rings' => 'jewellery.rings', 'Rings' => 'jewellery.rings',
], ),
'Lighting' => [ 'Lighting' => array(
'All' => 'lighting', 'All' => 'lighting',
'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras', 'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras',
'Ceiling lights' => 'lighting.ceiling-lights', 'Ceiling lights' => 'lighting.ceiling-lights',
@ -105,46 +105,46 @@ class BukowskisBridge extends BridgeAbstract
'Other' => 'lighting.other', 'Other' => 'lighting.other',
'Table lights' => 'lighting.table-lights', 'Table lights' => 'lighting.table-lights',
'Wall lights' => 'lighting.wall-lights', 'Wall lights' => 'lighting.wall-lights',
], ),
'Militaria' => [ 'Militaria' => array(
'All' => 'militaria', 'All' => 'militaria',
'Honors & Medals' => 'militaria.honors-medals', 'Honors & Medals' => 'militaria.honors-medals',
'Other militaria' => 'militaria.other-militaria', 'Other militaria' => 'militaria.other-militaria',
'Weaponry' => 'militaria.weaponry', 'Weaponry' => 'militaria.weaponry',
], ),
'Miscellaneous' => [ 'Miscellaneous' => array(
'All' => 'miscellaneous', 'All' => 'miscellaneous',
'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter', 'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter',
'Nickel silver' => 'miscellaneous.nickel-silver', 'Nickel silver' => 'miscellaneous.nickel-silver',
'Oriental' => 'miscellaneous.oriental', 'Oriental' => 'miscellaneous.oriental',
'Other' => 'miscellaneous.other', 'Other' => 'miscellaneous.other',
], ),
'Silver' => [ 'Silver' => array(
'All' => 'silver', 'All' => 'silver',
'Candle sticks' => 'silver.candle-sticks', 'Candle sticks' => 'silver.candle-sticks',
'Cups & Bowls' => 'silver.cups-bowls', 'Cups & Bowls' => 'silver.cups-bowls',
'Cutlery' => 'silver.cutlery', 'Cutlery' => 'silver.cutlery',
'Other' => 'silver.other', 'Other' => 'silver.other',
], ),
'Timepieces' => [ 'Timepieces' => array(
'All' => 'timepieces', 'All' => 'timepieces',
'Other' => 'timepieces.other', 'Other' => 'timepieces.other',
'Pocket watches' => 'timepieces.pocket-watches', 'Pocket watches' => 'timepieces.pocket-watches',
'Table clocks' => 'timepieces.table-clocks', 'Table clocks' => 'timepieces.table-clocks',
'Wrist watches' => 'timepieces.wrist-watches', 'Wrist watches' => 'timepieces.wrist-watches',
], ),
'Vintage & Fashion' => [ 'Vintage & Fashion' => array(
'All' => 'vintage-fashion', 'All' => 'vintage-fashion',
'Accessories' => 'vintage-fashion.accessories', 'Accessories' => 'vintage-fashion.accessories',
'Bags & Trunks' => 'vintage-fashion.bags-trunks', 'Bags & Trunks' => 'vintage-fashion.bags-trunks',
'Clothes' => 'vintage-fashion.clothes', 'Clothes' => 'vintage-fashion.clothes',
], ),
] )
], ),
'sort_order' => [ 'sort_order' => array(
'name' => 'Sort order', 'name' => 'Sort order',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Ending soon' => 'ending', 'Ending soon' => 'ending',
'Most recent' => 'recent', 'Most recent' => 'recent',
'Most bids' => 'most', 'Most bids' => 'most',
@ -154,18 +154,18 @@ class BukowskisBridge extends BridgeAbstract
'Lowest estimate' => 'low', 'Lowest estimate' => 'low',
'Highest estimate' => 'high', 'Highest estimate' => 'high',
'Alphabetical' => 'alphabetical', 'Alphabetical' => 'alphabetical',
], ),
], ),
'language' => [ 'language' => array(
'name' => 'Language', 'name' => 'Language',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'English' => 'en', 'English' => 'en',
'Swedish' => 'sv', 'Swedish' => 'sv',
'Finnish' => 'fi' 'Finnish' => 'fi'
], ),
], ),
]]; ));
const CACHE_TIMEOUT = 3600; // 1 hour const CACHE_TIMEOUT = 3600; // 1 hour
@ -180,13 +180,11 @@ class BukowskisBridge extends BridgeAbstract
$url = $baseUrl . '/' . $language . '/lots'; $url = $baseUrl . '/' . $language . '/lots';
if ($category) { if ($category)
$url = $url . '/category/' . $category; $url = $url . '/category/' . $category;
}
if ($sort_order) { if ($sort_order)
$url = $url . '/sort/' . $sort_order; $url = $url . '/sort/' . $sort_order;
}
$html = getSimpleHTMLDOM($url); $html = getSimpleHTMLDOM($url);
@ -203,13 +201,13 @@ class BukowskisBridge extends BridgeAbstract
) )
); );
$this->items[] = [ $this->items[] = array(
'title' => $title, 'title' => $title,
'uri' => $baseUrl . $relative_url, 'uri' => $baseUrl . $relative_url,
'uid' => $relative_url, 'uid' => $lot->getAttribute('data-lot-id'),
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title, 'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
'enclosures' => array_slice($images, 1), 'enclosures' => array_slice($images, 1),
]; );
} }
} }

View File

@ -1,7 +1,6 @@
<?php <?php
class BundesbankBridge extends BridgeAbstract {
class BundesbankBridge extends BridgeAbstract
{
const PARAM_LANG = 'lang'; const PARAM_LANG = 'lang';
const LANG_EN = 'en'; const LANG_EN = 'en';
@ -13,52 +12,48 @@ class BundesbankBridge extends BridgeAbstract
const MAINTAINER = 'logmanoriginal'; const MAINTAINER = 'logmanoriginal';
const CACHE_TIMEOUT = 86400; // 24 hours const CACHE_TIMEOUT = 86400; // 24 hours
const PARAMETERS = [ const PARAMETERS = array(
[ array(
self::PARAM_LANG => [ self::PARAM_LANG => array(
'name' => 'Language', 'name' => 'Language',
'type' => 'list', 'type' => 'list',
'defaultValue' => self::LANG_DE, 'defaultValue' => self::LANG_DE,
'values' => [ 'values' => array(
'English' => self::LANG_EN, 'English' => self::LANG_EN,
'Deutsch' => self::LANG_DE 'Deutsch' => self::LANG_DE
] )
] )
] )
]; );
public function getIcon() public function getIcon() {
{
return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico'; return self::URI . 'resource/crblob/1890/a7f48ee0ae35348748121770ba3ca009/mL/favicon-ico-data.ico';
} }
public function getURI() public function getURI() {
{ switch($this->getInput(self::PARAM_LANG)) {
switch ($this->getInput(self::PARAM_LANG)) { case self::LANG_EN: return self::URI . 'en/publications/reports/studies';
case self::LANG_EN: case self::LANG_DE: return self::URI . 'de/publikationen/berichte/studien';
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() public function collectData() {
{
$html = getSimpleHTMLDOM($this->getURI()); $html = getSimpleHTMLDOM($this->getURI());
$html = defaultLinkTo($html, $this->getURI()); $html = defaultLinkTo($html, $this->getURI());
foreach ($html->find('ul.resultlist li') as $study) { foreach($html->find('ul.resultlist li') as $study) {
$item = []; $item = array();
$item['uri'] = $study->find('.teasable__link', 0)->href; $item['uri'] = $study->find('.teasable__link', 0)->href;
// Get title without child elements (i.e. subtitle) // Get title without child elements (i.e. subtitle)
$title = $study->find('.teasable__title div.h2', 0); $title = $study->find('.teasable__title div.h2', 0);
foreach ($title->children as &$child) { foreach($title->children as &$child) {
$child->outertext = ''; $child->outertext = '';
} }
@ -67,24 +62,23 @@ class BundesbankBridge extends BridgeAbstract
// Add subtitle to the content if it exists // Add subtitle to the content if it exists
$item['content'] = ''; $item['content'] = '';
if ($subtitle = $study->find('.teasable__subtitle', 0)) { if($subtitle = $study->find('.teasable__subtitle', 0)) {
$item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>'; $item['content'] .= '<strong>' . $study->find('.teasable__subtitle', 0)->plaintext . '</strong>';
} }
$teasable = $study->find('.teasable__text', 0); $item['content'] .= '<p>' . $study->find('.teasable__text', 0)->plaintext . '</p>';
$teasableText = $teasable->plaintext ?? '';
$item['content'] .= '<p>' . $teasableText . '</p>';
$item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext); $item['timestamp'] = strtotime($study->find('.teasable__date', 0)->plaintext);
// Downloads and older studies don't have images // Downloads and older studies don't have images
if ($study->find('.teasable__image', 0)) { if($study->find('.teasable__image', 0)) {
$item['enclosures'] = [ $item['enclosures'] = array(
$study->find('.teasable__image img', 0)->src $study->find('.teasable__image img', 0)->src
]; );
} }
$this->items[] = $item; $this->items[] = $item;
} }
} }
} }

View File

@ -1,7 +1,5 @@
<?php <?php
class BundestagParteispendenBridge extends BridgeAbstract {
class BundestagParteispendenBridge extends BridgeAbstract
{
const MAINTAINER = 'mibe'; const MAINTAINER = 'mibe';
const NAME = 'Deutscher Bundestag - Parteispenden'; const NAME = 'Deutscher Bundestag - Parteispenden';
const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000'; const URI = 'https://www.bundestag.de/parlament/praesidium/parteienfinanzierung/fundstellen50000';
@ -26,21 +24,23 @@ TMPL;
https://www.bundestag.de/ajax/filterlist/de/parlament/praesidium/parteienfinanzierung/fundstellen50000/462002-462002 https://www.bundestag.de/ajax/filterlist/de/parlament/praesidium/parteienfinanzierung/fundstellen50000/462002-462002
URI; URI;
// Get the main page // Get the main page
$html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT); $html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT)
or returnServerError('Could not request AJAX list.');
// Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year. // 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) $firstAnchor = $html->find('a', 0)
or returnServerError('Could not find the proper HTML element.'); or returnServerError('Could not find the proper HTML element.');
$url = $firstAnchor->href; $url = 'https://www.bundestag.de' . $firstAnchor->href;
// Get the actual page with the soft money donations // Get the actual page with the soft money donations
$html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT); $html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT)
or returnServerError('Could not request ' . $url);
$rows = $html->find('table.table > tbody > tr') $rows = $html->find('table.table > tbody > tr')
or returnServerError('Could not find the proper HTML elements.'); or returnServerError('Could not find the proper HTML elements.');
foreach ($rows as $row) { foreach($rows as $row) {
$item = $this->generateItemFromRow($row); $item = $this->generateItemFromRow($row);
if (is_array($item)) { if (is_array($item)) {
$item['uri'] = $url; $item['uri'] = $url;
@ -52,11 +52,10 @@ URI;
private function generateItemFromRow(simple_html_dom_node $row) private function generateItemFromRow(simple_html_dom_node $row)
{ {
// The row must have 5 columns. There are monthly header rows, which are ignored here. // The row must have 5 columns. There are monthly header rows, which are ignored here.
if (count($row->children) != 5) { if(count($row->children) != 5)
return null; return null;
}
$item = []; $item = array();
// | column | paragraph inside column // | column | paragraph inside column
$party = $row->children[0]->children[0]->innertext; $party = $row->children[0]->children[0]->innertext;
@ -70,22 +69,20 @@ URI;
$content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date); $content = sprintf(self::CONTENT_TEMPLATE, $party, $amount, $donor, $date);
$item = [ $item = array(
'title' => $party . ': ' . $amount, 'title' => $party . ': ' . $amount,
'content' => $content, 'content' => $content,
'uid' => sha1($content), 'uid' => sha1($content),
]; );
// Try to get the link to the official document // Try to get the link to the official document
if ($dip != null) { if ($dip != null)
$item['enclosures'] = [$dip->href]; $item['enclosures'] = array($dip->href);
}
// Try to parse the date // Try to parse the date
$dateTime = DateTime::createFromFormat('d.m.Y', $date); $dateTime = DateTime::createFromFormat('d.m.Y', $date);
if ($dateTime !== false) { if ($dateTime !== false)
$item['timestamp'] = $dateTime->getTimestamp(); $item['timestamp'] = $dateTime->getTimestamp();
}
return $item; return $item;
} }

View File

@ -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();
}
}

View File

@ -1,18 +1,16 @@
<?php <?php
class CBCEditorsBlogBridge extends BridgeAbstract {
class CBCEditorsBlogBridge extends BridgeAbstract
{
const MAINTAINER = 'quickwick'; const MAINTAINER = 'quickwick';
const NAME = 'CBC Editors Blog'; const NAME = 'CBC Editors Blog';
const URI = 'https://www.cbc.ca/news/editorsblog'; const URI = 'https://www.cbc.ca/news/editorsblog';
const DESCRIPTION = 'Recent CBC Editor\'s Blog posts'; const DESCRIPTION = 'Recent CBC Editor\'s Blog posts';
public function collectData() public function collectData(){
{
$html = getSimpleHTMLDOM(self::URI); $html = getSimpleHTMLDOM(self::URI);
// Loop on each blog post entry // Loop on each blog post entry
foreach ($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) { foreach($html->find('div.contentListCards', 0)->find('a[data-test=type-story]') as $element) {
$headline = ($element->find('.headline', 0))->innertext; $headline = ($element->find('.headline', 0))->innertext;
$timestamp = ($element->find('time', 0))->datetime; $timestamp = ($element->find('time', 0))->datetime;
$articleUri = 'https://www.cbc.ca' . $element->href; $articleUri = 'https://www.cbc.ca' . $element->href;
@ -21,7 +19,7 @@ class CBCEditorsBlogBridge extends BridgeAbstract
$thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w'); $thumbnailUri = rtrim(explode(',', $thumbnailUris)[0], ' 300w');
// Fill item // Fill item
$item = []; $item = array();
$item['uri'] = $articleUri; $item['uri'] = $articleUri;
$item['id'] = $item['uri']; $item['id'] = $item['uri'];
$item['timestamp'] = $timestamp; $item['timestamp'] = $timestamp;
@ -30,7 +28,7 @@ class CBCEditorsBlogBridge extends BridgeAbstract
. $thumbnailUri . '" /><br>' . $summary; . $thumbnailUri . '" /><br>' . $summary;
$item['author'] = 'Editor\'s Blog'; $item['author'] = 'Editor\'s Blog';
if (isset($item['title'])) { if(isset($item['title'])) {
$this->items[] = $item; $this->items[] = $item;
} }
} }

View File

@ -1,118 +1,108 @@
<?php <?php
class CNETBridge extends BridgeAbstract {
class CNETBridge extends SitemapBridge
{
const MAINTAINER = 'ORelio'; const MAINTAINER = 'ORelio';
const NAME = 'CNET News'; const NAME = 'CNET News';
const URI = 'https://www.cnet.com/'; const URI = 'https://www.cnet.com/';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Returns the newest articles.'; const DESCRIPTION = 'Returns the newest articles.';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'topic' => [ 'topic' => array(
'name' => 'Topic', 'name' => 'Topic',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'All articles' => '', 'All articles' => '',
'Tech' => 'tech', 'Apple' => 'apple',
'Money' => 'personal-finance', 'Google' => 'google',
'Home' => 'home', 'Microsoft' => 'tags-microsoft',
'Wellness' => 'health', 'Computers' => 'topics-computers',
'Energy' => 'home/energy-and-utilities', 'Mobile' => 'topics-mobile',
'Deals' => 'deals', 'Sci-Tech' => 'topics-sci-tech',
'Computing' => 'tech/computing', 'Security' => 'topics-security',
'Mobile' => 'tech/mobile', 'Internet' => 'topics-internet',
'Science' => 'science', 'Tech Industry' => 'topics-tech-industry'
'Services' => 'tech/services-and-software' )
] )
], )
'limit' => self::LIMIT
]
];
public function collectData()
{
$topic = $this->getInput('topic');
$limit = $this->getInput('limit');
$limit = empty($limit) ? 10 : $limit;
$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';
$links = array_merge(
$this->sitemapXmlToList($this->getSitemapXml($sitemap_latest, true), $url_pattern, $limit),
$this->sitemapXmlToList($this->getSitemapXml($sitemap_previous, true), $url_pattern, $limit)
); );
if ($limit > 0 && count($links) > $limit) { private function cleanArticle($article_html) {
$links = array_slice($links, 0, $limit); $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;
} }
if (empty($links)) { public function collectData() {
returnClientError('Failed to retrieve article list');
// 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);
// Retrieve webpage
$pageUrl = self::URI . (empty($topic) ? 'news/' : $topic . '/');
$html = getSimpleHTMLDOM($pageUrl);
// Process articles
foreach($html->find('div.assetBody, div.riverPost') as $element) {
if(count($this->items) >= 10) {
break;
} }
foreach ($links as $article_uri) { $article_title = trim($element->find('h2, h3', 0)->plaintext);
$article_dom = convertLazyLoading(getSimpleHTMLDOMCached($article_uri)); $article_uri = self::URI . substr($element->find('a', 0)->href, 1);
$title = trim($article_dom->find('h1', 0)->plaintext); $article_thumbnail = $element->parent()->find('img[src]', 0)->src;
$author = $article_dom->find('span.c-assetAuthor_name', 0); $article_timestamp = strtotime($element->find('time.assetTime, div.timeAgo', 0)->plaintext);
$headline = $article_dom->find('p.c-contentHeader_description', 0); $article_author = trim($element->find('a[rel=author], a.name', 0)->plaintext);
$content = $article_dom->find('div.c-pageArticle_content, div.single-article__content, div.article-main-body', 0); $article_content = '<p><b>' . trim($element->find('p.dek', 0)->plaintext) . '</b></p>';
$date = null;
$enclosure = null;
foreach ($article_dom->find('script[type=application/ld+json]') as $ldjson) { if (is_null($article_thumbnail))
$datePublished = extractFromDelimiters($ldjson->innertext, '"datePublished":"', '"'); $article_thumbnail = extractFromDelimiters($element->innertext, '<img src="', '"');
if ($datePublished !== false) {
$date = strtotime($datePublished); if (!empty($article_title) && !empty($article_uri) && strpos($article_uri, self::URI . 'news/') !== false) {
}
$imageObject = extractFromDelimiters($ldjson->innertext, 'ImageObject","url":"', '"'); $article_html = getSimpleHTMLDOMCached($article_uri) or $article_html = null;
if ($imageObject !== false) {
$enclosure = $imageObject; if (!is_null($article_html)) {
}
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;
$article_content .= trim(
$this->cleanArticle(
extractFromDelimiters(
$article_html, '<article', '<footer'
)
)
);
} }
foreach ($content->find('div.c-shortcodeGallery') as $cleanup) { $item = array();
$cleanup->outertext = '';
}
foreach ($content->find('figure') as $figure) {
$img = $figure->find('img', 0);
if ($img) {
$figure->outertext = $img->outertext;
}
}
$content = $content->innertext;
if ($enclosure) {
$content = "<div><img src=\"$enclosure\" /></div>" . $content;
}
if ($headline) {
$content = '<p><b>' . $headline->plaintext . '</b></p><br />' . $content;
}
$item = [];
$item['uri'] = $article_uri; $item['uri'] = $article_uri;
$item['title'] = $title; $item['title'] = $article_title;
$item['author'] = $article_author;
if ($author) { $item['timestamp'] = $article_timestamp;
$item['author'] = $author->plaintext; $item['enclosures'] = array($article_thumbnail);
} $item['content'] = $article_content;
$item['content'] = $content;
if (!is_null($date)) {
$item['timestamp'] = $date;
}
if (!is_null($enclosure)) {
$item['enclosures'] = [$enclosure];
}
$this->items[] = $item; $this->items[] = $item;
} }
} }
}
} }

View File

@ -1,5 +1,4 @@
<?php <?php
class CNETFranceBridge extends FeedExpander class CNETFranceBridge extends FeedExpander
{ {
const MAINTAINER = 'leomaradan'; const MAINTAINER = 'leomaradan';
@ -7,25 +6,25 @@ class CNETFranceBridge extends FeedExpander
const URI = 'https://www.cnetfrance.fr/'; const URI = 'https://www.cnetfrance.fr/';
const CACHE_TIMEOUT = 3600; // 1h const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'CNET France RSS with filters'; const DESCRIPTION = 'CNET France RSS with filters';
const PARAMETERS = [ const PARAMETERS = array(
'filters' => [ 'filters' => array(
'title' => [ 'title' => array(
'name' => 'Exclude by title', 'name' => 'Exclude by title',
'required' => false, 'required' => false,
'title' => 'Title term, separated by semicolon (;)', 'title' => 'Title term, separated by semicolon (;)',
'exampleValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You' 'exampleValue' => 'bon plan;bons plans;au meilleur prix;des meilleures offres;Amazon Prime Day;RED by SFR ou B&You'
], ),
'url' => [ 'url' => array(
'name' => 'Exclude by url', 'name' => 'Exclude by url',
'required' => false, 'required' => false,
'title' => 'URL term, separated by semicolon (;)', 'title' => 'URL term, separated by semicolon (;)',
'exampleValue' => 'bon-plan;bons-plans' 'exampleValue' => 'bon-plan;bons-plans'
] )
] )
]; );
private $bannedTitle = []; private $bannedTitle = array();
private $bannedURL = []; private $bannedURL = array();
public function collectData() public function collectData()
{ {
@ -43,8 +42,10 @@ class CNETFranceBridge extends FeedExpander
$this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/'); $this->collectExpandableDatas('https://www.cnetfrance.fr/feeds/rss/news/');
} }
protected function parseItem(array $item) protected function parseItem($feedItem)
{ {
$item = parent::parseItem($feedItem);
foreach ($this->bannedTitle as $term) { foreach ($this->bannedTitle as $term) {
if (preg_match('/' . $term . '/mi', $item['title']) === 1) { if (preg_match('/' . $term . '/mi', $item['title']) === 1) {
return null; return null;
@ -52,7 +53,7 @@ class CNETFranceBridge extends FeedExpander
} }
foreach ($this->bannedURL as $term) { foreach ($this->bannedURL as $term) {
if (preg_match('#' . $term . '#mi', $item['uri'])) { if (preg_match('/' . $term . '/mi', $item['uri']) === 1) {
return null; return null;
} }
} }

View File

@ -7,72 +7,39 @@
// it is not reliable and contain no useful information. This bridge create a // it is not reliable and contain no useful information. This bridge create a
// sane feed with additional information like tags and a link to the CWE // sane feed with additional information like tags and a link to the CWE
// a description of the vulnerability. // a description of the vulnerability.
class CVEDetailsBridge extends BridgeAbstract class CVEDetailsBridge extends BridgeAbstract {
{
const MAINTAINER = 'Aaron Fischer'; const MAINTAINER = 'Aaron Fischer';
const NAME = 'CVE Details'; const NAME = 'CVE Details';
const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours const CACHE_TIMEOUT = 60 * 60 * 6; // 6 hours
const DESCRIPTION = 'Report new CVE vulnerabilities for a given vendor (and product)'; const DESCRIPTION = 'Report new CVE vulnerabilities for a given vendor (and product)';
const URI = 'https://www.cvedetails.com'; const URI = 'https://www.cvedetails.com';
const PARAMETERS = [[ const PARAMETERS = array(array(
// The Vendor ID can be taken from the URL // The Vendor ID can be taken from the URL
'vendor_id' => [ 'vendor_id' => array(
'name' => 'Vendor ID', 'name' => 'Vendor ID',
'type' => 'number', 'type' => 'number',
'required' => true, 'required' => true,
'exampleValue' => 74, // PHP 'exampleValue' => 74, // PHP
], ),
// The optional Product ID can be taken from the URL as well // The optional Product ID can be taken from the URL as well
'product_id' => [ 'product_id' => array(
'name' => 'Product ID', 'name' => 'Product ID',
'type' => 'number', 'type' => 'number',
'required' => false, 'required' => false,
'exampleValue' => 128, // PHP 'exampleValue' => 128, // PHP
], ),
]]; ));
private $html = null; private $html = null;
private $vendor = ''; private $vendor = '';
private $product = ''; private $product = '';
public function collectData()
{
if ($this->html == null) {
$this->fetchContent();
}
$var = $this->html->find('#searchresults > div > div.row');
foreach ($var as $i => $tr) {
$uri = $tr->find('h3 > a', 0)->href ?? null;
$title = $tr->find('h3 > a', 0)->innertext;
$content = $tr->find('.cvesummarylong', 0)->innertext ?? '';
$timestamp = $tr->find('[data-tsvfield="publishDate"]', 0)->innertext ?? 0;
$this->items[] = [
'uri' => $uri,
'title' => $title,
'timestamp' => $timestamp,
'content' => $content,
'categories' => [$this->vendor],
'enclosures' => [],
'uid' => $title,
];
if (count($this->items) >= 30) {
break;
}
}
}
// Make the actual request to cvedetails.com and stores the response
// (HTML) for later use and extract vendor and product from it.
private function fetchContent()
{
// build url
// Return the URL to query. // Return the URL to query.
// Because of the optional product ID, we need to attach it if it is // Because of the optional product ID, we need to attach it if it is
// set. The search result page has the exact same structure (with and // set. The search result page has the exact same structure (with and
// without the product ID). // without the product ID).
private function _buildURL() {
$url = self::URI . '/vulnerability-list/vendor_id-' . $this->getInput('vendor_id'); $url = self::URI . '/vulnerability-list/vendor_id-' . $this->getInput('vendor_id');
if ($this->getInput('product_id') !== '') { if ($this->getInput('product_id') !== '') {
$url .= '/product_id-' . $this->getInput('product_id'); $url .= '/product_id-' . $this->getInput('product_id');
@ -82,29 +49,38 @@ class CVEDetailsBridge extends BridgeAbstract
// number, which should be mostly accurate. // number, which should be mostly accurate.
$url .= '?order=1'; // Order by CVE number DESC $url .= '?order=1'; // Order by CVE number DESC
$html = getSimpleHTMLDOM($url); return $url;
}
// Make the actual request to cvedetails.com and stores the response
// (HTML) for later use and extract vendor and product from it.
private function _fetchContent() {
$html = getSimpleHTMLDOM($this->_buildURL());
$this->html = defaultLinkTo($html, self::URI); $this->html = defaultLinkTo($html, self::URI);
$vendor = $html->find('#contentdiv h1 > a', 0); $vendor = $html->find('#contentdiv > h1 > a', 0);
if ($vendor == null) { if ($vendor == null) {
returnServerError('Invalid Vendor ID ' . $this->getInput('vendor_id') . ' or Product ID ' . $this->getInput('product_id')); returnServerError('Invalid Vendor ID ' .
$this->getInput('vendor_id') .
' or Product ID ' .
$this->getInput('product_id'));
} }
$this->vendor = $vendor->innertext; $this->vendor = $vendor->innertext;
$product = $html->find('#contentdiv h1 > a', 1); $product = $html->find('#contentdiv > h1 > a', 1);
if ($product != null) { if ($product != null) {
$this->product = $product->innertext; $this->product = $product->innertext;
} }
} }
public function getName() // Build the name of the feed.
{ public function getName() {
if ($this->getInput('vendor_id') == '') { if ($this->getInput('vendor_id') == '') {
return self::NAME; return self::NAME;
} }
if ($this->html == null) { if ($this->html == null) {
$this->fetchContent(); $this->_fetchContent();
} }
$name = 'CVE Vulnerabilities for ' . $this->vendor; $name = 'CVE Vulnerabilities for ' . $this->vendor;
@ -114,4 +90,51 @@ class CVEDetailsBridge extends BridgeAbstract
return $name; return $name;
} }
// Pull the data from the HTML response and fill the items..
public function collectData() {
if ($this->html == null) {
$this->_fetchContent();
}
foreach ($this->html->find('#vulnslisttable .srrowns') as $i => $tr) {
// There are some optional vulnerability types, which will be
// added to the categories as well as the CWE number -- which is
// always given.
$categories = array($this->vendor);
$enclosures = array();
$cwe = $tr->find('td', 2)->find('a', 0);
if ($cwe != null) {
$cwe = $cwe->innertext;
$categories[] = 'CWE-' . $cwe;
$enclosures[] = 'https://cwe.mitre.org/data/definitions/' . $cwe . '.html';
}
$c = $tr->find('td', 4)->innertext;
if (trim($c) != '') {
$categories[] = $c;
}
if ($this->product != '') {
$categories[] = $this->product;
}
// The CVE number itself
$title = $tr->find('td', 1)->find('a', 0)->innertext;
$this->items[] = array(
'uri' => $tr->find('td', 1)->find('a', 0)->href,
'title' => $title,
'timestamp' => $tr->find('td', 5)->innertext,
'content' => $tr->next_sibling()->innertext,
'categories' => $categories,
'enclosures' => $enclosures,
'uid' => $tr->find('td', 1)->find('a', 0)->innertext,
);
// We only want to fetch the latest 10 CVEs
if (count($this->items) >= 10) {
break;
}
}
}
} }

View File

@ -1,32 +1,30 @@
<?php <?php
class CachetBridge extends BridgeAbstract class CachetBridge extends BridgeAbstract {
{
const NAME = 'Cachet Bridge'; const NAME = 'Cachet Bridge';
const URI = 'https://cachethq.io/'; const URI = 'https://cachethq.io/';
const DESCRIPTION = 'Returns status updates from any Cachet installation'; const DESCRIPTION = 'Returns status updates from any Cachet installation';
const MAINTAINER = 'klimplant'; const MAINTAINER = 'klimplant';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'host' => [ 'host' => array(
'name' => 'Cachet installation', 'name' => 'Cachet installation',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'title' => 'The URL of the Cachet installation', 'title' => 'The URL of the Cachet installation',
'exampleValue' => 'https://demo.cachethq.io/', 'exampleValue' => 'https://demo.cachethq.io/',
], 'additional_info' => [ ), 'additional_info' => array(
'name' => 'Additional Timestamps', 'name' => 'Additional Timestamps',
'type' => 'checkbox', 'type' => 'checkbox',
'title' => 'Whether to include the given timestamps' 'title' => 'Whether to include the given timestamps'
] )
] )
]; );
const CACHE_TIMEOUT = 300; const CACHE_TIMEOUT = 300;
private $componentCache = []; private $componentCache = array();
public function getURI() public function getURI() {
{
return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host'); return $this->getInput('host') === null ? 'https://cachethq.io/' : $this->getInput('host');
} }
@ -36,8 +34,7 @@ class CachetBridge extends BridgeAbstract
* @param string $ping * @param string $ping
* @return boolean * @return boolean
*/ */
private function validatePing($ping) private function validatePing($ping) {
{
$ping = json_decode($ping); $ping = json_decode($ping);
if ($ping === null) { if ($ping === null) {
return false; return false;
@ -51,8 +48,7 @@ class CachetBridge extends BridgeAbstract
* @param integer $id * @param integer $id
* @return string * @return string
*/ */
private function getComponentName($id) private function getComponentName($id) {
{
if ($id === 0) { if ($id === 0) {
return ''; return '';
} }
@ -68,8 +64,7 @@ class CachetBridge extends BridgeAbstract
return $component->data->name; return $component->data->name;
} }
public function collectData() public function collectData() {
{
$ping = getContents(urljoin($this->getURI(), '/api/v1/ping')); $ping = getContents(urljoin($this->getURI(), '/api/v1/ping'));
if (!$this->validatePing($ping)) { if (!$this->validatePing($ping)) {
returnClientError('Provided URI is invalid!'); returnClientError('Provided URI is invalid!');
@ -89,6 +84,7 @@ class CachetBridge extends BridgeAbstract
}); });
foreach ($incidents->data as $incident) { foreach ($incidents->data as $incident) {
if (isset($incident->permalink)) { if (isset($incident->permalink)) {
$permalink = $incident->permalink; $permalink = $incident->permalink;
} else { } else {
@ -118,13 +114,13 @@ class CachetBridge extends BridgeAbstract
$uidOrig = $permalink . $incident->created_at; $uidOrig = $permalink . $incident->created_at;
$uid = hash('sha512', $uidOrig); $uid = hash('sha512', $uidOrig);
$timestamp = strtotime($incident->created_at); $timestamp = strtotime($incident->created_at);
$categories = []; $categories = array();
$categories[] = $incident->human_status; $categories[] = $incident->human_status;
if ($componentName !== '') { if ($componentName !== '') {
$categories[] = $componentName; $categories[] = $componentName;
} }
$item = []; $item = array();
$item['uri'] = $permalink; $item['uri'] = $permalink;
$item['title'] = $title; $item['title'] = $title;
$item['timestamp'] = $timestamp; $item['timestamp'] = $timestamp;

View File

@ -1,118 +1,41 @@
<?php <?php
class CarThrottleBridge extends FeedExpander {
class CarThrottleBridge extends BridgeAbstract const NAME = 'Car Throttle ';
{ const URI = 'https://www.carthrottle.com';
const NAME = 'Car Throttle';
const URI = 'https://www.carthrottle.com/';
const DESCRIPTION = 'Get the latest car-related news from Car Throttle.'; const DESCRIPTION = 'Get the latest car-related news from Car Throttle.';
const MAINTAINER = 't0stiman'; const MAINTAINER = 't0stiman';
const DONATION_URI = 'https://ko-fi.com/tostiman';
const PARAMETERS = [ public function collectData() {
'Show articles from these categories:' => [ $this->collectExpandableDatas('https://www.carthrottle.com/rss', 10);
'news' => [
'name' => 'news',
'type' => 'checkbox'
],
'reviews' => [
'name' => 'reviews',
'type' => 'checkbox'
],
'features' => [
'name' => 'features',
'type' => 'checkbox'
],
'videos' => [
'name' => 'videos',
'type' => 'checkbox'
],
'gaming' => [
'name' => 'gaming',
'type' => 'checkbox'
]
]
];
public function collectData()
{
$this->items = [];
$this->handleCategory('news');
$this->handleCategory('reviews');
$this->handleCategory('features');
$this->handleCategory2('videos', 'video');
$this->handleCategory('gaming');
} }
private function handleCategory($category) protected function parseItem($feedItem) {
{ $item = parent::parseItem($feedItem);
if ($this->getInput($category)) {
$this->getArticles($category); //fetch page
$articlePage = getSimpleHTMLDOMCached($feedItem->link)
or returnServerError('Could not retrieve ' . $feedItem->link);
$subtitle = $articlePage->find('p.standfirst', 0);
$article = $articlePage->find('div.content_field', 0);
$item['content'] = str_get_html($subtitle . $article);
//convert <iframe>s to <a>s. meant for embedded videos.
foreach($item['content']->find('iframe') as $found) {
$iframeUrl = $found->getAttribute('src');
if ($iframeUrl) {
$found->outertext = '<a href="' . $iframeUrl . '">' . $iframeUrl . '</a>';
} }
} }
private function handleCategory2($categoryParameter, $categoryURLname) //remove scripts from the text
{ foreach ($item['content']->find('script') as $remove) {
if ($this->getInput($categoryParameter)) { $remove->outertext = '';
$this->getArticles($categoryURLname);
}
} }
private function getArticles($category) return $item;
{
$categoryPage = getSimpleHTMLDOMCached(self::URI . $category);
//for each post
foreach ($categoryPage->find('div.cmg-card') as $post) {
$item = [];
$titleElement = $post->find('a.title')[0];
$post_uri = self::URI . $titleElement->getAttribute('href');
if (!isset($post_uri) || $post_uri == '') {
continue;
}
$item['uri'] = $post_uri;
$item['title'] = $titleElement->innertext;
$articlePage = getSimpleHTMLDOMCached($item['uri']);
$item['author'] = $this->parseAuthor($articlePage);
$articleImage = $articlePage->find('figure')[0];
$article = $articlePage->find('div.first-column div.body')[0];
//remove ads
foreach ($article->find('aside') as $ad) {
$ad->outertext = '';
}
$summary = $articlePage->find('div.summary')[0];
//these are supposed to be hidden
foreach ($article->find('.visually-hidden') as $found) {
$found->outertext = '';
}
$item['content'] = $summary . $articleImage . $article;
array_push($this->items, $item);
}
}
private function parseAuthor($articlePage)
{
$authorDivs = $articlePage->find('div address');
if (!$authorDivs) {
return '';
}
$a = $authorDivs[0]->find('a')[0];
if ($a) {
return $a->innertext;
}
return $authorDivs[0]->innertext;
} }
} }

View File

@ -1,75 +0,0 @@
<?php
class CaschyBridge extends FeedExpander
{
const MAINTAINER = 'Tone866';
const NAME = 'Caschys Blog Bridge';
const URI = 'https://stadt-bremerhaven.de/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns the full articles instead of only the intro';
const PARAMETERS = [[
'category' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'Alle News'
=> 'https://stadt-bremerhaven.de/feed/'
]
],
'limit' => [
'name' => 'Limit',
'type' => 'number',
'required' => false,
'title' => 'Specify number of full articles to return',
'defaultValue' => 5
]
]];
const LIMIT = 5;
public function collectData()
{
$this->collectExpandableDatas(
$this->getInput('category'),
$this->getInput('limit') ?: static::LIMIT
);
}
protected function parseItem(array $item)
{
if (strpos($item['uri'], 'https://stadt-bremerhaven.de/') !== 0) {
return $item;
}
$article = getSimpleHTMLDOMCached($item['uri']);
if ($article) {
$article = defaultLinkTo($article, $item['uri']);
$item = $this->addArticleToItem($item, $article);
}
return $item;
}
private function addArticleToItem($item, $article)
{
// remove unwanted stuff
foreach (
$article->find('div.aawp, p.aawp-disclaimer, iframe.wp-embedded-content,
div.wp-embed, p.wp-caption-text, script') as $element
) {
$element->remove();
}
// reload html, as remove() is buggy
$article = str_get_html($article->outertext);
$categories = $article->find('div.post-category a');
foreach ($categories as $category) {
$item['categories'][] = $category->plaintext;
}
$content = $article->find('div.entry-inner', 0);
$item['content'] = $content;
return $item;
}
}

View File

@ -1,71 +1,63 @@
<?php <?php
class CastorusBridge extends BridgeAbstract {
class CastorusBridge extends BridgeAbstract
{
const MAINTAINER = 'logmanoriginal'; const MAINTAINER = 'logmanoriginal';
const NAME = 'Castorus Bridge'; const NAME = 'Castorus Bridge';
const URI = 'https://www.castorus.com'; const URI = 'https://www.castorus.com';
const CACHE_TIMEOUT = 600; // 10min const CACHE_TIMEOUT = 600; // 10min
const DESCRIPTION = 'Returns the latest changes'; const DESCRIPTION = 'Returns the latest changes';
const PARAMETERS = [ const PARAMETERS = array(
'Get latest changes' => [], 'Get latest changes' => array(),
'Get latest changes via ZIP code' => [ 'Get latest changes via ZIP code' => array(
'zip' => [ 'zip' => array(
'name' => 'ZIP code', 'name' => 'ZIP code',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'exampleValue' => '7', 'exampleValue' => '7',
'title' => 'Insert ZIP code (complete or partial). e.g: 78125 OR 781 OR 7' 'title' => 'Insert ZIP code (complete or partial). e.g: 78125 OR 781 OR 7'
] )
], ),
'Get latest changes via city name' => [ 'Get latest changes via city name' => array(
'city' => [ 'city' => array(
'name' => 'City name', 'name' => 'City name',
'type' => 'text', 'type' => 'text',
'required' => true, 'required' => true,
'exampleValue' => 'Paris', 'exampleValue' => 'Paris',
'title' => 'Insert city name (complete or partial). e.g: Paris OR Par OR P' 'title' => 'Insert city name (complete or partial). e.g: Paris OR Par OR P'
] )
] )
]; );
// Extracts the title from an actitiy // Extracts the title from an actitiy
private function extractActivityTitle($activity) private function extractActivityTitle($activity){
{
$title = $activity->find('a', 0); $title = $activity->find('a', 0);
if (!$title) { if(!$title)
returnServerError('Cannot find title!'); returnServerError('Cannot find title!');
}
return trim($title->plaintext); return htmlspecialchars(trim($title->plaintext));
} }
// Extracts the url from an actitiy // Extracts the url from an actitiy
private function extractActivityUrl($activity) private function extractActivityUrl($activity){
{
$url = $activity->find('a', 0); $url = $activity->find('a', 0);
if (!$url) { if(!$url)
returnServerError('Cannot find url!'); returnServerError('Cannot find url!');
}
return self::URI . $url->href; return self::URI . $url->href;
} }
// Extracts the time from an activity // Extracts the time from an activity
private function extractActivityTime($activity) private function extractActivityTime($activity){
{
// Unfortunately the time is part of the parent node, // Unfortunately the time is part of the parent node,
// so we have to clear all child nodes first // so we have to clear all child nodes first
$nodes = $activity->find('*'); $nodes = $activity->find('*');
if (!$nodes) { if(!$nodes)
returnServerError('Cannot find nodes!'); returnServerError('Cannot find nodes!');
}
foreach ($nodes as $node) { foreach($nodes as $node) {
$node->outertext = ''; $node->outertext = '';
} }
@ -73,36 +65,31 @@ class CastorusBridge extends BridgeAbstract
} }
// Extracts the price change // Extracts the price change
private function extractActivityPrice($activity) private function extractActivityPrice($activity){
{
$price = $activity->find('span', 1); $price = $activity->find('span', 1);
if (!$price) { if(!$price)
returnServerError('Cannot find price!'); returnServerError('Cannot find price!');
}
return $price->innertext; return $price->innertext;
} }
public function collectData() public function collectData(){
{
$zip_filter = trim($this->getInput('zip')); $zip_filter = trim($this->getInput('zip'));
$city_filter = trim($this->getInput('city')); $city_filter = trim($this->getInput('city'));
$html = getSimpleHTMLDOM(self::URI); $html = getSimpleHTMLDOM(self::URI);
if (!$html) { if(!$html)
returnServerError('Could not load data from ' . self::URI . '!'); returnServerError('Could not load data from ' . self::URI . '!');
}
$activities = $html->find('div#activite > li'); $activities = $html->find('div#activite > li');
if (!$activities) { if(!$activities)
returnServerError('Failed to find activities!'); returnServerError('Failed to find activities!');
}
foreach ($activities as $activity) { foreach($activities as $activity) {
$item = []; $item = array();
$item['title'] = $this->extractActivityTitle($activity); $item['title'] = $this->extractActivityTitle($activity);
$item['uri'] = $this->extractActivityUrl($activity); $item['uri'] = $this->extractActivityUrl($activity);
@ -115,17 +102,13 @@ class CastorusBridge extends BridgeAbstract
. $this->extractActivityPrice($activity) . $this->extractActivityPrice($activity)
. '</p>'; . '</p>';
if ( if(isset($zip_filter)
isset($zip_filter) && !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)) {
&& !(substr($item['title'], 0, strlen($zip_filter)) === $zip_filter)
) {
continue; // Skip this item continue; // Skip this item
} }
if ( if(isset($city_filter)
isset($city_filter) && !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)) {
&& !(substr($item['title'], strpos($item['title'], ' ') + 1, strlen($city_filter)) === $city_filter)
) {
continue; // Skip this item continue; // Skip this item
} }

View File

@ -1,42 +1,40 @@
<?php <?php
class CdactionBridge extends BridgeAbstract class CdactionBridge extends BridgeAbstract {
{
const NAME = 'CD-ACTION bridge'; const NAME = 'CD-ACTION bridge';
const URI = 'https://cdaction.pl'; const URI = 'https://cdaction.pl';
const DESCRIPTION = 'Fetches the latest posts from given category.'; const DESCRIPTION = 'Fetches the latest posts from given category.';
const MAINTAINER = 'tomaszkane'; const MAINTAINER = 'tomaszkane';
const PARAMETERS = [ [ const PARAMETERS = array( array(
'category' => [ 'category' => array(
'name' => 'Kategoria', 'name' => 'Kategoria',
'type' => 'list', 'type' => 'list',
'values' => [ 'values' => array(
'Najnowsze (wszystkie)' => 'najnowsze', 'Najnowsze (wszystkie)' => 'najnowsze',
'Newsy' => 'newsy', 'Newsy' => 'newsy',
'Recenzje' => 'recenzje', 'Recenzje' => 'recenzje',
'Teksty' => [ 'Teksty' => array(
'Publicystyka' => 'publicystyka', 'Publicystyka' => 'publicystyka',
'Zapowiedzi' => 'zapowiedzi', 'Zapowiedzi' => 'zapowiedzi',
'Już graliśmy' => 'juz-gralismy', 'Już graliśmy' => 'juz-gralismy',
'Poradniki' => 'poradniki', 'Poradniki' => 'poradniki',
], ),
'Kultura' => 'kultura', 'Kultura' => 'kultura',
'Wideo' => 'wideo', 'Wideo' => 'wideo',
'Czasopismo' => 'czasopismo', 'Czasopismo' => 'czasopismo',
'Technologie' => [ 'Technologie' => array(
'Artykuły' => 'artykuly', 'Artykuły' => 'artykuly',
'Testy' => 'testy', 'Testy' => 'testy',
], ),
'Na luzie' => [ 'Na luzie' => array(
'Konkursy' => 'konkursy', 'Konkursy' => 'konkursy',
'Nadgodziny' => 'nadgodziny', 'Nadgodziny' => 'nadgodziny',
] )
] )
]] ))
]; );
public function collectData() public function collectData() {
{
$html = getSimpleHTMLDOM($this->getURI() . '/' . $this->getInput('category')); $html = getSimpleHTMLDOM($this->getURI() . '/' . $this->getInput('category'));
$newsJson = $html->find('script#__NEXT_DATA__', 0)->innertext; $newsJson = $html->find('script#__NEXT_DATA__', 0)->innertext;
@ -46,7 +44,7 @@ class CdactionBridge extends BridgeAbstract
$queriesIndex = $this->getInput('category') === 'najnowsze' ? 0 : 1; $queriesIndex = $this->getInput('category') === 'najnowsze' ? 0 : 1;
foreach ($newsJson->props->pageProps->dehydratedState->queries[$queriesIndex]->state->data->results as $news) { foreach ($newsJson->props->pageProps->dehydratedState->queries[$queriesIndex]->state->data->results as $news) {
$item = []; $item = array();
$item['uri'] = $this->getURI() . '/' . $news->category->slug . '/' . $news->slug; $item['uri'] = $this->getURI() . '/' . $news->category->slug . '/' . $news->slug;
$item['title'] = $news->title; $item['title'] = $news->title;
$item['timestamp'] = $news->publishedAt; $item['timestamp'] = $news->publishedAt;

View File

@ -1,266 +0,0 @@
<?php
class CentreFranceBridge extends BridgeAbstract
{
const NAME = 'Centre France Newspapers';
const URI = 'https://www.centrefrance.com/';
const DESCRIPTION = 'Common bridge for all Centre France group newspapers.';
const CACHE_TIMEOUT = 7200; // 2h
const MAINTAINER = 'quent1';
const PARAMETERS = [
'global' => [
'newspaper' => [
'name' => 'Newspaper',
'type' => 'list',
'values' => [
'La Montagne' => 'lamontagne.fr',
'Le Populaire du Centre' => 'lepopulaire.fr',
'La République du Centre' => 'larep.fr',
'Le Berry Républicain' => 'leberry.fr',
'L\'Yonne Républicaine' => 'lyonne.fr',
'L\'Écho Républicain' => 'lechorepublicain.fr',
'Le Journal du Centre' => 'lejdc.fr',
'L\'Éveil de la Haute-Loire' => 'leveil.fr',
'Le Pays' => 'le-pays.fr'
]
],
'remove-reserved-for-subscribers-articles' => [
'name' => 'Remove reserved for subscribers articles',
'type' => 'checkbox',
'title' => 'Filter out articles that are only available to subscribers'
],
'limit' => [
'name' => 'Limit',
'type' => 'number',
'title' => 'How many articles to fetch. 0 to disable.',
'required' => true,
'defaultValue' => 15
]
],
'Local news' => [
'locality-slug' => [
'name' => 'Locality slug',
'type' => 'text',
'required' => false,
'title' => 'Fetch articles for a specific locality. If not set, headlines from the front page will be used instead.',
'exampleValue' => 'moulins-03000'
],
]
];
private static array $monthNumberByFrenchName = [
'janvier' => 1, 'février' => 2, 'mars' => 3, 'avril' => 4, 'mai' => 5, 'juin' => 6, 'juillet' => 7,
'août' => 8, 'septembre' => 9, 'octobre' => 10, 'novembre' => 11, 'décembre' => 12
];
public function collectData()
{
$value = $this->getInput('limit');
if (is_numeric($value) && (int)$value >= 0) {
$limit = $value;
} else {
$limit = static::PARAMETERS['global']['limit']['defaultValue'];
}
if (empty($this->getInput('newspaper'))) {
return;
}
$localitySlug = $this->getInput('locality-slug') ?? '';
$alreadyFoundArticlesURIs = [];
$newspaperUrl = 'https://www.' . $this->getInput('newspaper') . '/' . $localitySlug . '/';
$html = getSimpleHTMLDOM($newspaperUrl);
// Articles are detected through their titles
foreach ($html->find('.c-titre') as $articleTitleDOMElement) {
$articleLinkDOMElement = $articleTitleDOMElement->find('a', 0);
// Ignore articles in the « Les + partagés » block
if (strpos($articleLinkDOMElement->id, 'les_plus_partages') !== false) {
continue;
}
$articleURI = $articleLinkDOMElement->href;
// If the URI has already been processed, ignore it
if (in_array($articleURI, $alreadyFoundArticlesURIs, true)) {
continue;
}
// If news are filtered for a specific locality, filter out article for other localities
if ($localitySlug !== '' && !str_contains($articleURI, $localitySlug)) {
continue;
}
$articleTitle = '';
// If article is reserved for subscribers
if ($articleLinkDOMElement->find('span.premium-picto', 0)) {
if ($this->getInput('remove-reserved-for-subscribers-articles') === true) {
continue;
}
$articleTitle .= '🔒 ';
}
$articleTitleDOMElement = $articleLinkDOMElement->find('span[data-tb-title]', 0);
if ($articleTitleDOMElement === null) {
continue;
}
if ($limit > 0 && count($this->items) === $limit) {
break;
}
$articleTitle .= $articleLinkDOMElement->find('span[data-tb-title]', 0)->innertext;
$articleFullURI = urljoin('https://www.' . $this->getInput('newspaper') . '/', $articleURI);
$item = [
'title' => $articleTitle,
'uri' => $articleFullURI,
...$this->collectArticleData($articleFullURI)
];
$this->items[] = $item;
$alreadyFoundArticlesURIs[] = $articleURI;
}
}
private function collectArticleData($uri): array
{
$html = getSimpleHTMLDOMCached($uri, 86400 * 90); // 90d
$item = [
'enclosures' => [],
];
$articleInformations = $html->find('#content hgroup > div.typo-p3 > *');
if (is_array($articleInformations) && $articleInformations !== []) {
$publicationDateIndex = 0;
// Article author
$probableAuthorName = strip_tags($articleInformations[0]->innertext);
if (str_starts_with($probableAuthorName, 'Par ')) {
$publicationDateIndex = 1;
$item['author'] = substr($probableAuthorName, 4);
}
// Article publication date
preg_match('/Publié le (\d{2}) (.+) (\d{4})( à (\d{2})h(\d{2}))?/', strip_tags($articleInformations[$publicationDateIndex]->innertext), $articleDateParts);
if ($articleDateParts !== [] && array_key_exists($articleDateParts[2], self::$monthNumberByFrenchName)) {
$articleDate = new \DateTime('midnight');
$articleDate->setDate($articleDateParts[3], self::$monthNumberByFrenchName[$articleDateParts[2]], $articleDateParts[1]);
if (count($articleDateParts) === 7) {
$articleDate->setTime($articleDateParts[5], $articleDateParts[6]);
}
$item['timestamp'] = $articleDate->getTimestamp();
}
}
$articleContent = $html->find('#content>div.flex+div.grid section>.z-10')[0] ?? null;
if ($articleContent instanceof \simple_html_dom_node) {
$articleHiddenParts = $articleContent->find('.ad-slot, #cf-digiteka-player');
if (is_array($articleHiddenParts)) {
foreach ($articleHiddenParts as $articleHiddenPart) {
$articleContent->removeChild($articleHiddenPart);
}
}
$item['content'] = $articleContent->innertext;
}
$articleIllustration = $html->find('#content>div.flex+div.grid section>figure>img');
if (is_array($articleIllustration) && count($articleIllustration) === 1) {
$item['enclosures'][] = $articleIllustration[0]->getAttribute('src');
}
$articleAudio = $html->find('audio[src^="https://api.octopus.saooti.com/"]');
if (is_array($articleAudio) && count($articleAudio) === 1) {
$item['enclosures'][] = $articleAudio[0]->getAttribute('src');
}
$articleTags = $html->find('#content>div.flex+div.grid section>.bg-gray-light>a.border-gray-dark');
if (is_array($articleTags)) {
$item['categories'] = array_map(static fn ($articleTag) => $articleTag->innertext, $articleTags);
}
$explode = explode('_', $uri);
$array_reverse = array_reverse($explode);
$string = $array_reverse[0];
$uid = rtrim($string, '/');
if (is_numeric($uid)) {
$item['uid'] = $uid;
}
// If the article is a "grand format", we use another parsing strategy
if ($item['content'] === '' && $html->find('article') !== []) {
$articleContent = $html->find('article > section');
foreach ($articleContent as $contentPart) {
if ($contentPart->find('#journo') !== []) {
$item['author'] = $contentPart->find('#journo')->innertext;
continue;
}
$item['content'] .= $contentPart->innertext;
}
}
$item['content'] = str_replace('<span class="p-premium">premium</span>', '🔒', $item['content']);
$item['content'] = trim($item['content']);
return $item;
}
public function getName()
{
if (empty($this->getInput('newspaper'))) {
return static::NAME;
}
$newspaperNameByDomain = array_flip(self::PARAMETERS['global']['newspaper']['values']);
if (!isset($newspaperNameByDomain[$this->getInput('newspaper')])) {
return static::NAME;
}
$completeTitle = $newspaperNameByDomain[$this->getInput('newspaper')];
if (!empty($this->getInput('locality-slug'))) {
$localityName = explode('-', $this->getInput('locality-slug'));
array_pop($localityName);
$completeTitle .= ' ' . ucfirst(implode('-', $localityName));
}
return $completeTitle;
}
public function getIcon()
{
if (empty($this->getInput('newspaper'))) {
return static::URI . '/favicon.ico';
}
return 'https://www.' . $this->getInput('newspaper') . '/favicon.ico';
}
public function detectParameters($url)
{
$regex = '/^(https?:\/\/)?(www\.)?([a-z-]+\.fr)(\/)?([a-z-]+-[0-9]{5})?(\/)?$/';
$url = strtolower($url);
if (preg_match($regex, $url, $urlMatches) === 0) {
return null;
}
if (!in_array($urlMatches[3], self::PARAMETERS['global']['newspaper']['values'], true)) {
return null;
}
return [
'newspaper' => $urlMatches[3],
'locality-slug' => empty($urlMatches[5]) ? null : $urlMatches[5]
];
}
}

View File

@ -1,25 +1,41 @@
<?php <?php
class CeskaTelevizeBridge extends BridgeAbstract class CeskaTelevizeBridge extends BridgeAbstract {
{
const NAME = 'Česká televize Bridge'; const NAME = 'Česká televize Bridge';
const URI = 'https://www.ceskatelevize.cz'; const URI = 'https://www.ceskatelevize.cz';
const CACHE_TIMEOUT = 3600; const CACHE_TIMEOUT = 3600;
const DESCRIPTION = 'Return newest videos'; const DESCRIPTION = 'Return newest videos';
const MAINTAINER = 'kolarcz'; const MAINTAINER = 'kolarcz';
const PARAMETERS = [ const PARAMETERS = array(
[ array(
'url' => [ 'url' => array(
'name' => 'url to the show', 'name' => 'url to the show',
'required' => true, 'required' => true,
'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/' 'exampleValue' => 'https://www.ceskatelevize.cz/porady/1097181328-udalosti/'
] )
] )
]; );
public function collectData() private function fixChars($text) {
{ return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
private function getUploadTimeFromString($string) {
if (strpos($string, 'dnes') !== false) {
return strtotime('today');
} elseif (strpos($string, 'včera') !== false) {
return strtotime('yesterday');
} elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
returnServerError('Could not get date from Česká televize string');
}
$date = sprintf('%04d-%02d-%02d', isset($match[3]) ? $match[3] : date('Y'), $match[2], $match[1]);
return strtotime($date);
}
public function collectData() {
$url = $this->getInput('url'); $url = $this->getInput('url');
$validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/'; $validUrl = '/^(https:\/\/www\.ceskatelevize\.cz\/porady\/\d+-[a-z0-9-]+\/)(bonus\/)?$/';
@ -27,7 +43,7 @@ class CeskaTelevizeBridge extends BridgeAbstract
returnServerError('Invalid url'); returnServerError('Invalid url');
} }
$category = $match[4] ?? 'nove'; $category = isset($match[4]) ? $match[4] : 'nove';
$fixedUrl = "{$match[1]}dily/{$category}/"; $fixedUrl = "{$match[1]}dily/{$category}/";
$html = getSimpleHTMLDOM($fixedUrl); $html = getSimpleHTMLDOM($fixedUrl);
@ -38,46 +54,30 @@ class CeskaTelevizeBridge extends BridgeAbstract
$this->feedName .= " ({$category})"; $this->feedName .= " ({$category})";
} }
foreach ($html->find('#episodeListSection a[data-testid=card]') as $element) { foreach ($html->find('#episodeListSection a[data-testid=next-link]') as $element) {
$itemContent = $element->find('p[class^=content-]', 0); $itemTitle = $element->find('h3', 0);
$itemDate = $element->find('div[class^=playTime-] span, [data-testid=episode-item-broadcast] span', 0); $itemContent = $element->find('div[class^=content-]', 0);
$item = [ $itemDate = $element->find('div[class^=playTime-] span', 0);
'title' => $this->fixChars($element->find('h3', 0)->plaintext), $itemThumbnail = $element->find('img', 0);
'uri' => self::URI . $element->getAttribute('href'), $itemUri = self::URI . $element->getAttribute('href');
'content' => '<img src="' . $element->find('img', 0)->getAttribute('srcset') . '" /><br />' . $this->fixChars($itemContent->plaintext),
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext), $item = array(
]; 'title' => $this->fixChars($itemTitle->plaintext),
'uri' => $itemUri,
'content' => '<img src="' . $itemThumbnail->getAttribute('src') . '" /><br />'
. $this->fixChars($itemContent->plaintext),
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext)
);
$this->items[] = $item; $this->items[] = $item;
} }
} }
private function getUploadTimeFromString($string) public function getURI() {
{ return isset($this->feedUri) ? $this->feedUri : parent::getURI();
if (strpos($string, 'dnes') !== false) {
return strtotime('today');
} elseif (strpos($string, 'včera') !== false) {
return strtotime('yesterday');
} elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
returnServerError('Could not get date from Česká televize string');
} }
$date = sprintf('%04d-%02d-%02d', $match[3] ?? date('Y'), $match[2], $match[1]); public function getName() {
return strtotime($date); return isset($this->feedName) ? $this->feedName : parent::getName();
}
private function fixChars($text)
{
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
}
public function getURI()
{
return $this->feedUri ?? parent::getURI();
}
public function getName()
{
return $this->feedName ?? parent::getName();
} }
} }

Some files were not shown because too many files have changed in this diff Show More