Merge branch 'feature/rss-manual-parsing' into develop
This commit is contained in:
commit
fb2e1de03b
6
.github/workflows/android.yml
vendored
6
.github/workflows/android.yml
vendored
@ -3,12 +3,10 @@ name: Android CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -43,26 +43,33 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(':db')
|
||||
|
||||
// xpp3 has a conflict with kxml when running connectedCheck task
|
||||
configurations {
|
||||
all*.exclude group: 'xpp3', module: 'xpp3'
|
||||
}
|
||||
|
||||
implementation "androidx.core:core-ktx:1.2.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.1'
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
|
||||
implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:0.12'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.8.1'
|
||||
|
||||
implementation('com.squareup.retrofit2:retrofit:2.7.1') {
|
||||
exclude group: 'okhttp3', module: 'okhttp3'
|
||||
}
|
||||
implementation('com.squareup.retrofit2:converter-moshi:2.7.1') {
|
||||
exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2
|
||||
}
|
||||
|
||||
implementation 'com.squareup.retrofit2:converter-simplexml:2.7.1'
|
||||
implementation ('com.squareup.retrofit2:converter-simplexml:2.7.1') {
|
||||
exclude module: 'stax'
|
||||
exclude module: 'stax-api'
|
||||
exclude module: 'xpp3'
|
||||
}
|
||||
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1'
|
||||
|
||||
implementation 'com.squareup.moshi:moshi:1.9.2'
|
||||
|
9
api/src/androidTest/assets/localfeed/atom/atom_feed.xml
Normal file
9
api/src/androidTest/assets/localfeed/atom/atom_feed.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
|
||||
<id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<subtitle>Here is a subtitle</subtitle>
|
||||
</feed>
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
|
||||
<id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<subtitle>Here is a subtitle</subtitle>
|
||||
</feed>
|
71
api/src/androidTest/assets/localfeed/atom/atom_items.xml
Normal file
71
api/src/androidTest/assets/localfeed/atom/atom_items.xml
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
|
||||
<id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
|
||||
<title>Add an option to open item url in custom tab</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<summary>Summary</summary>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre>
|
||||
</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/e0945823eecf269e5beea646ac5d7e630e08afbf</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/e0945823eecf269e5beea646ac5d7e630e08afbf"/>
|
||||
<title>
|
||||
Use gradle parallel builds
|
||||
</title>
|
||||
<updated>2020-09-05T13:28:23Z</updated>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Use gradle parallel builds</pre>
|
||||
</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/85fcf03e64d8b482e4d2af8c2bcd1509d946944f</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/85fcf03e64d8b482e4d2af8c2bcd1509d946944f"/>
|
||||
<title>
|
||||
Use clear text mode for the feed url text input in AddFeedActivity
|
||||
</title>
|
||||
<updated>2020-09-05T12:23:48Z</updated>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Use clear text mode for the feed url text input in AddFeedActivity</pre>
|
||||
</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/d59e38ee9d11da186131b602425231eff0896956</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/d59e38ee9d11da186131b602425231eff0896956"/>
|
||||
<title>
|
||||
Use project level okhttp client with glide
|
||||
</title>
|
||||
<updated>2020-09-05T12:05:16Z</updated>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Use project level okhttp client with glide</pre>
|
||||
</content>
|
||||
</entry>
|
||||
</feed>
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
|
||||
<id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
|
||||
<title>Add an option to open item url in custom tab</title>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<summary>Summary</summary>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre>
|
||||
</content>
|
||||
</entry>
|
||||
</feed>
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
|
||||
<id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
|
||||
<title>Add an option to open item url in custom tab</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<summary>Summary</summary>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre>
|
||||
</content>
|
||||
</entry>
|
||||
</feed>
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
|
||||
<id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
|
||||
<link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&u=c56b216e8d128d0ec217062feeace9faca4dc893&v=4"/>
|
||||
<author>
|
||||
<name>Shinokuni</name>
|
||||
<uri>https://github.com/Shinokuni</uri>
|
||||
</author>
|
||||
<summary>Summary</summary>
|
||||
<content type="html">
|
||||
<pre style='white-space:pre-wrap;width:81ex'>Add an option to open item url in custom tab</pre>
|
||||
</content>
|
||||
</entry>
|
||||
</feed>
|
86
api/src/androidTest/assets/localfeed/json/json_feed.json
Normal file
86
api/src/androidTest/assets/localfeed/json/json_feed.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"version": "https://jsonfeed.org/version/1",
|
||||
"title": "News from Flying Meat",
|
||||
"home_page_url": "http://flyingmeat.com/blog/",
|
||||
"feed_url": "http://flyingmeat.com/blog/feed.json",
|
||||
"description": "News from your friends at Flying Meat.",
|
||||
"author": {
|
||||
"name": "Gus Mueller"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"title": "Acorn and 10.13",
|
||||
"content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I'm happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.</p>\n",
|
||||
"date_published": "2017-09-25T14:27:27-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"author": {
|
||||
"url": "this is an url",
|
||||
"name": "Author 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html",
|
||||
"title": "Acorn 6.1 Is Out",
|
||||
"content_html": "<p><a href=\"https://flyingmeat.com/acorn/\">Acorn 6.1 has been released</a>.</p>\n<p>You can <a href=\"http://shapeof.com/archives/2018/2/acorn_6.1_is_out.html\">read a longer post about it</a> over on Gus's blog, but the short of it is: Better, faster, smoother, stronger. And now with Metal 2 support.</p>\n",
|
||||
"date_published": "2018-02-16T09:59:11-08:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html",
|
||||
"title": "A Pair of Updates",
|
||||
"content_html": "<p>Happy summer solstice everybody! (at least for folks in the northern hemisphere, and for folks in the south… sorry. It's going to start getting brighter for you though).</p>\n<p>Today I've got a pair of minor app updates to annouce for you.</p>\n<p>First up is <a href=\"https://flyingmeat.com/acorn/\">Acorn 6.1.3</a>, which <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">fixes a number of bugs</a> including one that stemmed from trying to use QuickLook on a file that was created with Acorn 1.0. For the one or two of you that this was affecting, hurray!</p>\n<p>Next up is <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch</a>, which also <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">includes some bug fixes</a>, the beginnings of Voice Over support, performance improvements, and more.</p>\n<p>What's next for these apps? Work on Acorn 6.2 will begin shortly, as will Retrobatch 1.1. WWDC introduced some great new APIs that I want to take advantage of (cool new machine learning things), so that'll be a focus- as well as Dark Mode for Acorn and one other major thing I've got planned. Retrobatch will probably also get the Dark Mode treatment, but not until I've done it for Acorn first.</p>\n<p>So it's going to be a busy summer, but I'm looking forward to it.</p>\n",
|
||||
"date_published": "2018-06-21T10:18:46-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2018/9/retrobatch_1.1_is_out.html",
|
||||
"title": "Retrobatch 1.1 Is Out",
|
||||
"content_html": "<p>Here's something new for your lazy <strike>August</strike> September* morning: <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch 1.1 is out</a>.</p>\n<p>What's new and awesome? Well, <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch</a> now has some great scripting goodness in the form of a new Automator action which will run a workflow for you (and create Automator droplets), a new JavaScript node*, and the ability to run Retrobatch workflows from the terminal.</p>\n<p>We've added a handful of new nodes such as Dither, Auto Enhance, Instant Alpha, and Color Posterize. New options to existing nodes have also shown up, such as "Only scale smaller" for the Scale node.</p>\n<p>And an interesting idea that I've had folks ask about a number of times- it's now possible to run an image through a machine learning classifier, and then have the classification written to metadata such as the image title, or keywords. This was done by adding token support to the Set Specific Metadata node. This also means you can use other tokens such as the Current Year in metadata fields. Awesome? We think so.</p>\n<p>The <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">full release notes are available</a>, and if you have ideas or questions- make sure to <a href=\"https://forums.flyingmeat.com/\">poke around on the forums</a> or write us: <a href=\"mailto:support@flyingmeat.com\">support@flyingmeat.com</a>. We've got lots of ideas for future releases, but if you'd like something specific in there make sure to let us know.</p>\n<br/>\n\n<div style=\"color:#666\">\n* Whoa, it's September already?<br/><br/>\n\n<p>**I'm calling the JavaScript node a "preview". It works very well, but I'm not 100% sold on the API that I've provided to folks. So this is a disclaimer that it might change a little bit in the future.</div></p>\n",
|
||||
"date_published": "2018-09-07T09:43:10-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2018/9/retrobatch_1.1_is_out.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2018/9/acorn_6.2_with_mojave_dark_mode_is_out.html",
|
||||
"title": "Acorn 6.2 With Mojave Dark Mode Is Out",
|
||||
"content_html": "<p>On Monday I flipped some switches on the FM servers and <a href=\"https://flyingmeat.com/acorn/\">Acorn 6.2</a> was released to the universe. You might also remember that Monday a little known operating system from Apple was updated, which includes a neat new feature known as Dark Mode.</p>\n<center><img src=\"https://shapeof.com/archives/2018/media/acorn62.jpeg\" width=\"800\" style=\"\" /></center>\n\n<p>I think Acorn looks pretty good in Dark Aqua, especially the icon refresh from <a href=\"http://www.matthewskiles.com/\">Matthew Skiles</a>.</p>\n<p>To celebrate the new release, we've put <a href=\"https://flyingmeat.com/store/\">Acorn on sale for 50% off</a>. So go grab it at the insanely low price of $14.99. If you haven't already upgraded from previous versions of Acorn, now is a good time to do so.</p>\n<p>We've also packed a bunch of little changes, bug fixes, and compatibility with Mojave in there. And of course, there's more to come in the future as always.</p>\n",
|
||||
"date_published": "2018-09-27T14:37:21-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2018/9/acorn_6.2_with_mojave_dark_mode_is_out.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2019/1/acorn_6.3_is_out.html",
|
||||
"title": "Acorn 6.3 Is Out",
|
||||
"content_html": "<p><a href=\"https://flyingmeat.com/acorn/\">Acorn 6.3 is available</a>, and the full <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">release notes</a> are up as well.</p>\n<p>Here's what I think is awesome in this release:</p>\n<p><strong>Portrait Mask Support</strong>. If you have an iPhone running iOS 12 (and can take Portrait photos), Acorn will now detect the Portrait Matte from those images and turn it into a layer mask. The Portrait Matte is the image data which enables blurring in the background, or other fancy camera tricks. This means you can use this matte to erase and add fancy backgrounds or custom blurs for your image, all within Acorn.</p>\n<p><strong>Other Mask Features</strong>. You can now drag and drop masks from the layers list into another layer, or copy it out as a new layer. When exporting layers you now have an option to apply the mask on export, or just write it as an additional image along with everything else. There are a number of new shortcuts when dealing with layer masks as well.</p>\n<p><strong>Brush Stuff</strong>. If you're running MacOS 10.13 or later, you get a performance boost when brushing (painting, smudging, cloning, etc…). This is especially noticeable when brusing on deep color images.</p>\n<p>I've also added options to the brush palette for adjusting flow, softness and blending. In addition to all this, there's a bunch of new brushes under the "Basic Round" category which are designed for the new brush engine.</p>\n<p><strong>Other Stuff</strong>. There's other good things including improved PDF export, various MacOS Mojave UI fixes, additional speed improvements with with deep images, and more. And as always, it's a free upgrade for anyone who has already purchased Acorn 6.</p>\n",
|
||||
"date_published": "2019-01-09T13:00:07-08:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2019/1/acorn_6.3_is_out.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2019/4/retrobatch_1.2_released.html",
|
||||
"title": "Retrobatch 1.2 Released",
|
||||
"content_html": "<p>We're happy to announce that Retrobatch 1.2 has now been released, which is a free update for all owners of Retrobatch. Highlights of this release include:</p>\n<ul>\n<li><p><strong>Create animated GIF and PNG</strong> images with the Animated Image node. When using Retrobatch you can load in a folder of images and produce an optimized animated image with options for setting the frame rate, format, as well as letting the image loop or not.</p>\n</li>\n<li><p><strong>New nodes</strong> including "Round Corner", "Image Grid", and "Limit". We've also added improvements to the Write node allowing you to write back to the original processed image.</p>\n</li>\n<li><p><strong>Droplet support</strong> (Retrobatch Pro). Turn your workflow into an an application which you can drag and drop images onto. The droplet can work anywhere an application normally would, even in the Dock.</p>\n</li>\n<li><p><strong>Write Plug-Ins using JavaScript</strong> (Retrobatch Pro). Using the combined power of JavaScript and the native to MacOS Cocoa APIs, you can <a href=\"https://flyingmeat.com/retrobatch/jsapi/\">make and distribute</a> new plugins for Retrobatch. Got an idea for a plug-in and you want to use Core Image to make it? Or maybe you want to use Core Graphics to add some funky text to your images? Now you can do this with JavaScript and Cocoa.</p>\n</li>\n</ul>\n<p>The <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">full release notes are available</a>, as well as information on bug fixes we delivered in this update.</p>\n<p>As always, we're <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">always listening for feedback</a> and feature requests. And don't forget to head over to the <a href=\"http://forums.flyingmeat.com/c/retrobatch\">Retrobatch community formus</a> to chat with us and other Retrobatch users. </p>\n",
|
||||
"date_published": "2019-04-01T13:38:21-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2019/4/retrobatch_1.2_released.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2019/10/catalina_ready.html",
|
||||
"title": "Catalina Ready",
|
||||
"content_html": "<p>MacOS 10.15 Catalina was just released, and we're happy to let you know that both <a href=\"https://flyingmeat.com/acorn/\">Acorn 6.5.1</a> and <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch 1.2</a> are compatible with it.</p>\n<p>And to celebrate the release of Catalina, we're <a href=\"https://flyingmeat.com/store/\">discounting Acorn by 50% for a limited time</a>. So if you haven't upgraded yet, now is a good time.</p>\n",
|
||||
"date_published": "2019-10-07T10:48:03-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2019/10/catalina_ready.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2020/3/retrobatch_1.4_is_out.html",
|
||||
"title": "Retrobatch 1.4 Is Out",
|
||||
"content_html": "<p>I've just typed the magic commands* and let the servers do their thing and now <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch 1.4</a> is loose on the world.</p>\n<p>There's a couple of interesting new features in this update I'd like to call out. First up is JavaScript expressions in Retrobatch Pro. Various nodes in Retrobatch which allow you to set the size or length of a value (such as the Crop, Border, Gradient, Adjust Margin nodes) now have an option of running a little snippet of JavaScript code to figure out the value. This is a super powerful feature, which you can read about in our <a href=\"https://flyingmeat.com/retrobatch/jsapi-1/jsexpressions/\">JavaScript Expressions documentation</a></p>\n<p>Let's say you have some images of varying sizes, which are all at 480 x 380 or smaller, and you want them to expand to meet that size. But- you only want it to grow evenly on either side of the image, but you want to keep a baseline so only transparent area is added to the top of the image, and the bottom stays in the same spot. This little picture of the new Adjust Margins node shows how this can be done:</p>\n<center><img src=\"https://flyingmeat.com/retrobatch/jsapi-1/images/javascript_expression_fields_shot.png\" width=\"444\" style=\"border: solid 1px #777;\" /></center>\n\n<p>Yes, this is an oddball (and very real) case- but there's a billion of these little oddball cases out there. With the new JavaScript expressions support, these small but hard to do scenarios are now super easy.</p>\n<p>And yes, all of the JavaScript support in Retrobatch now sits atop <a href=\"https://github.com/ccgus/fmjs\">FMJS</a>, which any developer can use to build similar support into their apps.</p>\n<p>What else is new?</p>\n<p>File numbers with leading zeros for the Write node. You can add (and it's case sensitive) $FileNumber04$ in the File name: field of the Write node to have the file number of your image written out as part of the name, with a padding of up to 4 zeros. If you'd like to pad that number to 6, you would enter $FileNumber06$, and so on.</p>\n<p>The Mask to Alpha node got a new "invert colors" option. Normally Mask to Alpha will convert the black areas of your image to transparent, and the white to opaque (with gray somewhere inbetween). With the new Invert Colors option, Mask to Alpha will now convert the white areas of your image to transparent, and keep the black opaque. This is great if you are scanning in line drawings from your own artwork, and want to make the backgrounds transparent.</p>\n<p>This request comes up a lot in <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> as well. Previously you'd have to add an Invert Colors node (or filter for Acorn), then the Mask to Alpha, and then Invert Colors again. Now it's just a checkbox in Mask to Alpha, which is super easy. I've also added an update to the same filter in Acorn for the next release. You can grab a preview of it <a href=\"http://flyingmeat.com/download/latest/\">from here</a>.</p>\n<p>And finally for my short list, you can now make a droplet which doesn't take any files. Why is this useful? Well, imagine you have a workflow that reads an image from the clipboard, resizes it to a specific width, and then writes it back to the clipboard. Now you can make a little droplet to do just this. Just a double click from the Finder (or a single click from the Dock) and your workflow is run.</p>\n<p>The full release notes for Retrobatch 1.4 are <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">available in the usual place</a>.</p>\n<p>* <code>./bin/otbuild.sh -e 1.4</code></p>\n",
|
||||
"date_published": "2020-03-31T14:37:15-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2020/3/retrobatch_1.4_is_out.html"
|
||||
},
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2020/5/acorn_6.6_released.html",
|
||||
"title": "Acorn 6.6 is out with new Shape Processors and more",
|
||||
"content_html": "<p><a href=\"https://flyingmeat.com/acorn/\">Acorn 6.6</a> is out. You can update to this release via the <a href=\"https://flyingmeat.com/acorn/appstore/\">App Store</a> as or the Acorn ▸ Check for Updates… menu if you bought it directly from us.</p>\n<p>Originally this was going to be a bug fix release but I kept on adding useful things and it snowballed into a feature release. As usual, <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">the full release notes</a> have all the details about what was updated.</p>\n<p>The main new features are with the <a href=\"https://flyingmeat.com/acorn/docs/shape_processor.html\">Shape Processor</a>. If you're not already familiar with the shape processor, it's a neat ability Acorn has to take shapes on vector layers and pipe them through a series of actions, similar to how Automator or Acorn's bitmap filters work. Only instead of working on pixels, the processors will alter the shapes by scaling them or moving them around, or changing colors or blend modes. There's even a processor which will generate shapes for you- so if you want your canvas to fill up with hundreds of stars, you can do that.</p>\n<p>Acorn 6.6 adds new processors which let you set the stroke, fill, and blend mode of your processed shapes. You can now also flip your shapes and even shift colors. </p>\n<img src=\"https://shapeof.com/archives/2020/5/proc_shape.png\" width=\"660\" height=\"510\" />\n\n<p>Chaining these processors together can get you some neat looking images. You can make interesting desktop backgrounds, as well as textures for your photos. Or if you just need a bunch of hexagons arranged in a circle, that's just two processors stacked together.</p>\n<p>Have you made something interesting with the Shape Processor? I'd love to see it either via Twitter (I'm <a href=\"https://twitter.com/ccgus/\">@ccgus</a>) or via <a href=\"mailto:support@flyingmeat.com\">email</a>.</p>\n<p>There are of course the usual bug fixes and other <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">minor details</a>. And if you don't already have Acorn, a <a href=\"https://flyingmeat.com/acorn/\">no-strings attached free trial</a> is available on our website. Try it out, and we're always looking to hear from you about feature requests, thoughts, and anything else.</p>\n",
|
||||
"date_published": "2020-05-28T12:17:57-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2020/5/acorn_6.6_released.html"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"title": "Acorn and 10.13",
|
||||
"content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I'm happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.</p>\n",
|
||||
"url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"title": "Acorn and 10.13",
|
||||
"content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I'm happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.</p>\n",
|
||||
"date_published": "2017-09-25T14:27:27-07:00"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I'm happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I'm also working on some 10.13 goodies for Acorn 6 folks later this year. I can't wait to share that with you, but you'll have to wait just a little bit.</p>\n",
|
||||
"date_published": "2017-09-25T14:27:27-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"title": "Acorn and 10.13",
|
||||
"summary": "This is a summary",
|
||||
"content_html": "content_html",
|
||||
"content_text": "content_text",
|
||||
"date_published": "2017-09-25T14:27:27-07:00",
|
||||
"url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
|
||||
"image": "https://image.com",
|
||||
"authors": [
|
||||
{
|
||||
"url": "url 1",
|
||||
"name": "Author 1"
|
||||
},
|
||||
{
|
||||
"url": "url 2",
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"url": "url 3",
|
||||
"name": "Author 3"
|
||||
},
|
||||
{
|
||||
"url": "url 4",
|
||||
"name": "Author 4"
|
||||
},
|
||||
{
|
||||
"url": "url 5",
|
||||
"name": "Author 5"
|
||||
},
|
||||
{
|
||||
"url": "url 6",
|
||||
"name": "Author 6"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
269
api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml
Normal file
269
api/src/androidTest/assets/localfeed/rss1/rss1_feed.xml
Normal file
@ -0,0 +1,269 @@
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
|
||||
<channel rdf:about="https://slashdot.org/">
|
||||
<title>Slashdot</title>
|
||||
<link>https://slashdot.org/</link>
|
||||
<description>News for nerds, stuff that matters</description>
|
||||
<dc:language>en-us</dc:language>
|
||||
<dc:rights>Copyright 1997-2016, SlashdotMedia. All Rights Reserved.</dc:rights>
|
||||
<dc:date>2020-09-23T16:20:20+00:00</dc:date>
|
||||
<dc:publisher>Dice</dc:publisher>
|
||||
<dc:creator>help@slashdot.org</dc:creator>
|
||||
<dc:subject>Technology</dc:subject>
|
||||
<syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
|
||||
<syn:updateFrequency>1</syn:updateFrequency>
|
||||
<syn:updatePeriod>hourly</syn:updatePeriod>
|
||||
<items>
|
||||
<rdf:Seq>
|
||||
<rdf:li
|
||||
rdf:resource="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/0057256/jeff-bezos-is-opening-his-first-tuition-free-bezos-academy-preschool-where-each-child-will-be-the-customer?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/0050249/google-is-pulling-the-plug-on-paid-chrome-extensions-over-the-next-year?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://entertainment.slashdot.org/story/20/09/22/2147243/old-tv-caused-village-broadband-outages-for-18-months?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://mobile.slashdot.org/story/20/09/22/2316256/t-mobile-amassed-unprecedented-concentration-of-spectrum-att-complains?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://yro.slashdot.org/story/20/09/22/2030223/dark-web-drugs-raid-leads-to-179-arrests?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://hardware.slashdot.org/story/20/09/22/2055203/the-fairphone-3-is-a-repairable-dream-that-takes-beautiful-photos?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://tech.slashdot.org/story/20/09/23/0039222/tesla-unveils-model-s-plaid-520-miles-200-mph-and-0-60-mph-in-less-than-2s?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://apple.slashdot.org/story/20/09/22/211222/apple-ceo-impressed-by-remote-work-sees-permanent-changes?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://hardware.slashdot.org/story/20/09/22/236228/tesla-announces-tabless-battery-cells-that-will-improve-range-of-its-electric-cars?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://linux.slashdot.org/story/20/09/22/2243209/linux-journal-is-back?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://hardware.slashdot.org/story/20/09/22/2026238/shell-reportedly-to-slash-oil-and-gas-production-costs-to-focus-more-on-renewables?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
</rdf:Seq>
|
||||
</items>
|
||||
<image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
|
||||
<textinput rdf:resource="https://slashdot.org/search.pl" />
|
||||
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://rss.slashdot.org/slashdot/slashdotMain"
|
||||
rel="self" type="application/rdf+xml" />
|
||||
<feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"
|
||||
uri="slashdot/slashdotmain" />
|
||||
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://pubsubhubbub.appspot.com/"
|
||||
rel="hub" />
|
||||
</channel>
|
||||
<image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
|
||||
<title>Slashdot</title>
|
||||
<url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
|
||||
<link>https://slashdot.org/</link>
|
||||
</image>
|
||||
<item
|
||||
rdf:about="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Google Expands its Flutter Development Kit To Windows Apps</title>
|
||||
<link>
|
||||
https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed
|
||||
</link>
|
||||
<description>Google has announced that Flutter, its open source UI development kit for
|
||||
building cross-platform software from the same codebase, is finally available for
|
||||
Windows apps in alpha. From a report:For the world's leading desktop operating system
|
||||
with some 1 billion installations of Windows 10 alone, this has been a long time coming.
|
||||
Flutter's alpha incarnation was initially launched at Google's I/O developer conference
|
||||
back in 2017, before arriving in beta less than a year later. In its original guise,
|
||||
Flutter was designed for Android and iOS app development, but it has since expanded to
|
||||
cover the web, MacOS, and Linux, which are currently available in various alpha or beta
|
||||
iterations. Developers have had to consider unique platform-specific factors when
|
||||
designing for the desktop or mobile phones, such as different screen sizes and how
|
||||
people interact with their devices. On smartphones, people typically use touch and
|
||||
swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
|
||||
This means Flutter has had to expand its support to cover the additional inputs.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<content:encoded>content:encoded</content:encoded>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:date>2020-09-23T16:15:00+00:00</dc:date>
|
||||
<dc:subject>programming</dc:subject>
|
||||
<slash:department>how-about-that</slash:department>
|
||||
<slash:section>developers</slash:section>
|
||||
<slash:comments>1</slash:comments>
|
||||
<slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
|
||||
</item>
|
||||
<item
|
||||
rdf:about="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Firefox Usage is Down 85% Despite Mozilla's Top Exec Pay Going Up 400%</title>
|
||||
<link>
|
||||
https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&utm_medium=feed
|
||||
</link>
|
||||
<description>Software engineer Cal Paterson writes: Mozilla recently announced that they
|
||||
would be dismissing 250 people. That's a quarter of their workforce so there are some
|
||||
deep cuts to their work too. The victims include: the MDN docs (those are the web
|
||||
standards docs everyone likes better than w3schools), the Rust compiler and even some
|
||||
cuts to Firefox development. Like most people I want to see Mozilla do well but those
|
||||
three projects comprise pretty much what I think of as the whole point of Mozilla, so
|
||||
this news is a a big let down. The stated reason for the cuts is falling income. Mozilla
|
||||
largely relies on "royalties" for funding. In return for payment, Mozilla allows big
|
||||
technology companies to choose the default search engine in Firefox - the technology
|
||||
companies are ultimately paying to increase the number of searches Firefox users make
|
||||
with them. Mozilla haven't been particularly transparent about why these royalties are
|
||||
being reduced, except to blame the coronavirus. I'm sure the coronavirus is not a great
|
||||
help but I suspect the bigger problem is that Firefox's market share is now a tiny
|
||||
fraction of its previous size and so the royalties will be smaller too - fewer users, so
|
||||
fewer searches and therefore less money for Mozilla.
|
||||
|
||||
The real problem is not the royalty cuts, though. Mozilla has already received more than
|
||||
enough money to set themselves up for financial independence. Mozilla received up to
|
||||
half a billion dollars a year (each year!) for many years. The real problem is that
|
||||
Mozilla didn't use that money to achieve financial independence and instead just spent
|
||||
it each year, doing the organisational equivalent of living hand-to-mouth. Despite their
|
||||
slightly contrived legal structure as a non-profit that owns a for-profit, Mozilla are
|
||||
an NGO just like any other. In this article I want to apply the traditional measures
|
||||
that are applied to other NGOs to Mozilla in order to show what's wrong. These three
|
||||
measures are: overheads, ethics and results.<p><div class="share_submission"
|
||||
style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=Firefox+Usage+is+Down+85%25+Despite+Mozilla's+Top+Exec+Pay+Going+Up+400%25%3A+https%3A%2F%2Fbit.ly%2F33M9FB2"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1528219%2Ffirefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251650&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:date>2020-09-23T15:27:00+00:00</dc:date>
|
||||
<dc:subject>firefox</dc:subject>
|
||||
<slash:department>closer-look</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>31</slash:comments>
|
||||
<slash:hit_parade>31,29,24,21,3,1,0</slash:hit_parade>
|
||||
</item>
|
||||
<item
|
||||
rdf:about="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Climate Disruption Is Now Locked In. The Next Moves Will Be Crucial.</title>
|
||||
<link>
|
||||
https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&utm_medium=feed
|
||||
</link>
|
||||
<description>America is now under siege by climate change in ways that scientists have
|
||||
warned about for years. But there is a second part to their admonition: Decades of
|
||||
growing crisis are already locked into the global ecosystem and cannot be reversed. From
|
||||
a report: This means the kinds of cascading disasters occurring today -- drought in the
|
||||
West fueling historic wildfires that send smoke all the way to the East Coast, or
|
||||
parades of tropical storms lining up across the Atlantic to march destructively toward
|
||||
North America -- are no longer features of some dystopian future. They are the here and
|
||||
now, worsening for the next generation and perhaps longer, depending on humanity's
|
||||
willingness to take action. "I've been labeled an alarmist," said Peter Kalmus, a
|
||||
climate scientist in Los Angeles, where he and millions of others have inhaled
|
||||
dangerously high levels of smoke for weeks. "And I think it's a lot harder for people to
|
||||
say that I'm being alarmist now." Last month, before the skies over San Francisco turned
|
||||
a surreal orange, Death Valley reached 130 degrees Fahrenheit, the highest temperature
|
||||
ever measured on the planet. Dozens of people have perished from the heat in Phoenix,
|
||||
which in July suffered its hottest month on record, only to surpass that milestone in
|
||||
August.
|
||||
|
||||
Conversations about climate change have broken into everyday life, to the top of the
|
||||
headlines and to center stage in the presidential campaign. The questions are profound
|
||||
and urgent. Can this be reversed? What can be done to minimize the looming dangers for
|
||||
the decades ahead? Will the destruction of recent weeks become a moment of reckoning, or
|
||||
just a blip in the news cycle? The Times spoke with two dozen climate experts, including
|
||||
scientists, economists, sociologists and policymakers, and their answers were by turns
|
||||
alarming, cynical and hopeful. "It's as if we've been smoking a pack of cigarettes a day
|
||||
for decades" and the world is now feeling the effects, said Katharine Hayhoe, a climate
|
||||
scientist at Texas Tech University. But, she said, "we're not dead yet." Their most
|
||||
sobering message was that the world still hasn't seen the worst of it. Gone is the
|
||||
climate of yesteryear, and there's no going back. The effects of climate change evident
|
||||
today are the results of choices that countries made decades ago to keep pumping
|
||||
heat-trapping greenhouse gases into the atmosphere at ever-increasing rates despite
|
||||
warnings from scientists about the price to be paid.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=Climate+Disruption+Is+Now+Locked+In.+The+Next+Moves+Will+Be+Crucial.%3A+https%3A%2F%2Fbit.ly%2F32TsNxO"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1451213%2Fclimate-disruption-is-now-locked-in-the-next-moves-will-be-crucial%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251462&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:date>2020-09-23T14:51:00+00:00</dc:date>
|
||||
<dc:subject>earth</dc:subject>
|
||||
<slash:department>closer-look</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>67</slash:comments>
|
||||
<slash:hit_parade>67,61,50,33,12,2,1</slash:hit_parade>
|
||||
</item>
|
||||
<item
|
||||
rdf:about="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>A New York Clock That Told Time Now Tells the Time Remaining</title>
|
||||
<link>
|
||||
https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed
|
||||
</link>
|
||||
<description>For more than 20 years, Metronome, which includes a 62-foot-wide 15-digit
|
||||
electronic clock that faces Union Square in Manhattan, has been one of the city's most
|
||||
prominent and baffling public art projects. Its digital display once told the time in
|
||||
its own unique way, counting the hours, minutes and seconds (and fractions thereof) to
|
||||
and from midnight. But for years observers who did not understand how it worked
|
||||
suggested that it was measuring the acres of rainforest destroyed each year, tracking
|
||||
the world population or even that it had something to do with pi. On Saturday Metronome
|
||||
adopted a new ecologically sensitive mission. From a report: Now, instead of measuring
|
||||
24-hour cycles, it is measuring what two artists, Gan Golan and Andrew Boyd, present as
|
||||
a critical window for action to prevent the effects of global warming from becoming
|
||||
irreversible. On Saturday at 3:20 p.m., messages including "The Earth has a deadline"
|
||||
began to appear on the display. Then numbers -- 7:103:15:40:07 -- showed up,
|
||||
representing the years, days, hours, minutes and seconds until that deadline. As a
|
||||
handful of supporters watched, the number -- which the artists said was based on
|
||||
calculations by the Mercator Research Institute on Global Commons and Climate Change in
|
||||
Berlin -- began ticking down, second by second.
|
||||
|
||||
"This is our way to shout that number from the rooftops." Mr. Golan said just before the
|
||||
countdown began. "The world is literally counting on us." The Climate Clock, as the two
|
||||
artists call their project, will be displayed on the 14th Street building, One Union
|
||||
Square South, through Sept. 27, the end of Climate Week. The creators say their aim is
|
||||
to arrange for the clock to be permanently displayed, there or elsewhere. Mr. Golan said
|
||||
he came up with the idea to publicly illustrate the urgency of combating climate change
|
||||
about two years ago, shortly after his daughter was born. He asked Mr. Boyd, an activist
|
||||
from the Lower East Side, to work with him on the project.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=A+New+York+Clock+That+Told+Time+Now+Tells+the+Time+Remaining%3A+https%3A%2F%2Fbit.ly%2F2HrAt2b"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1420240%2Fa-new-york-clock-that-told-time-now-tells-the-time-remaining%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251272&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:date>2020-09-23T14:10:00+00:00</dc:date>
|
||||
<dc:subject>news</dc:subject>
|
||||
<slash:department>how-about-that</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>43</slash:comments>
|
||||
<slash:hit_parade>43,38,33,27,6,1,0</slash:hit_parade>
|
||||
</item>
|
||||
</rdf:RDF>
|
@ -0,0 +1,31 @@
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
||||
|
||||
<channel>
|
||||
<title>Slashdot</title>
|
||||
<description>News for nerds, stuff that matters</description>
|
||||
<dc:language>en-us</dc:language>
|
||||
<dc:rights>Copyright 1997-2016, SlashdotMedia. All Rights Reserved.</dc:rights>
|
||||
<dc:date>2020-09-23T16:20:20+00:00</dc:date>
|
||||
<dc:publisher>Dice</dc:publisher>
|
||||
<dc:creator>help@slashdot.org</dc:creator>
|
||||
<dc:subject>Technology</dc:subject>
|
||||
<syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
|
||||
<syn:updateFrequency>1</syn:updateFrequency>
|
||||
<syn:updatePeriod>hourly</syn:updatePeriod>
|
||||
<image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
|
||||
<textinput rdf:resource="https://slashdot.org/search.pl" />
|
||||
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://rss.slashdot.org/slashdot/slashdotMain"
|
||||
rel="self" type="application/rdf+xml" />
|
||||
<feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"
|
||||
uri="slashdot/slashdotmain" />
|
||||
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://pubsubhubbub.appspot.com/"
|
||||
rel="hub" />
|
||||
</channel>
|
||||
<image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
|
||||
<title>Slashdot</title>
|
||||
<url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
|
||||
<link>https://slashdot.org/</link>
|
||||
</image>
|
||||
</rdf:RDF>
|
@ -0,0 +1,43 @@
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns="http://purl.org/rss/1.0/">
|
||||
<item
|
||||
rdf:about="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>Google Expands its Flutter Development Kit To Windows Apps</title>
|
||||
<link>
|
||||
https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed
|
||||
</link>
|
||||
<description>Google has announced that Flutter, its open source UI development kit for
|
||||
building cross-platform software from the same codebase, is finally available for
|
||||
Windows apps in alpha. From a report:For the world's leading desktop operating system
|
||||
with some 1 billion installations of Windows 10 alone, this has been a long time coming.
|
||||
Flutter's alpha incarnation was initially launched at Google's I/O developer conference
|
||||
back in 2017, before arriving in beta less than a year later. In its original guise,
|
||||
Flutter was designed for Android and iOS app development, but it has since expanded to
|
||||
cover the web, MacOS, and Linux, which are currently available in various alpha or beta
|
||||
iterations. Developers have had to consider unique platform-specific factors when
|
||||
designing for the desktop or mobile phones, such as different screen sizes and how
|
||||
people interact with their devices. On smartphones, people typically use touch and
|
||||
swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
|
||||
This means Flutter has had to expand its support to cover the additional inputs.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:subject>programming</dc:subject>
|
||||
<slash:department>how-about-that</slash:department>
|
||||
<slash:section>developers</slash:section>
|
||||
<slash:comments>1</slash:comments>
|
||||
<slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
|
||||
</item>
|
||||
</rdf:RDF>
|
@ -0,0 +1,40 @@
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns="http://purl.org/rss/1.0/">
|
||||
<item>
|
||||
<title>Google Expands its Flutter Development Kit To Windows Apps</title>
|
||||
<description>Google has announced that Flutter, its open source UI development kit for
|
||||
building cross-platform software from the same codebase, is finally available for
|
||||
Windows apps in alpha. From a report:For the world's leading desktop operating system
|
||||
with some 1 billion installations of Windows 10 alone, this has been a long time coming.
|
||||
Flutter's alpha incarnation was initially launched at Google's I/O developer conference
|
||||
back in 2017, before arriving in beta less than a year later. In its original guise,
|
||||
Flutter was designed for Android and iOS app development, but it has since expanded to
|
||||
cover the web, MacOS, and Linux, which are currently available in various alpha or beta
|
||||
iterations. Developers have had to consider unique platform-specific factors when
|
||||
designing for the desktop or mobile phones, such as different screen sizes and how
|
||||
people interact with their devices. On smartphones, people typically use touch and
|
||||
swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
|
||||
This means Flutter has had to expand its support to cover the additional inputs.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:date>2020-09-23T16:15:00+00:00</dc:date>
|
||||
<dc:subject>programming</dc:subject>
|
||||
<slash:department>how-about-that</slash:department>
|
||||
<slash:section>developers</slash:section>
|
||||
<slash:comments>1</slash:comments>
|
||||
<slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
|
||||
</item>
|
||||
</rdf:RDF>
|
@ -0,0 +1,43 @@
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns="http://purl.org/rss/1.0/">
|
||||
<item
|
||||
rdf:about="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<link>
|
||||
https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed
|
||||
</link>
|
||||
<description>Google has announced that Flutter, its open source UI development kit for
|
||||
building cross-platform software from the same codebase, is finally available for
|
||||
Windows apps in alpha. From a report:For the world's leading desktop operating system
|
||||
with some 1 billion installations of Windows 10 alone, this has been a long time coming.
|
||||
Flutter's alpha incarnation was initially launched at Google's I/O developer conference
|
||||
back in 2017, before arriving in beta less than a year later. In its original guise,
|
||||
Flutter was designed for Android and iOS app development, but it has since expanded to
|
||||
cover the web, MacOS, and Linux, which are currently available in various alpha or beta
|
||||
iterations. Developers have had to consider unique platform-specific factors when
|
||||
designing for the desktop or mobile phones, such as different screen sizes and how
|
||||
people interact with their devices. On smartphones, people typically use touch and
|
||||
swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
|
||||
This means Flutter has had to expand its support to cover the additional inputs.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251868&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:date>2020-09-23T16:15:00+00:00</dc:date>
|
||||
<dc:subject>programming</dc:subject>
|
||||
<slash:department>how-about-that</slash:department>
|
||||
<slash:section>developers</slash:section>
|
||||
<slash:comments>1</slash:comments>
|
||||
<slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
|
||||
</item>
|
||||
</rdf:RDF>
|
@ -0,0 +1,120 @@
|
||||
<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns="http://purl.org/rss/1.0/">
|
||||
|
||||
<channel rdf:about="https://slashdot.org/">
|
||||
<title>Slashdot</title>
|
||||
<link>https://slashdot.org/</link>
|
||||
<description>News for nerds, stuff that matters</description>
|
||||
<dc:language>en-us</dc:language>
|
||||
<dc:rights>Copyright 1997-2016, SlashdotMedia. All Rights Reserved.</dc:rights>
|
||||
<dc:date>2020-09-23T16:20:20+00:00</dc:date>
|
||||
<dc:publisher>Dice</dc:publisher>
|
||||
<dc:creator>help@slashdot.org</dc:creator>
|
||||
<dc:subject>Technology</dc:subject>
|
||||
<syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
|
||||
<syn:updateFrequency>1</syn:updateFrequency>
|
||||
<syn:updatePeriod>hourly</syn:updatePeriod>
|
||||
<items>
|
||||
<rdf:Seq>
|
||||
<rdf:li
|
||||
rdf:resource="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/0057256/jeff-bezos-is-opening-his-first-tuition-free-bezos-academy-preschool-where-each-child-will-be-the-customer?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://news.slashdot.org/story/20/09/23/0050249/google-is-pulling-the-plug-on-paid-chrome-extensions-over-the-next-year?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://entertainment.slashdot.org/story/20/09/22/2147243/old-tv-caused-village-broadband-outages-for-18-months?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://mobile.slashdot.org/story/20/09/22/2316256/t-mobile-amassed-unprecedented-concentration-of-spectrum-att-complains?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://yro.slashdot.org/story/20/09/22/2030223/dark-web-drugs-raid-leads-to-179-arrests?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://hardware.slashdot.org/story/20/09/22/2055203/the-fairphone-3-is-a-repairable-dream-that-takes-beautiful-photos?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://tech.slashdot.org/story/20/09/23/0039222/tesla-unveils-model-s-plaid-520-miles-200-mph-and-0-60-mph-in-less-than-2s?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://apple.slashdot.org/story/20/09/22/211222/apple-ceo-impressed-by-remote-work-sees-permanent-changes?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://hardware.slashdot.org/story/20/09/22/236228/tesla-announces-tabless-battery-cells-that-will-improve-range-of-its-electric-cars?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://linux.slashdot.org/story/20/09/22/2243209/linux-journal-is-back?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
<rdf:li
|
||||
rdf:resource="https://hardware.slashdot.org/story/20/09/22/2026238/shell-reportedly-to-slash-oil-and-gas-production-costs-to-focus-more-on-renewables?utm_source=rss1.0mainlinkanon&utm_medium=feed" />
|
||||
</rdf:Seq>
|
||||
</items>
|
||||
<image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
|
||||
<textinput rdf:resource="https://slashdot.org/search.pl" />
|
||||
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://rss.slashdot.org/slashdot/slashdotMain"
|
||||
rel="self" type="application/rdf+xml" />
|
||||
<feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"
|
||||
uri="slashdot/slashdotmain" />
|
||||
<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://pubsubhubbub.appspot.com/"
|
||||
rel="hub" />
|
||||
</channel>
|
||||
<image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
|
||||
<title>Slashdot</title>
|
||||
<url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
|
||||
<link>https://slashdot.org/</link>
|
||||
</image>
|
||||
<item
|
||||
rdf:about="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed">
|
||||
<title>A New York Clock That Told Time Now Tells the Time Remaining</title>
|
||||
<description>For more than 20 years, Metronome, which includes a 62-foot-wide 15-digit
|
||||
electronic clock that faces Union Square in Manhattan, has been one of the city's most
|
||||
prominent and baffling public art projects. Its digital display once told the time in
|
||||
its own unique way, counting the hours, minutes and seconds (and fractions thereof) to
|
||||
and from midnight. But for years observers who did not understand how it worked
|
||||
suggested that it was measuring the acres of rainforest destroyed each year, tracking
|
||||
the world population or even that it had something to do with pi. On Saturday Metronome
|
||||
adopted a new ecologically sensitive mission. From a report: Now, instead of measuring
|
||||
24-hour cycles, it is measuring what two artists, Gan Golan and Andrew Boyd, present as
|
||||
a critical window for action to prevent the effects of global warming from becoming
|
||||
irreversible. On Saturday at 3:20 p.m., messages including "The Earth has a deadline"
|
||||
began to appear on the display. Then numbers -- 7:103:15:40:07 -- showed up,
|
||||
representing the years, days, hours, minutes and seconds until that deadline. As a
|
||||
handful of supporters watched, the number -- which the artists said was based on
|
||||
calculations by the Mercator Research Institute on Global Commons and Climate Change in
|
||||
Berlin -- began ticking down, second by second.
|
||||
|
||||
"This is our way to shout that number from the rooftops." Mr. Golan said just before the
|
||||
countdown began. "The world is literally counting on us." The Climate Clock, as the two
|
||||
artists call their project, will be displayed on the 14th Street building, One Union
|
||||
Square South, through Sept. 27, the end of Climate Week. The creators say their aim is
|
||||
to arrange for the clock to be permanently displayed, there or elsewhere. Mr. Golan said
|
||||
he came up with the idea to publicly illustrate the urgency of combating climate change
|
||||
about two years ago, shortly after his daughter was born. He asked Mr. Boyd, an activist
|
||||
from the Lower East Side, to work with him on the project.<p><div
|
||||
class="share_submission" style="position:relative;"> <a class="slashpop"
|
||||
href="http://twitter.com/home?status=A+New+York+Clock+That+Told+Time+Now+Tells+the+Time+Remaining%3A+https%3A%2F%2Fbit.ly%2F2HrAt2b"><img
|
||||
src="https://a.fsdn.com/sd/twitter_icon_large.png"></a> <a class="slashpop"
|
||||
href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1420240%2Fa-new-york-clock-that-told-time-now-tells-the-time-remaining%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"><img
|
||||
src="https://a.fsdn.com/sd/facebook_icon_large.png"></a>
|
||||
|
||||
|
||||
</div></p><p><a
|
||||
href="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0moreanon&amp;utm_medium=feed">Read
|
||||
more of this story</a> at Slashdot.</p><iframe
|
||||
src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;id=17251272&amp;smallembed=1"
|
||||
style="height: 300px; width: 100%; border: none;"></iframe>
|
||||
</description>
|
||||
<dc:creator>msmash</dc:creator>
|
||||
<dc:creator></dc:creator>
|
||||
<dc:creator>creator 2</dc:creator>
|
||||
<dc:creator>creator 3</dc:creator>
|
||||
<dc:creator>creator 4</dc:creator>
|
||||
<dc:creator>creator 5</dc:creator>
|
||||
<dc:date>2020-09-23T14:10:00+00:00</dc:date>
|
||||
<dc:subject>news</dc:subject>
|
||||
<slash:department>how-about-that</slash:department>
|
||||
<slash:section>news</slash:section>
|
||||
<slash:comments>43</slash:comments>
|
||||
<slash:hit_parade>43,38,33,27,6,1,0</slash:hit_parade>
|
||||
</item>
|
||||
</rdf:RDF>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||
<channel>
|
||||
<title></title>
|
||||
<atom:link href="https://news.ycombinator.com/feed/" rel="self"/>
|
||||
<link>https://news.ycombinator.com/</link>
|
||||
<description>Links for the intellectually curious, ranked by readers.</description>
|
||||
</channel>
|
||||
</rss>
|
10
api/src/androidTest/assets/localfeed/rss2/rss_full_feed.xml
Normal file
10
api/src/androidTest/assets/localfeed/rss2/rss_full_feed.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||
<channel>
|
||||
<title>Hacker News</title>
|
||||
<atom:link href="https://news.ycombinator.com/feed/" rel="self" />
|
||||
<atom:link href="https://news.ycombinator.com/hub" rel="hub" />
|
||||
<link>https://news.ycombinator.com/</link>
|
||||
<description>Links for the intellectually curious, ranked by readers.</description>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
|
||||
version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
<enclosure length="0" type="image/jpg" url="https://image1.jpg" />
|
||||
<media:content medium="image" url="https://image2.jpg"/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
|
||||
version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
<media:content medium="image" url="https://image1.jpg"><media:title>image1 title</media:title></media:content>
|
||||
</item>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
<media:content type="image/jpeg" url="https://image2.jpg"><media:title>image2 title</media:title></media:content>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
|
||||
version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
<media:group>
|
||||
<media:content medium="image" url="https://image1.jpg" />
|
||||
<media:content medium="image" url="https://image2.jpg">
|
||||
<media:title>image2 title</media:title>
|
||||
</media:content>
|
||||
</media:group>
|
||||
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<link>link</link>
|
||||
<dc:creator><![CDATA[creator]]></dc:creator>
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<title>title</title>
|
||||
<link>link</link>
|
||||
<guid>guid</guid>
|
||||
|
||||
<dc:creator><![CDATA[creator 1]]></dc:creator>
|
||||
<dc:creator><![CDATA[creator 2]]></dc:creator>
|
||||
<dc:creator><![CDATA[creator 3]]></dc:creator>
|
||||
<dc:creator><![CDATA[creator 4]]></dc:creator>
|
||||
|
||||
<dc:date>2020-08-05T14:03:48Z</dc:date>
|
||||
<category><![CDATA[Category 1]]></category>
|
||||
<category><![CDATA[Category 2]]></category>
|
||||
<category><![CDATA[Category 3]]></category>
|
||||
<category><![CDATA[Category 4]]></category>
|
||||
<category><![CDATA[Category 5]]></category>
|
||||
<category><![CDATA[Category 6]]></category>
|
||||
<guid isPermaLink="false">guid</guid>
|
||||
|
||||
<description><![CDATA[description]]></description>
|
||||
<content:encoded><![CDATA[content:encoded]]></content:encoded>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
59
api/src/androidTest/assets/localfeed/rss_feed.xml
Normal file
59
api/src/androidTest/assets/localfeed/rss_feed.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||
<channel>
|
||||
<title>Hacker News</title>
|
||||
<link>https://news.ycombinator.com/</link>
|
||||
<description>Links for the intellectually curious, ranked by readers.</description>
|
||||
<item>
|
||||
<title>Africa declared free of wild polio</title>
|
||||
<link>https://www.bbc.com/news/world-africa-53887947</link>
|
||||
<pubDate>Tue, 25 Aug 2020 17:15:49 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24273602</comments>
|
||||
<author>Author 1</author>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273602">Comments</a>]]></description>
|
||||
<media:description>media description</media:description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Palantir S-1</title>
|
||||
<link>https://www.sec.gov/Archives/edgar/data/1321655/000119312520230013/d904406ds1.htm</link>
|
||||
<pubDate>Tue, 25 Aug 2020 21:03:42 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24276086</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24276086">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Openwifi: Linux mac80211 compatible full-stack 802.11/Wi-Fi design based on SDR</title>
|
||||
<link>https://github.com/open-sdr/openwifi</link>
|
||||
<pubDate>Tue, 25 Aug 2020 17:45:19 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24273919</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273919">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Syllabus for Eric's PhD Students</title>
|
||||
<link>https://docs.google.com/document/d/11D3kHElzS2HQxTwPqcaTnU5HCJ8WGE5brTXI4KLf4dM/edit</link>
|
||||
<pubDate>Tue, 25 Aug 2020 18:55:12 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24274699</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24274699">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>WebBundles harmful to content blocking, security tools, and the open web</title>
|
||||
<link>https://brave.com/webbundles-harmful-to-content-blocking-security-tools-and-the-open-web/</link>
|
||||
<pubDate>Tue, 25 Aug 2020 19:18:50 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24274968</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24274968">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Zappos CEO Tony Hsieh is stepping down after 21 years</title>
|
||||
<link>https://footwearnews.com/2020/business/executive-moves/zappos-ceo-tony-hsieh-steps-down-1203045974/</link>
|
||||
<pubDate>Tue, 25 Aug 2020 06:11:42 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24268522</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24268522">Comments</a>]]></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Evgeny Kuznetsov practices with Bauer stick that has hole in the blade</title>
|
||||
<link>https://russianmachineneverbreaks.com/2020/07/17/evgeny-kuznetsov-practices-with-bauer-stick-that-has-hole-in-the-blade/</link>
|
||||
<pubDate>Tue, 25 Aug 2020 19:38:09 +0000</pubDate>
|
||||
<comments>https://news.ycombinator.com/item?id=24275159</comments>
|
||||
<description><![CDATA[<a href="https://news.ycombinator.com/item?id=24275159">Comments</a>]]></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
@ -29,7 +29,7 @@ class OPMLParserTest {
|
||||
|
||||
@Test
|
||||
fun readOpmlTest() {
|
||||
val stream = context.resources.assets.open("subscriptions.opml")
|
||||
val stream = context.resources.assets.open("opml/subscriptions.opml")
|
||||
|
||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
||||
|
||||
@ -52,7 +52,7 @@ class OPMLParserTest {
|
||||
|
||||
@Test
|
||||
fun readLiteSubscriptionsTest() {
|
||||
val stream = context.resources.assets.open("lite_subscriptions.opml")
|
||||
val stream = context.resources.assets.open("opml/lite_subscriptions.opml")
|
||||
|
||||
var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
|
||||
|
||||
@ -68,7 +68,7 @@ class OPMLParserTest {
|
||||
|
||||
@Test
|
||||
fun opmlVersionTest() {
|
||||
val stream = context.resources.assets.open("wrong_version.opml")
|
||||
val stream = context.resources.assets.open("opml/wrong_version.opml")
|
||||
|
||||
OPMLParser.read(stream)
|
||||
.test()
|
||||
|
@ -0,0 +1,190 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import android.accounts.NetworkErrorException
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.HttpManager
|
||||
import com.readrops.api.utils.LibUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.UnknownFormatException
|
||||
import junit.framework.TestCase.*
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.Buffer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LocalRSSDataSourceTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
private lateinit var url: HttpUrl
|
||||
|
||||
private val mockServer: MockWebServer = MockWebServer()
|
||||
private val localRSSDataSource = LocalRSSDataSource(HttpManager.getInstance().okHttpClient)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
mockServer.start(8080)
|
||||
url = mockServer.url("/rss")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockServer.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun successfulQueryTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss_feed.xml")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8")
|
||||
.addHeader(LibUtils.ETAG_HEADER, "ETag-value")
|
||||
.addHeader(LibUtils.LAST_MODIFIED_HEADER, "Last-Modified")
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null)
|
||||
val feed = pair?.first!!
|
||||
|
||||
assertEquals(feed.name, "Hacker News")
|
||||
assertEquals(feed.url, "http://localhost:8080/rss")
|
||||
assertEquals(feed.siteUrl, "https://news.ycombinator.com/")
|
||||
assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.")
|
||||
|
||||
assertEquals(feed.etag, "ETag-value")
|
||||
assertEquals(feed.lastModified, "Last-Modified")
|
||||
|
||||
assertEquals(pair.second.size, 7)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun headersTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss_feed.xml")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/rss+xml; charset=UTF-8")
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val headers = Headers.headersOf(LibUtils.ETAG_HEADER, "ETag", LibUtils.LAST_MODIFIED_HEADER, "Last-Modified")
|
||||
localRSSDataSource.queryRSSResource(url.toString(), headers)
|
||||
|
||||
val request = mockServer.takeRequest()
|
||||
|
||||
assertEquals(request.headers[LibUtils.ETAG_HEADER], "ETag")
|
||||
assertEquals(request.headers[LibUtils.LAST_MODIFIED_HEADER], "Last-Modified")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun jsonFeedTest() {
|
||||
val stream = context.resources.assets.open("localfeed/json/json_feed.json")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/feed+json")
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!!
|
||||
|
||||
assertEquals(pair.first.name, "News from Flying Meat")
|
||||
assertEquals(pair.second.size, 10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun specialCasesAtomTest() {
|
||||
val stream = context.resources.assets.open("localfeed/atom/atom_feed_no_url_siteurl.xml")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/atom+xml")
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!!
|
||||
|
||||
assertEquals(pair.first.url, "http://localhost:8080/rss")
|
||||
assertEquals(pair.first.siteUrl, "http://localhost")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun specialCasesRSS1Test() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_feed_no_url_siteurl.xml")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(LibUtils.CONTENT_TYPE_HEADER, "application/rdf+xml")
|
||||
.setBody(Buffer().readFrom(stream)))
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!!
|
||||
|
||||
assertEquals(pair.first.url, "http://localhost:8080/rss")
|
||||
assertEquals(pair.first.siteUrl, "http://localhost")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun response304Test() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED))
|
||||
|
||||
val pair = localRSSDataSource.queryRSSResource(url.toString(), null)
|
||||
|
||||
assertNull(pair)
|
||||
}
|
||||
|
||||
@Test(expected = NetworkErrorException::class)
|
||||
fun response404Test() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null)
|
||||
}
|
||||
|
||||
@Test(expected = UnknownFormatException::class)
|
||||
fun noContentTypeTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null)
|
||||
}
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun badContentTypeTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", ""))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null)
|
||||
}
|
||||
|
||||
@Test(expected = UnknownFormatException::class)
|
||||
fun badContentTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/xml")
|
||||
.setBody("<html> </html>"))
|
||||
|
||||
localRSSDataSource.queryRSSResource(url.toString(), null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isUrlResourceSuccessfulTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/atom+xml; charset=UTF-8"))
|
||||
|
||||
assertTrue(localRSSDataSource.isUrlRSSResource(url.toString()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isUrlRSSResourceFailureTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
|
||||
|
||||
assertFalse(localRSSDataSource.isUrlRSSResource(url.toString()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isUrlRSSResourceBadContentTypeTest() {
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/xml; charset=UTF-8")
|
||||
.setBody("<html> </html>"))
|
||||
|
||||
assertFalse(localRSSDataSource.isUrlRSSResource(url.toString()))
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ATOMFeedAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = ATOMFeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.assets.open("localfeed/atom/atom_feed.xml")
|
||||
|
||||
val feed = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(feed.name, "Recent Commits to Readrops:develop")
|
||||
assertEquals(feed.url, "https://github.com/readrops/Readrops/commits/develop.atom")
|
||||
assertEquals(feed.siteUrl, "https://github.com/readrops/Readrops/commits/develop")
|
||||
assertEquals(feed.description, "Here is a subtitle")
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ATOMItemsAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = ATOMItemsAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/atom/atom_items.xml")
|
||||
|
||||
val items = adapter.fromXml(stream)
|
||||
val item = items[0]
|
||||
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(item.title, "Add an option to open item url in custom tab")
|
||||
assertEquals(item.link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-09-06T21:09:59Z"))
|
||||
assertEquals(item.author, "Shinokuni")
|
||||
assertEquals(item.description, "Summary")
|
||||
assertEquals(item.guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertNotNull(item.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noDateTest() {
|
||||
val stream = context.resources.assets.open("localfeed/atom/atom_items_no_date.xml")
|
||||
|
||||
val item = adapter.fromXml(stream).first()
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noTitleTest() {
|
||||
val stream = context.resources.assets.open("localfeed/atom/atom_items_no_title.xml")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noLinkTest() {
|
||||
val stream = context.resources.assets.open("localfeed/atom/atom_items_no_link.xml")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
|
||||
assertTrue(exception.message!!.contains("Item link is required"))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.squareup.moshi.Moshi
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class JSONFeedAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(JSONFeedAdapter())
|
||||
.build()
|
||||
.adapter<Feed>(Feed::class.java)
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.assets.open("localfeed/json/json_feed.json")
|
||||
|
||||
val feed = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
|
||||
assertEquals(feed.name, "News from Flying Meat")
|
||||
assertEquals(feed.url, "http://flyingmeat.com/blog/feed.json")
|
||||
assertEquals(feed.siteUrl, "http://flyingmeat.com/blog/")
|
||||
assertEquals(feed.description, "News from your friends at Flying Meat.")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import junit.framework.TestCase.*
|
||||
import okio.Buffer
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class JSONItemsAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), JSONItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(List::class.java, Item::class.java))
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/json/json_feed.json")
|
||||
|
||||
val items = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(items.size, 10)
|
||||
assertEquals(item.guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(item.title, "Acorn and 10.13")
|
||||
assertEquals(item.link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2017-09-25T14:27:27-07:00"))
|
||||
assertEquals(item.author, "Author 1")
|
||||
assertNotNull(item.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/json/json_items_other_cases.json")
|
||||
|
||||
val item = adapter.fromJson(Buffer().readFrom(stream))!!.first()
|
||||
|
||||
assertEquals(item.description, "This is a summary")
|
||||
assertEquals(item.content, "content_html")
|
||||
assertEquals(item.imageLink, "https://image.com")
|
||||
assertEquals(item.author, "Author 1, Author 3, Author 4, Author 5, ...")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullDateTest() {
|
||||
val stream = context.resources.assets.open("localfeed/json/json_items_no_date.json")
|
||||
|
||||
val item = adapter.fromJson(Buffer().readFrom(stream))!!.first()
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = context.resources.assets.open("localfeed/json/json_items_no_title.json")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream)) }
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullLinkTest() {
|
||||
val stream = context.resources.assets.open("localfeed/json/json_items_no_link.json")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromJson(Buffer().readFrom(stream)) }
|
||||
assertTrue(exception.message!!.contains("Item link is required"))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RSS1FeedAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = RSS1FeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCaseTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_feed.xml")
|
||||
|
||||
val feed = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(feed.name, "Slashdot")
|
||||
assertEquals(feed.url, "https://slashdot.org/")
|
||||
assertEquals(feed.siteUrl, "https://slashdot.org/")
|
||||
assertEquals(feed.description, "News for nerds, stuff that matters")
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RSS1ItemsAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = RSS1ItemsAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_feed.xml")
|
||||
|
||||
val items = adapter.fromXml(stream)
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(item.title, "Google Expands its Flutter Development Kit To Windows Apps")
|
||||
assertEquals(item.link.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(item.guid.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-09-23T16:15:00+00:00"))
|
||||
assertEquals(item.author, "msmash")
|
||||
assertNotNull(item.description)
|
||||
assertEquals(item.content, "content:encoded")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun specialCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_items_special_cases.xml")
|
||||
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.author, "msmash, creator 2, creator 3, creator 4, ...")
|
||||
assertEquals(item.link, "https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-" +
|
||||
"that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullDateTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_date.xml")
|
||||
|
||||
val item = adapter.fromXml(stream).first()
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_title.xml")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullLinkTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss1/rss1_items_no_link.xml")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
|
||||
assertTrue(exception.message!!.contains("RSS1 link or about element is required"))
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.ParseException
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RSS2FeedAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = RSS2FeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_full_feed.xml")
|
||||
|
||||
val feed = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(feed.name, "Hacker News")
|
||||
assertEquals(feed.url, "https://news.ycombinator.com/feed/")
|
||||
assertEquals(feed.siteUrl, "https://news.ycombinator.com/")
|
||||
assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.")
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun nullTitleTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_feed_special_cases.xml")
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RSS2ItemsAdapterTest {
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().context
|
||||
|
||||
private val adapter = RSS2ItemsAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss_feed.xml")
|
||||
|
||||
val items = adapter.fromXml(stream)
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(items.size, 7)
|
||||
assertEquals(item.title, "Africa declared free of wild polio")
|
||||
assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947")
|
||||
assertEquals(item.pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000"))
|
||||
assertEquals(item.author, "Author 1")
|
||||
assertEquals(item.description, "<a href=\"https://news.ycombinator.com/item?id=24273602\">Comments</a>")
|
||||
assertEquals(item.guid, "https://www.bbc.com/news/world-africa-53887947")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherNamespacesTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_other_namespaces.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.guid, "guid")
|
||||
assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-08-05T14:03:48Z"))
|
||||
assertEquals(item.content, "content:encoded")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noDateTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_date.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noTitleTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_title.xml")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noLinkTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_no_link.xml")
|
||||
|
||||
val exception = Assert.assertThrows(ParseException::class.java) { adapter.fromXml(stream) }
|
||||
assertTrue(exception.message!!.contains("Item link is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enclosureTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_enclosure.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaContentTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_content.xml")
|
||||
val items = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(items.first().imageLink, "https://image1.jpg")
|
||||
assertEquals(items[1].imageLink, "https://image2.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaGroupTest() {
|
||||
val stream = context.resources.assets.open("localfeed/rss2/rss_items_media_group.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
}
|
@ -3,7 +3,10 @@
|
||||
|
||||
<!-- for tests only -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application android:requestLegacyExternalStorage="true" />
|
||||
<application
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:usesCleartextTraffic="true" />
|
||||
|
||||
</manifest>
|
@ -1,9 +0,0 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
/*
|
||||
A simple class to give an abstract level to rss/atom/json feed classes
|
||||
*/
|
||||
abstract class AFeed {
|
||||
var etag: String? = null
|
||||
var lastModified: String? = null
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import android.accounts.NetworkErrorException
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.readrops.api.localfeed.json.JSONFeedAdapter
|
||||
import com.readrops.api.localfeed.json.JSONItemsAdapter
|
||||
import com.readrops.api.utils.LibUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.UnknownFormatException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class LocalRSSDataSource(private val httpClient: OkHttpClient) {
|
||||
|
||||
/**
|
||||
* Query RSS url
|
||||
* @param url url to query
|
||||
* @param headers request headers
|
||||
* @return a Feed object with its items
|
||||
*/
|
||||
@Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class)
|
||||
@WorkerThread
|
||||
fun queryRSSResource(url: String, headers: Headers?): Pair<Feed, List<Item>>? {
|
||||
val response = queryUrl(url, headers)
|
||||
|
||||
return when {
|
||||
response.isSuccessful -> {
|
||||
val header = response.header(LibUtils.CONTENT_TYPE_HEADER)
|
||||
?: throw UnknownFormatException("Unable to get $url content-type")
|
||||
|
||||
val contentType = LibUtils.parseContentType(header)
|
||||
?: throw ParseException("Unable to parse $url content-type")
|
||||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
val bodyArray = response.peekBody(Long.MAX_VALUE).bytes()
|
||||
|
||||
// if we can't guess type based on content-type header, we use the content
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN)
|
||||
type = LocalRSSHelper.getRSSContentType(ByteArrayInputStream(bodyArray))
|
||||
// if we can't guess type even with the content, we are unable to go further
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type")
|
||||
|
||||
val feed = parseFeed(ByteArrayInputStream(bodyArray), type, response)
|
||||
val items = parseItems(ByteArrayInputStream(bodyArray), type)
|
||||
|
||||
response.body?.close()
|
||||
Pair(feed, items)
|
||||
}
|
||||
response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null
|
||||
else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided url is a RSS resource
|
||||
* @param url url to check
|
||||
* @return true if [url] is a RSS resource, false otherwise
|
||||
*/
|
||||
@WorkerThread
|
||||
fun isUrlRSSResource(url: String): Boolean {
|
||||
val response = queryUrl(url, null)
|
||||
|
||||
return if (response.isSuccessful) {
|
||||
val header = response.header(LibUtils.CONTENT_TYPE_HEADER)
|
||||
?: return false
|
||||
|
||||
val contentType = LibUtils.parseContentType(header)
|
||||
?: return false
|
||||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN)
|
||||
type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method
|
||||
|
||||
type != LocalRSSHelper.RSSType.UNKNOWN
|
||||
} else false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun queryUrl(url: String, headers: Headers?): Response {
|
||||
val requestBuilder = Request.Builder().url(url)
|
||||
headers?.let { requestBuilder.headers(it) }
|
||||
|
||||
return httpClient.newCall(requestBuilder.build()).execute()
|
||||
}
|
||||
|
||||
private fun parseFeed(stream: InputStream, type: LocalRSSHelper.RSSType, response: Response): Feed {
|
||||
val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) {
|
||||
val adapter = XmlAdapter.xmlFeedAdapterFactory(type)
|
||||
|
||||
adapter.fromXml(stream)
|
||||
} else {
|
||||
val adapter = Moshi.Builder()
|
||||
.add(JSONFeedAdapter())
|
||||
.build()
|
||||
.adapter(Feed::class.java)
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
}
|
||||
|
||||
handleSpecialCases(feed, type, response)
|
||||
|
||||
feed.etag = response.header(LibUtils.ETAG_HEADER)
|
||||
feed.lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER)
|
||||
|
||||
return feed
|
||||
}
|
||||
|
||||
private fun parseItems(stream: InputStream, type: LocalRSSHelper.RSSType): List<Item> {
|
||||
return if (type != LocalRSSHelper.RSSType.JSONFEED) {
|
||||
val adapter = XmlAdapter.xmlItemsAdapterFactory(type)
|
||||
|
||||
adapter.fromXml(stream)
|
||||
} else {
|
||||
val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(MutableList::class.java, Item::class.java), JSONItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(MutableList::class.java, Item::class.java))
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) {
|
||||
with(feed) {
|
||||
if (type == LocalRSSHelper.RSSType.RSS_2) {
|
||||
// if an atom:link element was parsed, we still replace its value as it is unreliable,
|
||||
// otherwise we just add the rss url
|
||||
url = response.request.url.toString()
|
||||
} else if (type == LocalRSSHelper.RSSType.ATOM || type == LocalRSSHelper.RSSType.RSS_1) {
|
||||
if (url == null) url = response.request.url.toString()
|
||||
if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
object LocalRSSHelper {
|
||||
|
||||
private const val RSS_1_CONTENT_TYPE = "application/rdf+xml"
|
||||
private const val RSS_2_CONTENT_TYPE = "application/rss+xml"
|
||||
private const val ATOM_CONTENT_TYPE = "application/atom+xml"
|
||||
private const val JSONFEED_CONTENT_TYPE = "application/feed+json"
|
||||
private const val JSON_CONTENT_TYPE = "application/json"
|
||||
|
||||
private const val RSS_1_REGEX = "<rdf:RDF.*xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
|
||||
private const val RSS_2_REGEX = "rss.*version=\"2.0\""
|
||||
private const val ATOM_REGEX = "<feed.* xmlns=\"http://www.w3.org/2005/Atom\""
|
||||
|
||||
/**
|
||||
* Guess RSS type based on content-type header
|
||||
*/
|
||||
fun getRSSType(contentType: String): RSSType {
|
||||
return when (contentType) {
|
||||
RSS_1_CONTENT_TYPE -> RSSType.RSS_1
|
||||
RSS_2_CONTENT_TYPE -> RSSType.RSS_2
|
||||
ATOM_CONTENT_TYPE -> RSSType.ATOM
|
||||
JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED
|
||||
else -> RSSType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess RSS type based on xml content
|
||||
*/
|
||||
fun getRSSContentType(content: InputStream): RSSType {
|
||||
val stringBuffer = StringBuffer()
|
||||
val reader = content.bufferedReader()
|
||||
|
||||
// we get the first 10 lines which should be sufficient to get the type,
|
||||
// otherwise iterating over the whole file could be too slow
|
||||
for (i in 0..9) stringBuffer.append(reader.readLine())
|
||||
|
||||
val string = stringBuffer.toString()
|
||||
val type = when {
|
||||
RSS_1_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_1
|
||||
RSS_2_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_2
|
||||
ATOM_REGEX.toRegex().containsMatchIn(string) -> RSSType.ATOM
|
||||
else -> RSSType.UNKNOWN
|
||||
}
|
||||
|
||||
reader.close()
|
||||
content.close()
|
||||
return type
|
||||
}
|
||||
|
||||
enum class RSSType {
|
||||
RSS_1,
|
||||
RSS_2,
|
||||
ATOM,
|
||||
JSONFEED,
|
||||
UNKNOWN
|
||||
}
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
package com.readrops.api.localfeed;
|
||||
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.readrops.api.localfeed.atom.ATOMFeed;
|
||||
import com.readrops.api.localfeed.json.JSONFeed;
|
||||
import com.readrops.api.localfeed.rss.RSSFeed;
|
||||
import com.readrops.api.localfeed.rss.RSSLink;
|
||||
import com.readrops.api.utils.HttpManager;
|
||||
import com.readrops.api.utils.LibUtils;
|
||||
import com.readrops.api.utils.UnknownFormatException;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import org.simpleframework.xml.Serializer;
|
||||
import org.simpleframework.xml.core.Persister;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class RSSQuery {
|
||||
|
||||
private static final String TAG = RSSQuery.class.getSimpleName();
|
||||
|
||||
private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)";
|
||||
|
||||
private static final String RSS_2_REGEX = "rss.*version=\"2.0\"";
|
||||
|
||||
private static final String ATOM_REGEX = "<feed.* xmlns=\"http://www.w3.org/2005/Atom\"";
|
||||
|
||||
/**
|
||||
* Request the url given in parameter.
|
||||
* This method is synchronous, it <b>has</b> to be called from another thread than the main one.
|
||||
*
|
||||
* @param url url to request
|
||||
* @throws Exception
|
||||
*/
|
||||
public RSSQueryResult queryUrl(String url, Map<String, String> headers) throws Exception {
|
||||
Response response = query(url, headers);
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
String header = response.header(LibUtils.CONTENT_TYPE_HEADER);
|
||||
RSSType type = getRSSType(header);
|
||||
|
||||
if (type == null)
|
||||
return new RSSQueryResult(new UnknownFormatException("bad content type : " + header + "for " + url));
|
||||
|
||||
return parseFeed(response.body().byteStream(), type, response);
|
||||
} else if (response.code() == 304)
|
||||
return null;
|
||||
else
|
||||
return new RSSQueryResult(new NetworkErrorException("Error " + response.code() + " when requesting url " + url));
|
||||
}
|
||||
|
||||
public boolean isUrlFeedLink(String url) throws IOException {
|
||||
Response response = query(url, new HashMap<>());
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
String header = response.header(LibUtils.CONTENT_TYPE_HEADER);
|
||||
RSSType type = getRSSType(header);
|
||||
|
||||
if (type == RSSType.RSS_UNKNOWN) {
|
||||
RSSType contentType = getContentRSSType(response.body().string());
|
||||
return contentType != RSSType.RSS_UNKNOWN;
|
||||
} else return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
private Response query(String url, Map<String, String> headers) throws IOException {
|
||||
OkHttpClient okHttpClient = HttpManager.getInstance().getOkHttpClient();
|
||||
HttpManager.getInstance().setCredentials(null);
|
||||
|
||||
Request.Builder builder = new Request.Builder().url(url);
|
||||
for (String header : headers.keySet()) {
|
||||
String value = headers.get(header);
|
||||
builder.addHeader(header, value);
|
||||
}
|
||||
|
||||
Request request = builder.build();
|
||||
return okHttpClient.newCall(request).execute();
|
||||
}
|
||||
|
||||
private RSSType getRSSType(String contentType) {
|
||||
Pattern pattern = Pattern.compile(RSS_CONTENT_TYPE_REGEX);
|
||||
Matcher matcher = pattern.matcher(contentType);
|
||||
|
||||
String header;
|
||||
if (matcher.find())
|
||||
header = matcher.group(0);
|
||||
else
|
||||
header = contentType;
|
||||
|
||||
switch (header) {
|
||||
case LibUtils.RSS_DEFAULT_CONTENT_TYPE:
|
||||
return RSSType.RSS_2;
|
||||
case LibUtils.ATOM_CONTENT_TYPE:
|
||||
return RSSType.RSS_ATOM;
|
||||
case LibUtils.JSON_CONTENT_TYPE:
|
||||
return RSSType.RSS_JSON;
|
||||
case LibUtils.RSS_TEXT_CONTENT_TYPE:
|
||||
case LibUtils.HTML_CONTENT_TYPE:
|
||||
case LibUtils.RSS_APPLICATION_CONTENT_TYPE:
|
||||
default:
|
||||
Log.d(TAG, "bad content type : " + contentType);
|
||||
return RSSType.RSS_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse input feed
|
||||
*
|
||||
* @param stream source to parse
|
||||
* @param type rss type, important to know the feed format
|
||||
* @param response query response
|
||||
* @throws Exception
|
||||
*/
|
||||
private RSSQueryResult parseFeed(InputStream stream, RSSType type, Response response) throws Exception {
|
||||
String xml = LibUtils.inputStreamToString(stream);
|
||||
Serializer serializer = new Persister();
|
||||
|
||||
if (type == RSSType.RSS_UNKNOWN) {
|
||||
RSSType contentType = getContentRSSType(xml);
|
||||
if (contentType == RSSType.RSS_UNKNOWN) {
|
||||
return new RSSQueryResult(new UnknownFormatException("Unknown content format"));
|
||||
} else
|
||||
type = contentType;
|
||||
}
|
||||
|
||||
String etag = response.header(LibUtils.ETAG_HEADER);
|
||||
String lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER);
|
||||
AFeed feed = null;
|
||||
RSSQueryResult queryResult = new RSSQueryResult();
|
||||
|
||||
switch (type) {
|
||||
case RSS_2:
|
||||
feed = serializer.read(RSSFeed.class, xml);
|
||||
|
||||
// workaround if the channel does not have any atom:link tag
|
||||
if (((RSSFeed) feed).getChannel().getFeedUrl() == null) {
|
||||
((RSSFeed) feed).getChannel().getLinks().add(new RSSLink(null, response.request().url().toString()));
|
||||
}
|
||||
break;
|
||||
case RSS_ATOM:
|
||||
feed = serializer.read(ATOMFeed.class, xml);
|
||||
((ATOMFeed) feed).setWebsiteUrl(response.request().url().scheme() + "://" + response.request().url().host());
|
||||
((ATOMFeed) feed).setUrl(response.request().url().toString());
|
||||
break;
|
||||
case RSS_JSON:
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.build();
|
||||
|
||||
JsonAdapter<JSONFeed> jsonFeedAdapter = moshi.adapter(JSONFeed.class);
|
||||
feed = jsonFeedAdapter.fromJson(xml);
|
||||
break;
|
||||
}
|
||||
|
||||
queryResult.setFeed(feed);
|
||||
queryResult.setRssType(type);
|
||||
|
||||
feed.setEtag(etag);
|
||||
feed.setLastModified(lastModified);
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
|
||||
private RSSType getContentRSSType(String content) {
|
||||
RSSType type;
|
||||
|
||||
if (Pattern.compile(RSS_2_REGEX).matcher(content).find())
|
||||
type = RSSType.RSS_2;
|
||||
else if (Pattern.compile(ATOM_REGEX).matcher(content).find())
|
||||
type = RSSType.RSS_ATOM;
|
||||
else
|
||||
type = RSSType.RSS_UNKNOWN;
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
public enum RSSType {
|
||||
RSS_2,
|
||||
RSS_ATOM,
|
||||
RSS_JSON,
|
||||
RSS_UNKNOWN
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package com.readrops.api.localfeed;
|
||||
|
||||
public class RSSQueryResult {
|
||||
|
||||
private AFeed feed;
|
||||
|
||||
private RSSQuery.RSSType rssType;
|
||||
|
||||
private Exception exception;
|
||||
|
||||
public RSSQueryResult(Exception exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public RSSQueryResult() {
|
||||
|
||||
}
|
||||
|
||||
public AFeed getFeed() {
|
||||
return feed;
|
||||
}
|
||||
|
||||
public void setFeed(AFeed feed) {
|
||||
this.feed = feed;
|
||||
}
|
||||
|
||||
public RSSQuery.RSSType getRssType() {
|
||||
return rssType;
|
||||
}
|
||||
|
||||
public void setRssType(RSSQuery.RSSType rssType) {
|
||||
this.rssType = rssType;
|
||||
}
|
||||
|
||||
public void setException(Exception exception) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public Exception getException() {
|
||||
return exception;
|
||||
}
|
||||
}
|
39
api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt
Normal file
39
api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import com.readrops.api.localfeed.atom.ATOMFeedAdapter
|
||||
import com.readrops.api.localfeed.atom.ATOMItemsAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import java.io.InputStream
|
||||
|
||||
interface XmlAdapter<T> {
|
||||
|
||||
fun fromXml(inputStream: InputStream): T
|
||||
|
||||
companion object {
|
||||
fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<Feed> {
|
||||
return when (type) {
|
||||
LocalRSSHelper.RSSType.RSS_1 -> RSS1FeedAdapter()
|
||||
LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter()
|
||||
LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter()
|
||||
else -> throw IllegalArgumentException("Unknown RSS type : $type")
|
||||
}
|
||||
}
|
||||
|
||||
fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<List<Item>> {
|
||||
return when (type) {
|
||||
LocalRSSHelper.RSSType.RSS_1 -> RSS1ItemsAdapter()
|
||||
LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter()
|
||||
LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter()
|
||||
else -> throw IllegalArgumentException("Unknown RSS type : $type")
|
||||
}
|
||||
}
|
||||
|
||||
const val AUTHORS_MAX = 4
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
package com.readrops.api.localfeed.atom;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
@Root(name = "author", strict = false)
|
||||
public class ATOMAuthor {
|
||||
|
||||
@Element(required = false)
|
||||
private String name;
|
||||
|
||||
@Element(required = false)
|
||||
private String email;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package com.readrops.api.localfeed.atom;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Root(name = "entry", strict = false)
|
||||
public class ATOMEntry {
|
||||
|
||||
@Element(required = false)
|
||||
private String title;
|
||||
|
||||
@ElementList(name = "link", inline = true, required = false)
|
||||
private List<ATOMLink> links;
|
||||
|
||||
@Element(required = false)
|
||||
private String updated;
|
||||
|
||||
@Element(required = false)
|
||||
private String summary;
|
||||
|
||||
@Element(required = false)
|
||||
private String id;
|
||||
|
||||
@Element(required = false)
|
||||
private String content;
|
||||
|
||||
@Attribute(name = "type", required = false)
|
||||
private String contentType;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public List<ATOMLink> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public void setLinks(List<ATOMLink> links) {
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
public String getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
public void setUpdated(String updated) {
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
for (ATOMLink link : links) {
|
||||
if (link.getRel() == null || link.getRel().equals("self") || link.getRel().equals("alternate"))
|
||||
return link.getHref();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
package com.readrops.api.localfeed.atom;
|
||||
|
||||
import com.readrops.api.localfeed.AFeed;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Root(name = "feed", strict = false)
|
||||
public class ATOMFeed extends AFeed {
|
||||
|
||||
@Element(required = false)
|
||||
private String title;
|
||||
|
||||
@ElementList(name = "link", inline = true, required = false)
|
||||
private List<ATOMLink> links;
|
||||
|
||||
private String url;
|
||||
|
||||
private String websiteUrl;
|
||||
|
||||
@Element(required = false)
|
||||
private String id;
|
||||
|
||||
@Element(required = false)
|
||||
private String subtitle;
|
||||
|
||||
@Element(required = false)
|
||||
private String updated;
|
||||
|
||||
@Element(required = false)
|
||||
private ATOMAuthor author;
|
||||
|
||||
@ElementList(inline = true, required = false)
|
||||
private List<ATOMEntry> entries;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public List<ATOMLink> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public void setLinks(List<ATOMLink> links) {
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
public String getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.subtitle = subtitle;
|
||||
}
|
||||
|
||||
public String getUpdated() {
|
||||
return updated;
|
||||
}
|
||||
|
||||
public void setUpdated(String updated) {
|
||||
this.updated = updated;
|
||||
}
|
||||
|
||||
public ATOMAuthor getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(ATOMAuthor author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public List<ATOMEntry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setEntries(List<ATOMEntry> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getWebsiteUrl() {
|
||||
return websiteUrl;
|
||||
}
|
||||
|
||||
public void setWebsiteUrl(String websiteUrl) {
|
||||
this.websiteUrl = websiteUrl;
|
||||
}
|
||||
|
||||
/*public String getWebSiteUrl() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
if (links.size() > 0) {
|
||||
if (links.get(0).getRel() != null)
|
||||
return links.get(0).getHref();
|
||||
else {
|
||||
if (links.size() > 1)
|
||||
return links.get(1).getHref();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
} else
|
||||
return null;
|
||||
}*/
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.nonNullText
|
||||
import com.readrops.api.utils.nullableText
|
||||
import com.readrops.db.entities.Feed
|
||||
import java.io.InputStream
|
||||
|
||||
class ATOMFeedAdapter : XmlAdapter<Feed> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): Feed {
|
||||
val konsume = inputStream.konsumeXml()
|
||||
val feed = Feed()
|
||||
|
||||
return try {
|
||||
konsume.child("feed") {
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> parseLink(this@allChildrenAutoIgnore, feed)
|
||||
"subtitle" -> description = nullableText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsume.close()
|
||||
feed
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLink(konsume: Konsumer, feed: Feed) {
|
||||
val rel = konsume.attributes.getValueOpt("rel")
|
||||
|
||||
if (rel == "self")
|
||||
feed.url = konsume.attributes["href"]
|
||||
else if (rel == "alternate")
|
||||
feed.siteUrl = konsume.attributes["href"]
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "subtitle")
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
|
||||
class ATOMItemsAdapter : XmlAdapter<List<Item>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): List<Item> {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
val items = arrayListOf<Item>()
|
||||
|
||||
return try {
|
||||
konsumer.child("feed") {
|
||||
allChildrenAutoIgnore("entry") {
|
||||
val item = Item().apply {
|
||||
allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"id" -> guid = nullableText()
|
||||
"updated" -> pubDate = DateUtils.parse(nullableText())
|
||||
"link" -> parseLink(this, this@apply)
|
||||
"author" -> allChildrenAutoIgnore("name") { author = nullableText() }
|
||||
"summary" -> description = nullableTextRecursively()
|
||||
"content" -> content = nullableTextRecursively()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateItem(item)
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.guid == null) item.guid = item.link
|
||||
|
||||
items += item
|
||||
}
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLink(konsumer: Konsumer, item: Item) {
|
||||
konsumer.apply {
|
||||
if (attributes.getValueOpt("rel") == null ||
|
||||
attributes["rel"] == "alternate")
|
||||
item.link = attributes.getValueOpt("href")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "id", "updated", "link", "author", "summary", "content")
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package com.readrops.api.localfeed.atom;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
@Root(name = "link", strict = false)
|
||||
public class ATOMLink {
|
||||
|
||||
@Attribute(name = "href", required = false)
|
||||
private String href;
|
||||
|
||||
@Attribute(name = "rel", required = false)
|
||||
private String rel;
|
||||
|
||||
public String getHref() {
|
||||
return href;
|
||||
}
|
||||
|
||||
public void setHref(String href) {
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
public String getRel() {
|
||||
return rel;
|
||||
}
|
||||
|
||||
public void setRel(String rel) {
|
||||
this.rel = rel;
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JSONAuthor(val name: String,
|
||||
val url: String,
|
||||
@Json(name = "avatar") val avatarUrl: String?)
|
@ -1,15 +0,0 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.localfeed.AFeed
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JSONFeed(val version: String,
|
||||
val title: String,
|
||||
@Json(name = "home_page_url") val homePageUrl: String?,
|
||||
@Json(name = "feed_url") val feedUrl: String?,
|
||||
val description: String?,
|
||||
@Json(name = "icon") val iconUrl: String?,
|
||||
@Json(name = "favicon") val faviconUrl: String?,
|
||||
val items: List<JSONItem>) : AFeed()
|
@ -0,0 +1,45 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.nextNonEmptyString
|
||||
import com.readrops.api.utils.nextNullableString
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
|
||||
class JSONFeedAdapter {
|
||||
|
||||
@ToJson
|
||||
fun toJson(feed: Feed) = ""
|
||||
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): Feed {
|
||||
return try {
|
||||
val feed = Feed()
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
with(feed) {
|
||||
when (reader.selectName(names)) {
|
||||
0 -> name = reader.nextNonEmptyString()
|
||||
1 -> siteUrl = reader.nextNullableString()
|
||||
2 -> url = reader.nextNullableString()
|
||||
3 -> description = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
feed
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names: JsonReader.Options = JsonReader.Options.of("title", "home_page_url",
|
||||
"feed_url", "description")
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JSONItem(val id: String,
|
||||
val title: String?,
|
||||
val summary: String?,
|
||||
@Json(name = "content_text") val contentText: String?,
|
||||
@Json(name = "content_html") val contentHtml: String?,
|
||||
val url: String?,
|
||||
@Json(name = "image") val imageUrl: String?,
|
||||
@Json(name = "date_published") val pubDate: String,
|
||||
@Json(name = "date_modified") val modDate: String?,
|
||||
val author: JSONAuthor?) {
|
||||
|
||||
fun getContent(): String? {
|
||||
return contentHtml ?: contentText
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.nextNonEmptyString
|
||||
import com.readrops.api.utils.nextNullableString
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class JSONItemsAdapter : JsonAdapter<List<Item>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<Item>?) {
|
||||
// not useful
|
||||
}
|
||||
|
||||
override fun fromJson(reader: JsonReader): List<Item> {
|
||||
return try {
|
||||
val items = arrayListOf<Item>()
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"items" -> parseItems(reader, items)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseItems(reader: JsonReader, items: MutableList<Item>) {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
reader.beginObject()
|
||||
val item = Item()
|
||||
|
||||
var contentText: String? = null
|
||||
var contentHtml: String? = null
|
||||
|
||||
while (reader.hasNext()) {
|
||||
with(item) {
|
||||
when (reader.selectName(names)) {
|
||||
0 -> guid = reader.nextNonEmptyString()
|
||||
1 -> link = reader.nextNonEmptyString()
|
||||
2 -> title = reader.nextNonEmptyString()
|
||||
3 -> contentHtml = reader.nextNullableString()
|
||||
4 -> contentText = reader.nextNullableString()
|
||||
5 -> description = reader.nextNullableString()
|
||||
6 -> imageLink = reader.nextNullableString()
|
||||
7 -> pubDate = DateUtils.parse(reader.nextNullableString())
|
||||
8 -> author = parseAuthor(reader) // jsonfeed 1.0
|
||||
9 -> author = parseAuthors(reader) // jsonfeed 1.1
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateItem(item)
|
||||
item.content = if (contentHtml != null) contentHtml else contentText
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
|
||||
reader.endObject()
|
||||
items += item
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
|
||||
private fun parseAuthor(reader: JsonReader): String? {
|
||||
var author: String? = null
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"name" -> author = reader.nextNullableString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
return author
|
||||
}
|
||||
|
||||
private fun parseAuthors(reader: JsonReader): String? {
|
||||
val authors = arrayListOf<String?>()
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
authors.add(parseAuthor(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
|
||||
return if (authors.filterNotNull().isNotEmpty())
|
||||
authors.filterNotNull().joinToString(limit = AUTHORS_MAX) else null
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names: JsonReader.Options = JsonReader.Options.of("id", "url", "title",
|
||||
"content_html", "content_text", "summary", "image", "date_published", "author", "authors")
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package com.readrops.api.localfeed.rss;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Root(name = "channel", strict = false)
|
||||
public class RSSChannel {
|
||||
|
||||
@Element(name = "title", required = false)
|
||||
private String title;
|
||||
|
||||
@Element(name = "description", required = false)
|
||||
private String description;
|
||||
|
||||
// workaround to get the two links (feed and regular)
|
||||
@ElementList(name = "link", inline = true, required = false)
|
||||
private List<RSSLink> links;
|
||||
|
||||
@Element(name = "lastBuildDate", required = false)
|
||||
private String lastUpdated;
|
||||
|
||||
@ElementList(inline = true, required = false)
|
||||
private List<RSSItem> items;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<RSSLink> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public void setLinks(List<RSSLink> links) {
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
public List<RSSItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<RSSItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public String getLastUpdated() {
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
public void setLastUpdated(String lastUpdated) {
|
||||
this.lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
public String getFeedUrl() {
|
||||
if (links.size() > 1) {
|
||||
if (links.get(0).getHref() != null)
|
||||
return links.get(0).getHref();
|
||||
else
|
||||
return links.get(1).getHref();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
if (links.size() > 1) {
|
||||
if (links.get(1).getText() != null)
|
||||
return links.get(1).getText();
|
||||
else
|
||||
return links.get(0).getText();
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package com.readrops.api.localfeed.rss;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
@Root(name = "enclosure", strict = false)
|
||||
public class RSSEnclosure {
|
||||
|
||||
@Attribute(required = false)
|
||||
private String type;
|
||||
|
||||
@Attribute(required = false)
|
||||
private String url;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package com.readrops.api.localfeed.rss;
|
||||
|
||||
import com.readrops.api.localfeed.AFeed;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
@Root(name = "rss", strict = false)
|
||||
public class RSSFeed extends AFeed {
|
||||
|
||||
@Element(name = "channel", required = false)
|
||||
private RSSChannel channel;
|
||||
|
||||
public RSSChannel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void setChannel(RSSChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
package com.readrops.api.localfeed.rss;
|
||||
|
||||
import org.simpleframework.xml.Element;
|
||||
import org.simpleframework.xml.ElementList;
|
||||
import org.simpleframework.xml.Namespace;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Root(name = "item", strict = false)
|
||||
public class RSSItem {
|
||||
|
||||
@Element
|
||||
private String title;
|
||||
|
||||
@Element(name = "link", required = false)
|
||||
private String link;
|
||||
|
||||
@Element(name = "imageLink", required = false)
|
||||
private String imageLink;
|
||||
|
||||
@ElementList(name = "content", inline = true, required = false)
|
||||
@Namespace(prefix = "media")
|
||||
private List<RSSMediaContent> mediaContents;
|
||||
|
||||
@ElementList(name = "enclosure", inline = true, required = false)
|
||||
private List<RSSEnclosure> enclosures;
|
||||
|
||||
@ElementList(name = "creator", inline = true, required = false)
|
||||
@Namespace(prefix = "dc", reference = "http://purl.org/dc/elements/1.1/")
|
||||
private List<String> creator;
|
||||
|
||||
@Element(required = false)
|
||||
private String author;
|
||||
|
||||
@Element(name = "pubDate", required = false)
|
||||
private String pubDate;
|
||||
|
||||
@Element(name = "date", required = false)
|
||||
@Namespace(prefix = "dc")
|
||||
private String date;
|
||||
|
||||
@Element(name = "description", required = false)
|
||||
private String description;
|
||||
|
||||
@Element(name = "encoded", required = false)
|
||||
@Namespace(prefix = "content")
|
||||
private String content;
|
||||
|
||||
@Element(required = false)
|
||||
private String guid;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public String getImageLink() {
|
||||
return imageLink;
|
||||
}
|
||||
|
||||
public void setImageLink(String imageLink) {
|
||||
this.imageLink = imageLink;
|
||||
}
|
||||
|
||||
public List<String> getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
public void setCreator(List<String> creator) {
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
public String getPubDate() {
|
||||
return pubDate;
|
||||
}
|
||||
|
||||
public void setPubDate(String pubDate) {
|
||||
this.pubDate = pubDate;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getGuid() {
|
||||
return guid;
|
||||
}
|
||||
|
||||
public void setGuid(String guid) {
|
||||
this.guid = guid;
|
||||
}
|
||||
|
||||
public List<RSSMediaContent> getMediaContents() {
|
||||
return mediaContents;
|
||||
}
|
||||
|
||||
public void setMediaContents(List<RSSMediaContent> mediaContents) {
|
||||
this.mediaContents = mediaContents;
|
||||
}
|
||||
|
||||
public List<RSSEnclosure> getEnclosures() {
|
||||
return enclosures;
|
||||
}
|
||||
|
||||
public void setEnclosures(List<RSSEnclosure> enclosures) {
|
||||
this.enclosures = enclosures;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
if (pubDate != null)
|
||||
return pubDate;
|
||||
else
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
if (creator != null && !creator.isEmpty())
|
||||
return creator.get(0);
|
||||
else
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package com.readrops.api.localfeed.rss;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Root;
|
||||
import org.simpleframework.xml.Text;
|
||||
|
||||
@Root(name = "link", strict = false)
|
||||
public class RSSLink {
|
||||
|
||||
@Text(required = false)
|
||||
private String text;
|
||||
|
||||
@Attribute(name = "href", required = false)
|
||||
private String href;
|
||||
|
||||
public RSSLink() {
|
||||
|
||||
}
|
||||
|
||||
public RSSLink(String text, String href) {
|
||||
this.text = text;
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
public String getHref() {
|
||||
return href;
|
||||
}
|
||||
|
||||
public void setHref(String href) {
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package com.readrops.api.localfeed.rss;
|
||||
|
||||
import org.simpleframework.xml.Attribute;
|
||||
import org.simpleframework.xml.Root;
|
||||
|
||||
@Root(name = "content", strict = false)
|
||||
public class RSSMediaContent {
|
||||
|
||||
@Attribute(required = false)
|
||||
private String url;
|
||||
|
||||
@Attribute(required = false)
|
||||
private String medium;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getMedium() {
|
||||
return medium;
|
||||
}
|
||||
|
||||
public void setMedium(String medium) {
|
||||
this.medium = medium;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.nonNullText
|
||||
import com.readrops.api.utils.nullableText
|
||||
import com.readrops.db.entities.Feed
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS1FeedAdapter : XmlAdapter<Feed> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): Feed {
|
||||
val konsume = inputStream.konsumeXml()
|
||||
val feed = Feed()
|
||||
|
||||
return try {
|
||||
konsume.child("RDF") {
|
||||
allChildrenAutoIgnore("channel") {
|
||||
feed.url = attributes.getValueOpt("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> siteUrl = nonNullText()
|
||||
"description" -> description = nullableText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsume.close()
|
||||
feed
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "description")
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS1ItemsAdapter : XmlAdapter<List<Item>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): List<Item> {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
val items = arrayListOf<Item>()
|
||||
|
||||
return try {
|
||||
konsumer.child("RDF") {
|
||||
allChildrenAutoIgnore("item") {
|
||||
val authors = arrayListOf<String?>()
|
||||
val about = attributes.getValueOpt("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
|
||||
val item = Item().apply {
|
||||
allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"link" -> link = nullableText()
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:creator" -> authors += nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.link == null) item.link = about
|
||||
?: throw ParseException("RSS1 link or about element is required")
|
||||
item.guid = item.link
|
||||
|
||||
if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull()
|
||||
.joinToString(limit = AUTHORS_MAX)
|
||||
|
||||
validateItem(item)
|
||||
|
||||
items += item
|
||||
}
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
if (item.title == null) throw ParseException("Item title is required")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "description", "date", "link", "creator", "encoded")
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.ParseException
|
||||
import com.readrops.api.utils.nonNullText
|
||||
import com.readrops.api.utils.nullableText
|
||||
import com.readrops.db.entities.Feed
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS2FeedAdapter : XmlAdapter<Feed> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): Feed {
|
||||
val konsume = inputStream.konsumeXml()
|
||||
val feed = Feed()
|
||||
|
||||
return try {
|
||||
konsume.child("rss") {
|
||||
child("channel") {
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
"title" -> name = Jsoup.parse(nonNullText()).text()
|
||||
"description" -> description = nullableText()
|
||||
"link" -> siteUrl = nullableText()
|
||||
"atom:link" -> {
|
||||
if (attributes.getValueOpt("rel") == "self")
|
||||
url = attributes.getValueOpt("href")
|
||||
}
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsume.close()
|
||||
feed
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "description", "link")
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.*
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS2ItemsAdapter : XmlAdapter<List<Item>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): List<Item> {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
val items = mutableListOf<Item>()
|
||||
|
||||
return try {
|
||||
konsumer.child("rss") {
|
||||
child("channel") {
|
||||
allChildrenAutoIgnore("item") {
|
||||
val creators = arrayListOf<String?>()
|
||||
|
||||
val item = Item().apply {
|
||||
allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = LibUtils.cleanText(nonNullText())
|
||||
"link" -> link = nonNullText()
|
||||
"author" -> author = nullableText()
|
||||
"dc:creator" -> creators += nullableText()
|
||||
"pubDate" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"guid" -> guid = nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
"enclosure" -> parseEnclosure(this, item = this@apply)
|
||||
"media:content" -> parseMediaContent(this, item = this@apply)
|
||||
"media:group" -> parseMediaGroup(this, item = this@apply)
|
||||
else -> skipContents() // for example media:description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeItem(item, creators)
|
||||
|
||||
items += item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
items
|
||||
} catch (e: KonsumerException) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnclosure(konsumer: Konsumer, item: Item) {
|
||||
if (konsumer.attributes.getValueOpt("type") != null
|
||||
&& LibUtils.isMimeImage(konsumer.attributes["type"]) && item.imageLink == null)
|
||||
item.imageLink = konsumer.attributes.getValueOpt("url")
|
||||
}
|
||||
|
||||
private fun isMediumImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOpt("medium") != null && LibUtils.isMimeImage(attributes["medium"])
|
||||
}
|
||||
|
||||
private fun isTypeImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOpt("type") != null && LibUtils.isMimeImage(attributes["type"])
|
||||
}
|
||||
|
||||
private fun parseMediaContent(konsumer: Konsumer, item: Item) {
|
||||
if ((isMediumImage(konsumer) || isTypeImage(konsumer)) && item.imageLink == null)
|
||||
item.imageLink = konsumer.attributes.getValueOpt("url")
|
||||
|
||||
konsumer.skipContents() // ignore media content sub elements
|
||||
}
|
||||
|
||||
private fun parseMediaGroup(konsumer: Konsumer, item: Item) {
|
||||
konsumer.allChildrenAutoIgnore("content") {
|
||||
when (tagName) {
|
||||
"media:content" -> parseMediaContent(this, item)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalizeItem(item: Item, creators: List<String?>) {
|
||||
item.apply {
|
||||
validateItem(this)
|
||||
|
||||
if (pubDate == null) pubDate = LocalDateTime.now()
|
||||
if (guid == null) guid = link
|
||||
if (author == null && creators.filterNotNull().isNotEmpty())
|
||||
author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "author", "creator", "pubDate", "date",
|
||||
"guid", "description", "encoded", "enclosure", "content", "group")
|
||||
}
|
||||
}
|
76
api/src/main/java/com/readrops/api/utils/DateUtils.java
Normal file
76
api/src/main/java/com/readrops/api/utils/DateUtils.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.readrops.api.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
import org.joda.time.format.DateTimeFormatter;
|
||||
import org.joda.time.format.DateTimeFormatterBuilder;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class DateUtils {
|
||||
|
||||
private static final String TAG = DateUtils.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Base of common RSS 2 date formats.
|
||||
* Examples :
|
||||
* Fri, 04 Jan 2019 22:21:46 GMT
|
||||
* Fri, 04 Jan 2019 22:21:46 +0000
|
||||
*/
|
||||
private static final String RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss";
|
||||
|
||||
private static final String GMT_PATTERN = "ZZZ";
|
||||
|
||||
private static final String OFFSET_PATTERN = "Z";
|
||||
|
||||
private static final String ISO_PATTERN = ".SSSZZ";
|
||||
|
||||
private static final String EDT_PATTERN = "zzz";
|
||||
|
||||
/**
|
||||
* Date pattern for format : 2019-01-04T22:21:46+00:00
|
||||
*/
|
||||
private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
|
||||
|
||||
@Nullable
|
||||
public static LocalDateTime parse(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
|
||||
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
|
||||
.toFormatter()
|
||||
.withLocale(Locale.ENGLISH)
|
||||
.withOffsetParsed();
|
||||
|
||||
return formatter.parseLocalDateTime(value);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String formattedDateByLocal(LocalDateTime dateTime) {
|
||||
return DateTimeFormat.mediumDate()
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime);
|
||||
}
|
||||
|
||||
public static String formattedDateTimeByLocal(LocalDateTime dateTime) {
|
||||
return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime);
|
||||
}
|
||||
}
|
@ -3,4 +3,9 @@ package com.readrops.api.utils
|
||||
import com.squareup.moshi.JsonReader
|
||||
|
||||
fun JsonReader.nextNullableString(): String? =
|
||||
if (peek() != JsonReader.Token.NULL) nextString() else nextNull()
|
||||
if (peek() != JsonReader.Token.NULL) nextString().ifEmpty { null }?.trim() else nextNull()
|
||||
|
||||
fun JsonReader.nextNonEmptyString(): String {
|
||||
val text = nextString()
|
||||
return if (text.isNotEmpty()) text.trim() else throw ParseException("Json value can't be null")
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.readrops.api.utils
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Whitespace
|
||||
import com.gitlab.mvysny.konsumexml.textRecursively
|
||||
|
||||
fun Konsumer.nonNullText(): String {
|
||||
val text = text(whitespace = Whitespace.preserve)
|
||||
return if (text.isNotEmpty()) text.trim() else throw ParseException("$name text can't be null")
|
||||
}
|
||||
|
||||
fun Konsumer.nullableText(): String? {
|
||||
val text = text(whitespace = Whitespace.preserve)
|
||||
return if (text.isNotEmpty()) text.trim() else null
|
||||
}
|
||||
|
||||
fun Konsumer.nullableTextRecursively(): String? {
|
||||
val text = textRecursively()
|
||||
return if (text.isNotEmpty()) text.trim() else null
|
||||
}
|
@ -4,10 +4,15 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class LibUtils {
|
||||
|
||||
@ -28,6 +33,8 @@ public final class LibUtils {
|
||||
public static final int HTTP_NOT_FOUND = 404;
|
||||
public static final int HTTP_CONFLICT = 409;
|
||||
|
||||
private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)";
|
||||
|
||||
|
||||
public static String inputStreamToString(InputStream input) {
|
||||
Scanner scanner = new Scanner(input).useDelimiter("\\A");
|
||||
@ -44,4 +51,26 @@ public final class LibUtils {
|
||||
return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
|
||||
|| type.equals("image/png");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String parseContentType(String header) {
|
||||
Matcher matcher = Pattern.compile(RSS_CONTENT_TYPE_REGEX)
|
||||
.matcher(header);
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove html tags and trim the text
|
||||
*
|
||||
* @param text string to clean
|
||||
* @return cleaned text
|
||||
*/
|
||||
public static String cleanText(String text) {
|
||||
return Jsoup.parse(text).text().trim();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class LocalRSSHelperTest {
|
||||
|
||||
@Test
|
||||
fun standardContentTypesTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/rdf+xml"),
|
||||
LocalRSSHelper.RSSType.RSS_1)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/rss+xml"),
|
||||
LocalRSSHelper.RSSType.RSS_2)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/atom+xml"),
|
||||
LocalRSSHelper.RSSType.ATOM)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/json"),
|
||||
LocalRSSHelper.RSSType.JSONFEED)
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/feed+json"),
|
||||
LocalRSSHelper.RSSType.JSONFEED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonSupportedContentTypesTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSType("application/xml"),
|
||||
LocalRSSHelper.RSSType.UNKNOWN)
|
||||
assertEquals(LocalRSSHelper.getRSSType("text/xml"),
|
||||
LocalRSSHelper.RSSType.UNKNOWN)
|
||||
assertEquals(LocalRSSHelper.getRSSType("text/html"),
|
||||
LocalRSSHelper.RSSType.UNKNOWN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rss1ContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss1full.xsl"?>
|
||||
<?xml-stylesheet type="text/css" media="screen" href="http://rss.slashdot.org/~d/styles/itemcontent.css"?>
|
||||
<rdf:RDF xmlns:admin="http://webns.net/mvcb/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/"
|
||||
""".trimIndent().toByteArray()
|
||||
)), LocalRSSHelper.RSSType.RSS_1)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun rss2ContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<rss
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
version="2.0"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
|
||||
</rss>""".toByteArray()
|
||||
)), LocalRSSHelper.RSSType.RSS_2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atomContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
</feed>""".toByteArray()
|
||||
)), LocalRSSHelper.RSSType.ATOM)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>""".trimMargin().toByteArray()
|
||||
)), LocalRSSHelper.RSSType.UNKNOWN)
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.readrops.api.localfeed
|
||||
|
||||
import com.readrops.api.localfeed.atom.ATOMFeedAdapter
|
||||
import com.readrops.api.localfeed.atom.ATOMItemsAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class XmlAdapterTest {
|
||||
|
||||
@Test
|
||||
fun xmlFeedAdapterFactoryTest() {
|
||||
assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1FeedAdapter)
|
||||
assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2FeedAdapter)
|
||||
assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter)
|
||||
|
||||
Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun xmlItemsAdapterFactoryTest() {
|
||||
assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1ItemsAdapter)
|
||||
assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2ItemsAdapter)
|
||||
assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter)
|
||||
|
||||
Assert.assertThrows(IllegalArgumentException::class.java) { XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN) }
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
package com.readrops.app;
|
||||
|
||||
import com.readrops.app.utils.DateUtils;
|
||||
package com.readrops.api.utils;
|
||||
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.junit.Test;
|
||||
@ -13,48 +11,48 @@ public class DateUtilsTest {
|
||||
public void rssDateTest() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46 GMT")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46 GMT")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rssDate2Test() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46 +0000")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46 +0000")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rssDate3Test() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atomJsonDateTest() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2019-01-04T22:21:46+00:00")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("2019-01-04T22:21:46+00:00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void atomJsonDate2Test() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2019-01-04T22:21:46-0000")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("2019-01-04T22:21:46-0000")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isoPatternTest() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2020, 6, 30, 11, 39, 37, 206);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2020-06-30T11:39:37.206-07:00")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("2020-06-30T11:39:37.206-07:00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void edtPatternTest() {
|
||||
LocalDateTime dateTime = new LocalDateTime(2020, 7, 17, 16, 30, 0);
|
||||
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 17 Jul 2020 16:30:00 EDT")));
|
||||
assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 17 Jul 2020 16:30:00 EDT")));
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package com.readrops.api.utils
|
||||
|
||||
import com.squareup.moshi.JsonReader
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNull
|
||||
import okio.Buffer
|
||||
import org.junit.Test
|
||||
|
||||
class JsonReaderExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun nextNullableStringNullCaseTest() {
|
||||
val reader = JsonReader.of(Buffer().readFrom("""
|
||||
{
|
||||
"field": null
|
||||
}
|
||||
""".trimIndent().byteInputStream()))
|
||||
|
||||
reader.beginObject()
|
||||
reader.nextName()
|
||||
|
||||
assertNull(reader.nextNullableString())
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextNullableStringEmptyCaseTest() {
|
||||
val reader = JsonReader.of(Buffer().readFrom("""
|
||||
{
|
||||
"field": ""
|
||||
}
|
||||
""".trimIndent().byteInputStream()))
|
||||
|
||||
reader.beginObject()
|
||||
reader.nextName()
|
||||
|
||||
assertNull(reader.nextNullableString())
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextNullableValueNormalCaseTest() {
|
||||
val reader = JsonReader.of(Buffer().readFrom("""
|
||||
{
|
||||
"field": "value"
|
||||
}
|
||||
""".trimIndent().byteInputStream()))
|
||||
|
||||
reader.beginObject()
|
||||
reader.nextName()
|
||||
|
||||
assertEquals(reader.nextNullableString(), "value")
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextNonEmptyStringTest() {
|
||||
val reader = JsonReader.of(Buffer().readFrom("""
|
||||
{
|
||||
"field": "value"
|
||||
}
|
||||
""".trimIndent().byteInputStream()))
|
||||
|
||||
reader.beginObject()
|
||||
reader.nextName()
|
||||
|
||||
assertEquals(reader.nextNullableString(), "value")
|
||||
reader.endObject()
|
||||
}
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun nextNonEmptyStringEmptyCaseTest() {
|
||||
val reader = JsonReader.of(Buffer().readFrom("""
|
||||
{
|
||||
"field": ""
|
||||
}
|
||||
""".trimIndent().byteInputStream()))
|
||||
|
||||
reader.beginObject()
|
||||
reader.nextName()
|
||||
|
||||
reader.nextNonEmptyString()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.readrops.api.utils
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.KonsumerException
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class KonsumerExtensionsTest {
|
||||
|
||||
@Test(expected = KonsumerException::class)
|
||||
fun nonNullTextNullCaseTest() {
|
||||
val xml = """
|
||||
<description></description>
|
||||
""".trimIndent()
|
||||
|
||||
xml.konsumeXml().apply {
|
||||
child("description") { nonNullText() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonNullTextNonNullCaseTest() {
|
||||
val xml = """
|
||||
<description>
|
||||
description
|
||||
</description>
|
||||
""".trimIndent()
|
||||
|
||||
xml.konsumeXml().apply {
|
||||
val description = child("description") { nonNullText() }
|
||||
assertEquals(description, "description")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullableTextNullCaseTest() {
|
||||
val xml = """
|
||||
<description></description>
|
||||
""".trimIndent()
|
||||
|
||||
xml.konsumeXml().apply {
|
||||
val description = child("description") { nullableText() }
|
||||
assertNull(description)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullableTextNonNullCaseTest() {
|
||||
val xml = """
|
||||
<description>
|
||||
description
|
||||
</description>
|
||||
""".trimIndent()
|
||||
|
||||
xml.konsumeXml().apply {
|
||||
val description = child("description") { nullableText() }
|
||||
assertEquals(description, "description")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullableTextRecursivelyNullCaseTest() {
|
||||
val xml = """
|
||||
<description></description>
|
||||
""".trimIndent()
|
||||
|
||||
xml.konsumeXml().apply {
|
||||
val description = child("description") { nullableTextRecursively() }
|
||||
assertNull(description)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullableTextRecursivelyNonNullCaseTest() {
|
||||
val xml = """
|
||||
<description>
|
||||
descrip<a>tion</a>
|
||||
</description>
|
||||
""".trimIndent()
|
||||
|
||||
xml.konsumeXml().apply {
|
||||
val description = child("description") { nullableTextRecursively() }
|
||||
assertEquals(description, "description")
|
||||
}
|
||||
}
|
||||
}
|
25
api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt
Normal file
25
api/src/test/java/com/readrops/api/utils/LibUtilsTest.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package com.readrops.api.utils
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class LibUtilsTest {
|
||||
|
||||
@Test
|
||||
fun contentTypeWithCharsetTest() {
|
||||
assertEquals(LibUtils.parseContentType("application/rss+xml; charset=UTF-8"),
|
||||
"application/rss+xml")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun contentTypeWithoutCharsetText() {
|
||||
assertEquals(LibUtils.parseContentType("text/xml"),
|
||||
"text/xml")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanTextTest() {
|
||||
val text = " <p>This is a text<br/>to</p> clean "
|
||||
assertEquals("This is a text to clean", LibUtils.cleanText(text))
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ActivityItemBinding;
|
||||
import com.readrops.app.utils.DateUtils;
|
||||
import com.readrops.api.utils.DateUtils;
|
||||
import com.readrops.app.utils.GlideApp;
|
||||
import com.readrops.app.utils.PermissionManager;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
|
@ -29,7 +29,7 @@ import com.readrops.app.R;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
import com.readrops.app.databinding.ListItemBinding;
|
||||
import com.readrops.app.utils.DateUtils;
|
||||
import com.readrops.api.utils.DateUtils;
|
||||
import com.readrops.app.utils.GlideRequests;
|
||||
import com.readrops.app.utils.Utils;
|
||||
|
||||
|
@ -2,30 +2,25 @@ package com.readrops.app.repositories;
|
||||
|
||||
import android.accounts.NetworkErrorException;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource;
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.utils.HttpManager;
|
||||
import com.readrops.api.utils.LibUtils;
|
||||
import com.readrops.api.utils.ParseException;
|
||||
import com.readrops.api.utils.UnknownFormatException;
|
||||
import com.readrops.app.utils.FeedInsertionResult;
|
||||
import com.readrops.app.utils.HtmlParser;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.utils.matchers.FeedMatcher;
|
||||
import com.readrops.app.utils.matchers.ItemMatcher;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.api.localfeed.AFeed;
|
||||
import com.readrops.api.localfeed.RSSQuery;
|
||||
import com.readrops.api.localfeed.RSSQueryResult;
|
||||
import com.readrops.api.localfeed.atom.ATOMFeed;
|
||||
import com.readrops.api.localfeed.json.JSONFeed;
|
||||
import com.readrops.api.localfeed.rss.RSSFeed;
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.utils.LibUtils;
|
||||
import com.readrops.api.utils.ParseException;
|
||||
import com.readrops.api.utils.UnknownFormatException;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
@ -33,20 +28,24 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import kotlin.Pair;
|
||||
import okhttp3.Headers;
|
||||
|
||||
public class LocalFeedRepository extends ARepository<Void> {
|
||||
|
||||
private static final String TAG = LocalFeedRepository.class.getSimpleName();
|
||||
|
||||
private LocalRSSDataSource dataSource;
|
||||
|
||||
public LocalFeedRepository(@NonNull Context context, @Nullable Account account) {
|
||||
super(context, account);
|
||||
|
||||
syncResult = new SyncResult();
|
||||
dataSource = new LocalRSSDataSource(HttpManager.getInstance().getOkHttpClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -64,47 +63,31 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
return Observable.create(emitter -> {
|
||||
List<Feed> feedList;
|
||||
|
||||
if (feeds == null || feeds.size() == 0)
|
||||
if (feeds == null || feeds.isEmpty()) {
|
||||
feedList = database.feedDao().getFeeds(account.getId());
|
||||
else
|
||||
feedList = new ArrayList<>(feeds);
|
||||
|
||||
RSSQuery rssQuery = new RSSQuery();
|
||||
List<FeedInsertionResult> syncErrors = new ArrayList<>();
|
||||
} else {
|
||||
feedList = feeds;
|
||||
}
|
||||
|
||||
for (Feed feed : feedList) {
|
||||
emitter.onNext(feed);
|
||||
FeedInsertionResult syncError = new FeedInsertionResult();
|
||||
|
||||
try {
|
||||
HashMap<String, String> headers = new HashMap<>();
|
||||
if (feed.getEtag() != null)
|
||||
headers.put(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
|
||||
if (feed.getLastModified() != null)
|
||||
headers.put(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified());
|
||||
Headers.Builder headers = new Headers.Builder();
|
||||
if (feed.getEtag() != null) {
|
||||
headers.add(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
|
||||
}
|
||||
if (feed.getLastModified() != null) {
|
||||
headers.add(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified());
|
||||
}
|
||||
|
||||
RSSQueryResult queryResult = rssQuery.queryUrl(feed.getUrl(), headers);
|
||||
if (queryResult != null && queryResult.getException() == null)
|
||||
insertNewItems(queryResult.getFeed(), queryResult.getRssType());
|
||||
else if (queryResult != null && queryResult.getException() != null) {
|
||||
Exception e = queryResult.getException();
|
||||
Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build());
|
||||
|
||||
if (e instanceof UnknownFormatException)
|
||||
syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
|
||||
else if (e instanceof NetworkErrorException)
|
||||
syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
|
||||
|
||||
syncError.setFeed(feed);
|
||||
syncErrors.add(syncError);
|
||||
if (pair != null) {
|
||||
insertNewItems(feed, pair.getSecond());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (e instanceof IOException)
|
||||
syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
|
||||
else
|
||||
syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
|
||||
|
||||
syncError.setFeed(feed);
|
||||
syncErrors.add(syncError);
|
||||
Log.d(TAG, "sync: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,28 +104,26 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
FeedInsertionResult insertionResult = new FeedInsertionResult();
|
||||
|
||||
try {
|
||||
RSSQuery rssNet = new RSSQuery();
|
||||
RSSQueryResult queryResult = rssNet.queryUrl(parsingResult.getUrl(), new HashMap<>());
|
||||
Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(parsingResult.getUrl(),
|
||||
null);
|
||||
Feed feed = insertFeed(pair.getFirst(), parsingResult);
|
||||
|
||||
if (queryResult != null && queryResult.getException() == null) {
|
||||
Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType(), parsingResult);
|
||||
if (feed != null) {
|
||||
insertionResult.setFeed(feed);
|
||||
insertionResult.setParsingResult(parsingResult);
|
||||
insertionResults.add(insertionResult);
|
||||
}
|
||||
} else if (queryResult != null && queryResult.getException() != null) {
|
||||
insertionResult.setParsingResult(parsingResult);
|
||||
insertionResult.setInsertionError(getErrorFromException(queryResult.getException()));
|
||||
|
||||
insertionResults.add(insertionResult);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (e instanceof IOException)
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
|
||||
else
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
|
||||
|
||||
} catch (UnknownFormatException e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
|
||||
} catch (NetworkErrorException | IOException e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
|
||||
} finally {
|
||||
insertionResult.setParsingResult(parsingResult);
|
||||
insertionResults.add(insertionResult);
|
||||
}
|
||||
@ -152,67 +133,38 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
});
|
||||
}
|
||||
|
||||
private void insertNewItems(AFeed feed, RSSQuery.RSSType type) throws ParseException {
|
||||
Feed dbFeed;
|
||||
List<Item> items;
|
||||
@SuppressWarnings("SimplifyStreamApiCallChains")
|
||||
private void insertNewItems(Feed feed, List<Item> items) {
|
||||
database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), feed.getId());
|
||||
|
||||
switch (type) {
|
||||
case RSS_2:
|
||||
dbFeed = database.feedDao().getFeedByUrl(((RSSFeed) feed).getChannel().getFeedUrl(), account.getId());
|
||||
items = ItemMatcher.itemsFromRSS(((RSSFeed) feed).getChannel().getItems(), dbFeed);
|
||||
break;
|
||||
case RSS_ATOM:
|
||||
dbFeed = database.feedDao().getFeedByUrl(((ATOMFeed) feed).getUrl(), account.getId());
|
||||
items = ItemMatcher.itemsFromATOM(((ATOMFeed) feed).getEntries(), dbFeed);
|
||||
break;
|
||||
case RSS_JSON:
|
||||
dbFeed = database.feedDao().getFeedByUrl(((JSONFeed) feed).getFeedUrl(), account.getId());
|
||||
items = ItemMatcher.itemsFromJSON(((JSONFeed) feed).getItems(), dbFeed);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown RSS type");
|
||||
}
|
||||
|
||||
database.feedDao().updateHeaders(dbFeed.getEtag(), dbFeed.getLastModified(), dbFeed.getId());
|
||||
Collections.sort(items, Item::compareTo);
|
||||
|
||||
int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context, SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
|
||||
if (maxItems > 0 && items.size() > maxItems)
|
||||
int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context,
|
||||
SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
|
||||
if (maxItems > 0 && items.size() > maxItems) {
|
||||
items = items.subList(items.size() - maxItems, items.size());
|
||||
|
||||
insertItems(items, dbFeed);
|
||||
}
|
||||
|
||||
private Feed insertFeed(AFeed feed, RSSQuery.RSSType type, ParsingResult parsingResult) {
|
||||
Feed dbFeed;
|
||||
switch (type) {
|
||||
case RSS_2:
|
||||
dbFeed = FeedMatcher.feedFromRSS((RSSFeed) feed);
|
||||
break;
|
||||
case RSS_ATOM:
|
||||
dbFeed = FeedMatcher.feedFromATOM((ATOMFeed) feed);
|
||||
break;
|
||||
case RSS_JSON:
|
||||
dbFeed = FeedMatcher.feedFromJSON((JSONFeed) feed);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown RSS type");
|
||||
items.stream().forEach(item -> item.setFeedId(feed.getId()));
|
||||
insertItems(items, feed);
|
||||
}
|
||||
|
||||
dbFeed.setFolderId(parsingResult.getFolderId());
|
||||
private Feed insertFeed(Feed feed, ParsingResult parsingResult) {
|
||||
feed.setFolderId(parsingResult.getFolderId());
|
||||
|
||||
if (database.feedDao().feedExists(dbFeed.getUrl(), account.getId()))
|
||||
if (database.feedDao().feedExists(feed.getUrl(), account.getId())) {
|
||||
return null; // feed already inserted
|
||||
}
|
||||
|
||||
setFeedColors(dbFeed);
|
||||
dbFeed.setAccountId(account.getId());
|
||||
setFeedColors(feed);
|
||||
feed.setAccountId(account.getId());
|
||||
|
||||
// we need empty headers to query the feed just after, without any 304 result
|
||||
dbFeed.setEtag(null);
|
||||
dbFeed.setLastModified(null);
|
||||
feed.setEtag(null);
|
||||
feed.setLastModified(null);
|
||||
|
||||
dbFeed.setId((int) (database.feedDao().compatInsert(dbFeed)));
|
||||
return dbFeed;
|
||||
feed.setId((int) (database.feedDao().compatInsert(feed)));
|
||||
return feed;
|
||||
}
|
||||
|
||||
private void insertItems(Collection<Item> items, Feed feed) {
|
||||
@ -253,13 +205,4 @@ public class LocalFeedRepository extends ARepository<Void> {
|
||||
syncResult.getItems().addAll(itemsToInsert);
|
||||
database.itemDao().insert(itemsToInsert);
|
||||
}
|
||||
|
||||
private FeedInsertionResult.FeedInsertionError getErrorFromException(Exception e) {
|
||||
if (e instanceof UnknownFormatException)
|
||||
return FeedInsertionResult.FeedInsertionError.FORMAT_ERROR;
|
||||
else if (e instanceof NetworkErrorException)
|
||||
return FeedInsertionResult.FeedInsertionError.NETWORK_ERROR;
|
||||
else
|
||||
return FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
package com.readrops.app.utils;
|
||||
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
import org.joda.time.format.DateTimeFormatter;
|
||||
import org.joda.time.format.DateTimeFormatterBuilder;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class DateUtils {
|
||||
|
||||
/**
|
||||
* Base of common RSS 2 date formats.
|
||||
* Examples :
|
||||
* Fri, 04 Jan 2019 22:21:46 GMT
|
||||
* Fri, 04 Jan 2019 22:21:46 +0000
|
||||
*/
|
||||
private static final String RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss";
|
||||
|
||||
private static final String GMT_PATTERN = "ZZZ";
|
||||
|
||||
private static final String OFFSET_PATTERN = "Z";
|
||||
|
||||
private static final String ISO_PATTERN = ".SSSZZ";
|
||||
|
||||
private static final String EDT_PATTERN = "zzz";
|
||||
|
||||
/**
|
||||
* Date pattern for format : 2019-01-04T22:21:46+00:00
|
||||
*/
|
||||
private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
|
||||
|
||||
public static LocalDateTime stringToLocalDateTime(String value) {
|
||||
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
|
||||
.appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
|
||||
.appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
|
||||
.appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
|
||||
.toFormatter()
|
||||
.withLocale(Locale.ENGLISH)
|
||||
.withOffsetParsed();
|
||||
|
||||
return formatter.parseLocalDateTime(value);
|
||||
}
|
||||
|
||||
public static String formattedDateByLocal(LocalDateTime dateTime) {
|
||||
return DateTimeFormat.mediumDate()
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime);
|
||||
}
|
||||
|
||||
public static String formattedDateTimeByLocal(LocalDateTime dateTime) {
|
||||
return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
|
||||
.withLocale(Locale.getDefault())
|
||||
.print(dateTime);
|
||||
}
|
||||
}
|
@ -17,8 +17,6 @@ import androidx.annotation.NonNull;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.readrops.api.utils.HttpManager;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
@ -97,16 +95,6 @@ public final class Utils {
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove html tags and trim the text
|
||||
*
|
||||
* @param text string to clean
|
||||
* @return cleaned text
|
||||
*/
|
||||
public static String cleanText(String text) {
|
||||
return Jsoup.parse(text).text().trim();
|
||||
}
|
||||
|
||||
public static Bitmap getBitmapFromDrawable(Drawable drawable) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
|
||||
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
|
@ -1,65 +0,0 @@
|
||||
package com.readrops.app.utils.matchers;
|
||||
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.api.localfeed.atom.ATOMFeed;
|
||||
import com.readrops.api.localfeed.json.JSONFeed;
|
||||
import com.readrops.api.localfeed.rss.RSSChannel;
|
||||
import com.readrops.api.localfeed.rss.RSSFeed;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
public final class FeedMatcher {
|
||||
|
||||
public static Feed feedFromRSS(RSSFeed rssFeed) {
|
||||
Feed feed = new Feed();
|
||||
RSSChannel channel = rssFeed.getChannel();
|
||||
|
||||
feed.setName(Jsoup.parse(channel.getTitle()).text());
|
||||
feed.setUrl(channel.getFeedUrl());
|
||||
feed.setSiteUrl(channel.getUrl());
|
||||
feed.setDescription(channel.getDescription());
|
||||
feed.setLastUpdated(channel.getLastUpdated());
|
||||
|
||||
feed.setEtag(rssFeed.getEtag());
|
||||
feed.setLastModified(rssFeed.getLastModified());
|
||||
|
||||
feed.setFolderId(null);
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
public static Feed feedFromATOM(ATOMFeed atomFeed) {
|
||||
Feed feed = new Feed();
|
||||
|
||||
feed.setName(atomFeed.getTitle());
|
||||
feed.setDescription(atomFeed.getSubtitle());
|
||||
feed.setUrl(atomFeed.getUrl());
|
||||
feed.setSiteUrl(atomFeed.getWebsiteUrl());
|
||||
feed.setDescription(atomFeed.getSubtitle());
|
||||
feed.setLastUpdated(atomFeed.getUpdated());
|
||||
|
||||
feed.setEtag(atomFeed.getEtag());
|
||||
feed.setLastModified(atomFeed.getLastModified());
|
||||
|
||||
feed.setFolderId(null);
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
public static Feed feedFromJSON(JSONFeed jsonFeed) {
|
||||
Feed feed = new Feed();
|
||||
|
||||
feed.setName(jsonFeed.getTitle());
|
||||
feed.setUrl(jsonFeed.getFeedUrl());
|
||||
feed.setSiteUrl(jsonFeed.getHomePageUrl());
|
||||
feed.setDescription(jsonFeed.getDescription());
|
||||
|
||||
feed.setEtag(jsonFeed.getEtag());
|
||||
feed.setLastModified(jsonFeed.getLastModified());
|
||||
feed.setIconUrl(jsonFeed.getFaviconUrl());
|
||||
|
||||
feed.setFolderId(null);
|
||||
|
||||
return feed;
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package com.readrops.app.utils.matchers;
|
||||
|
||||
import com.readrops.app.utils.DateUtils;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.api.localfeed.atom.ATOMEntry;
|
||||
import com.readrops.api.localfeed.json.JSONItem;
|
||||
import com.readrops.api.localfeed.rss.RSSEnclosure;
|
||||
import com.readrops.api.localfeed.rss.RSSItem;
|
||||
import com.readrops.api.localfeed.rss.RSSMediaContent;
|
||||
import com.readrops.api.utils.ParseException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class ItemMatcher {
|
||||
|
||||
public static List<Item> itemsFromRSS(List<RSSItem> items, Feed feed) throws ParseException {
|
||||
List<Item> dbItems = new ArrayList<>();
|
||||
|
||||
for (RSSItem item : items) {
|
||||
Item newItem = new Item();
|
||||
|
||||
newItem.setAuthor(item.getAuthor());
|
||||
newItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
|
||||
newItem.setDescription(item.getDescription());
|
||||
newItem.setGuid(item.getGuid() != null ? item.getGuid() : item.getLink());
|
||||
newItem.setTitle(Utils.cleanText(item.getTitle()));
|
||||
|
||||
try {
|
||||
newItem.setPubDate(DateUtils.stringToLocalDateTime(item.getDate()));
|
||||
} catch (Exception e) {
|
||||
throw new ParseException();
|
||||
}
|
||||
|
||||
newItem.setLink(item.getLink());
|
||||
newItem.setFeedId(feed.getId());
|
||||
|
||||
if (item.getMediaContents() != null && !item.getMediaContents().isEmpty()) {
|
||||
for (RSSMediaContent mediaContent : item.getMediaContents()) {
|
||||
if (mediaContent.getMedium() != null && Utils.isTypeImage(mediaContent.getMedium())) {
|
||||
newItem.setImageLink(mediaContent.getUrl());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item.getEnclosures() != null) {
|
||||
for (RSSEnclosure enclosure : item.getEnclosures()) {
|
||||
if (enclosure.getType() != null && Utils.isTypeImage(enclosure.getType())
|
||||
&& enclosure.getUrl() != null) {
|
||||
newItem.setImageLink(enclosure.getUrl());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dbItems.add(newItem);
|
||||
}
|
||||
|
||||
return dbItems;
|
||||
}
|
||||
|
||||
public static List<Item> itemsFromATOM(List<ATOMEntry> items, Feed feed) throws ParseException {
|
||||
List<Item> dbItems = new ArrayList<>();
|
||||
|
||||
for (ATOMEntry item : items) {
|
||||
Item dbItem = new Item();
|
||||
|
||||
dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
|
||||
dbItem.setDescription(item.getSummary());
|
||||
dbItem.setGuid(item.getId());
|
||||
dbItem.setTitle(Utils.cleanText(item.getTitle()));
|
||||
|
||||
try {
|
||||
dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getUpdated()));
|
||||
} catch (Exception e) {
|
||||
throw new ParseException();
|
||||
}
|
||||
|
||||
dbItem.setLink(item.getUrl());
|
||||
|
||||
dbItem.setFeedId(feed.getId());
|
||||
|
||||
dbItems.add(dbItem);
|
||||
}
|
||||
|
||||
return dbItems;
|
||||
}
|
||||
|
||||
public static List<Item> itemsFromJSON(List<JSONItem> items, Feed feed) throws ParseException {
|
||||
List<Item> dbItems = new ArrayList<>();
|
||||
|
||||
for (JSONItem item : items) {
|
||||
Item dbItem = new Item();
|
||||
|
||||
if (item.getAuthor() != null)
|
||||
dbItem.setAuthor(item.getAuthor().getName());
|
||||
|
||||
dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
|
||||
dbItem.setDescription(item.getSummary());
|
||||
dbItem.setGuid(item.getId());
|
||||
dbItem.setTitle(Utils.cleanText(item.getTitle()));
|
||||
|
||||
try {
|
||||
dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getPubDate()));
|
||||
} catch (Exception e) {
|
||||
throw new ParseException();
|
||||
}
|
||||
|
||||
dbItem.setLink(item.getUrl());
|
||||
|
||||
dbItem.setFeedId(feed.getId());
|
||||
|
||||
dbItems.add(dbItem);
|
||||
}
|
||||
|
||||
return dbItems;
|
||||
}
|
||||
}
|
@ -7,13 +7,14 @@ import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource;
|
||||
import com.readrops.api.utils.HttpManager;
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.app.utils.FeedInsertionResult;
|
||||
import com.readrops.app.utils.HtmlParser;
|
||||
import com.readrops.app.utils.ParsingResult;
|
||||
import com.readrops.api.localfeed.RSSQuery;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -47,13 +48,12 @@ public class AddFeedsViewModel extends AndroidViewModel {
|
||||
|
||||
public Single<List<ParsingResult>> parseUrl(String url) {
|
||||
return Single.create(emitter -> {
|
||||
RSSQuery rssApi = new RSSQuery();
|
||||
LocalRSSDataSource dataSource = new LocalRSSDataSource(HttpManager.getInstance().getOkHttpClient());
|
||||
List<ParsingResult> results = new ArrayList<>();
|
||||
|
||||
if (rssApi.isUrlFeedLink(url)) {
|
||||
if (dataSource.isUrlRSSResource(url)) {
|
||||
ParsingResult parsingResult = new ParsingResult(url, null);
|
||||
results.add(parsingResult);
|
||||
|
||||
} else {
|
||||
results.addAll(HtmlParser.getFeedLink(url));
|
||||
}
|
||||
|
@ -7,17 +7,9 @@ import com.readrops.app.utils.Utils;
|
||||
import org.junit.Test;
|
||||
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
||||
public class UtilsTest {
|
||||
|
||||
@Test
|
||||
public void cleanTextTest() {
|
||||
String text = " <p>This is a text<br/>to</p> clean ";
|
||||
|
||||
assertEquals("This is a text to clean", Utils.cleanText(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void colorTooBrightTest() {
|
||||
assertTrue(Utils.isColorTooBright(-986896));
|
||||
|
@ -29,6 +29,9 @@ public abstract class FeedDao implements BaseDao<Feed> {
|
||||
@Query("Select * from Feed Where id = :feedId")
|
||||
public abstract Feed getFeedById(int feedId);
|
||||
|
||||
@Query("Select id From Feed Where url = :url and account_id = :accountId")
|
||||
public abstract int getFeedIdByUrl(String url, int accountId);
|
||||
|
||||
@Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end")
|
||||
public abstract boolean feedExists(String feedUrl, int accountId);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user