1
0
Fork 0
mirror of https://gitlab.com/news-flash/article_scraper.git synced 2025-07-07 16:15:32 +02:00

Compare commits

...

24 commits

Author SHA1 Message Date
Jan Lukas Gernert
9f349f8c6f need reqwest streams 2025-05-04 18:00:59 +02:00
Jan Lukas Gernert
498008f630 bump version 2025-05-04 17:51:30 +02:00
Jan Lukas Gernert
ee53f58aeb Merge branch 'empty-body' into 'master'
check for empty http response and parsed documents without root element

See merge request news-flash/article_scraper!11
2025-05-04 15:50:59 +00:00
Jan Lukas Gernert
06990acbc0 fix libxml CI build 2025-05-04 17:38:46 +02:00
Jan Lukas Gernert
f361392c04 check for empty http response and parsed documents without root element 2025-05-04 17:34:33 +02:00
Jan Lukas Gernert
9b374a28c7 update ftr-site-config 2025-04-05 15:47:08 +02:00
Jan Lukas Gernert
b92500fca2 better error messages 2025-04-05 15:45:41 +02:00
Jan Lukas Gernert
0978335d3b [f] ignore url harvest error 2025-03-28 17:18:03 +01:00
Jan Lukas Gernert
9f56ed03b8 article_scraper: don't specify reqwest features 2025-03-10 13:42:31 +01:00
Jan Lukas Gernert
8cfcd6d9f3 clippy 2025-01-17 03:05:55 +01:00
Jan Lukas Gernert
ca1cc47af1 update CI image 2025-01-17 03:02:40 +01:00
Jan Lukas Gernert
7c658a4ba8 resolver 2 2025-01-17 02:58:41 +01:00
Jan Lukas Gernert
89eb87fa85 update thiserror, ftr-site-config submodule and bump version 2025-01-17 02:55:59 +01:00
Jan Lukas Gernert
7fcb781c68 remove useless format! 2024-11-02 11:34:47 +01:00
Jan Lukas Gernert
11ee29feda thumbnail: check for attribute with name property as well (fixes #4) 2024-11-02 11:30:55 +01:00
Jan Lukas Gernert
b3ce28632d update submodule 2024-07-10 11:59:21 +02:00
Jan Lukas Gernert
6932902b7b update CI image 2024-07-06 23:43:23 +02:00
Jan Lukas Gernert
c16e11fdda init parser according to (https://gitlab.gnome.org/GNOME/libxml2/-/wikis/Thread-safety) 2024-07-06 23:38:43 +02:00
Jan Lukas Gernert
f4e4e64b9e absolute default size for embedded youtube videos 2024-06-10 22:27:10 +02:00
Jan Lukas Gernert
df8ebcbb35 treat iframes as valid emtry tags 2024-06-10 22:06:48 +02:00
Jan Lukas Gernert
e01c8e9d34 negative score for thumbnails with emoji alt 2024-06-10 20:40:19 +02:00
Jan Lukas Gernert
06018d98d4 replace emoji images 2024-06-08 23:18:00 +02:00
Jan Lukas Gernert
11e9261bf2 fmt 2024-06-08 01:03:00 +02:00
Jan Lukas Gernert
3e5654e197 fix tests 2024-06-08 01:02:52 +02:00
27 changed files with 1192 additions and 155 deletions

View file

@ -4,12 +4,14 @@ stages:
run-build:
stage: build
image: rust:1.77
image: rust:1.86
before_script:
- rustup component add rustfmt
- rustup component add clippy
- export LIBXML2=$(pkg-config libxml-2.0 --variable=libdir)/libxml2.so
script:
- rustc --version && cargo --version
- echo $LIBXML2
- cargo fmt -- --check
- cargo clippy --all-targets --all-features -- -D warnings
- cargo build --release

View file

@ -1,6 +1,10 @@
[workspace]
members = ["article_scraper", "article_scraper_cli"]
resolver = "2"
members = [
"article_scraper",
"article_scraper_cli",
]
[workspace.package]
version = "2.1.2"
authors = ["Jan Lukas Gernert <jangernert@gmail.com>"]
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://gitlab.com/news-flash/article_scraper"

View file

@ -1,31 +1,33 @@
[package]
name = "article_scraper"
version = "2.1.0"
authors = ["Jan Lukas Gernert <jangernert@gmail.com>"]
edition = "2018"
license = "GPL-3.0-or-later"
description = "Scrap article contents from the web. Powered by fivefilters full text feed configurations & mozilla readability."
repository = "https://gitlab.com/news-flash/article_scraper"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
readme = "../Readme.md"
keywords = ["article", "scrape", "full-text", "readability"]
exclude = ["resources/tests"]
[dependencies]
thiserror = "1.0"
thiserror = "2.0"
libxml = "0.3"
reqwest = { version = "0.12", features = ["json", "native-tls", "gzip", "brotli", "stream"] }
reqwest = { version = "0.12", features = ["stream"] }
tokio = { version = "1", features = ["macros", "fs", "io-util"] }
url = "2.5"
regex = "1.10"
regex = "1.11"
encoding_rs = "0.8"
chrono = "0.4"
base64 = "0.22"
image = "0.25"
log = "0.4"
rust-embed="8.3"
once_cell = "1.19"
rust-embed="8.6"
once_cell = "1.20"
escaper = "0.1"
futures = "0.3"
unic-emoji-char = "0.9"
[dev-dependencies]
env_logger = "0.11"

@ -1 +1 @@
Subproject commit 737398ef6b121db2d72042b5406a95dfd497113f
Subproject commit 69aa220193d99427d3822fabccdfaeede56cd532

View file

@ -1 +1 @@
<article><iframe id="video" width="100%" height="400" src="https://www.youtube.com/embed/8KjaIumu-jI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="RIGGED! Arena Shuffler is BROKEN | 13 Land Mono Red Burn"><empty></empty></iframe></article>
<article><iframe id="video" width="480" height="360" aspect-ratio="auto" src="https://www.youtube.com/embed/8KjaIumu-jI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="RIGGED! Arena Shuffler is BROKEN | 13 Land Mono Red Burn"><empty></empty></iframe></article>

View file

@ -8,13 +8,13 @@
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>At root</p>
<iframe width="480" height="360" src="https://www.youtube.com/embed/LtOGa5M8AuU" frameborder="0" allowfullscreen="" aspect-ratio="auto"><empty></empty></iframe>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/LtOGa5M8AuU" frameborder="0" allowfullscreen=""><empty></empty></iframe>
<iframe src="https://player.vimeo.com/video/32246206?color=ffffff+title=0+byline=0+portrait=0" width="500" height="281" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""><empty></empty></iframe>
<p>In a paragraph</p>
<p><iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/LtOGa5M8AuU" frameborder="0" allowfullscreen=""><empty></empty></iframe></p>
<p>In a div</p>
<div><iframe width="480" height="360" src="https://www.youtube.com/embed/LtOGa5M8AuU" frameborder="0" allowfullscreen="" aspect-ratio="auto"><empty></empty></iframe></div>
<h2>Foo</h2>
<p>
Tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,

View file

@ -250,7 +250,9 @@
capable HDR 10 standard. That makes sense since it's
more widely supported, but it would have been nice
to see Dolby's, too.</p>
<p>
<iframe allowfullscreen="" frameborder="0" gesture="media" height="360" src="https://www.youtube.com/embed/c8aFcHFu8QM" width="480" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>And speaking of Dolby technology, Microsoft is also
highlighting Atmos support on the One X, just like
it did with the One S. The company's app lets you

View file

@ -6,7 +6,7 @@
<td>
<img src="http://fakehost/366/logo_bana/corner_1.gif" width="7" height="7"/>
</td>
<td/>
<td><empty></empty></td>
<td>
<img src="http://fakehost/366/logo_bana/corner_2.gif" width="7" height="7"/>
</td>
@ -80,7 +80,7 @@
</tr>
<tr>
<td>
<audio src="http://ohanashi2.up.seesaa.net/mp3/ae_0101.mp3" controls=""/>
<audio src="http://ohanashi2.up.seesaa.net/mp3/ae_0101.mp3" controls=""><empty></empty></audio>
</td>
</tr>
<tr>
@ -109,7 +109,7 @@
<td>
<img src="http://fakehost/366/logo_bana/corner_1.gif" width="7" height="7"/>
</td>
<td/>
<td><empty></empty></td>
<td>
<img src="http://fakehost/366/logo_bana/corner_2.gif" width="7" height="7"/>
</td>

View file

@ -1,5 +1,7 @@
<article><div id="readability-page-1" itemprop="articleBody">
<P>
<iframe src="http://www.dailymotion.com/embed/video/x2p552m?syndication=131181" frameborder="0" width="534" height="320"><empty></empty></iframe>
</P>
<p>Les députés ont, sans surprise, adopté à une large majorité (438 contre 86 et 42 abstentions) le projet de loi sur le renseignement défendu par le gouvernement lors dun vote solennel, mardi 5 mai. Il sera désormais examiné par le Sénat, puis le Conseil constitutionnel, prochainement saisi par 75 députés. Dans un souci d'apaisement, François Hollande avait annoncé par avance qu'il saisirait les Sages.</p>
<p><strong>Revivez <a href="http://fakehost/pixels/live/2015/05/05/suivez-le-vote-de-la-loi-renseignement-en-direct_4628012_4408996.html" target="_blank">le direct du vote à lAssemblée avec vos questions.</a></strong></p>
<p>Ont voté contre : 10 députés socialistes (sur 288), 35 UMP (sur 198), 11 écologistes (sur 18), 11 UDI (sur 30), 12 députés Front de gauche (sur 15) et 7 non-inscrits (sur 9). <a href="http://www2.assemblee-nationale.fr/scrutins/detail/%28legislature%29/14/%28num%29/1109" target="_blank">Le détail est disponible sur le site de l'Assemblée nationale.</a></p>

View file

@ -8,7 +8,9 @@
<p>Lappareil, mis à disposition par Airbus, était arrivé à Katmandou mercredi matin avec 55 personnels de santé et humanitaires, ainsi que 25 tonnes de matériel (abris, médicaments, aide alimentaire). Un deuxième avion dépêché par Paris, qui était immobilisé aux Emirats depuis mardi avec 20 tonnes de matériel, est arrivé jeudi à Katmandou, <a href="http://www.liberation.fr/monde/2015/04/29/embouteillages-et-retards-a-l-aeroport-de-katmandou_1276612" target="_blank">dont le petit aéroport est engorgé</a> par le trafic et lafflux daide humanitaire. Il devait lui aussi ramener des Français, <em>«les plus éprouvés»</em> par la catastrophe et les <em>«plus vulnérables (blessés, familles avec enfants)»</em>, selon le ministère des Affaires étrangères.</p>
<p>2 209 Français ont été localisés sains et saufs tandis que 393 nont pas encore pu être joints, selon le Quai dOrsay. Environ 400 Français ont demandé à être rapatriés dans les vols mis en place par la France.</p>
<p>Le séisme a fait près de 5 500 morts et touche huit des 28 millions dhabitants du Népal. Des dizaines de milliers de personnes sont sans abri.</p>
<p>
<iframe src="http://www.dailymotion.com/embed/video/x2oikl3" frameborder="0" width="100%" data-aspect-ratio="0.5625" data-responsive="1"><empty></empty></iframe>
<br/></p>
</div>
</article>
</section></article>

View file

@ -16,7 +16,7 @@
<p>The name and basic idea might sound like one of those endless score attack games like "Temple Run," but that's not the case. "Super Mario Run" is divided into hand-crafted levels with a clear end-point like any other Mario game, meaning you're essentially getting the Mario experience for $10 without needing to control his movement.</p>
<p>$10 might seem like a bit much compared to the $0 people pay for most mobile games, but it's possible the game has $10 worth of levels to play in it. It's also not iPhone exclusive, but the Android version will launch at a later, currently unknown date. </p>
<p>To see "Super Mario Run" in action, check out the footage below:</p>
<div><iframe allowfullscreen="" src="https://www.youtube.com/embed/E39ychZKnDI" frameborder="0" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
</section>
</article></DIV></article>

View file

@ -28,7 +28,7 @@
<div id="rv-player">
<p><span>转播到腾讯微博</span></p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAJAQMAAAAB5D5xAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAADUExURQAAAKd6PdoAAAABdFJOUwBA5thmAAAAC0lEQVQI12NgwAkAABsAAVJE5KkAAAAASUVORK5CYIIvKiAgfHhHdjAwfDUwZDc5YmEzMGM3MDcxY2I5OTIyMTk4MzYyZGRlZmNlICov"/></p></div>
<iframe src="http://v.qq.com/video/playview.html?vid=x033633yrm8" height="0" width="0"><empty></empty></iframe>

View file

@ -11,7 +11,9 @@
<h3 id="jDDW9T">
21) <a href="https://www.vox.com/culture/2017/12/12/16765308/last-jedi-star-wars-review-rey-carrie-fisher-poe-finn-kylo-ren" target="_blank"><em>Star Wars: The Last Jedi</em></a>
</h3>
<div id="x5htN5">
<iframe src="https://www.youtube.com/embed/Q0CbN8sfihY?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="WdtoaT">
I am as shocked as anyone that a <em>Star Wars</em> movie found its way onto my list — but I was bowled over by <em>The Last Jedi</em>, which may be one of the series best. In the hands of writer-director <a href="https://www.vox.com/culture/2017/12/13/16761916/rian-johnson-star-wars-last-jedi-looper-brick-brothers-bloom-fly-breaking-bad" target="_blank">Rian Johnson</a> (who will also oversee <a href="https://www.theverge.com/2017/11/9/16630902/star-wars-new-trilogy-rian-johnson-disney-lucasfilm" target="_blank">a new <em>Star Wars</em> trilogy</a>), <em>The Last Jedi</em> is beautiful to look at and keeps its eye on the relationships between characters and how they communicate with one another, in addition to the bigger galactic story. The same characters are back, but they seem infused with new life, and the galaxy with a new kind of hope. The movies best details are in the strong bonds that develop between characters, and I left the film with the realization that for the first time in my life, I loved a <em>Star Wars</em> movie. Now I understand the magic.
</p>
@ -21,7 +23,9 @@
<h3 id="3XHosO">
20) <a href="https://www.vox.com/2017/10/6/16434046/faces-places-review-agnes-varda-jr" target="_blank"><em>Faces Places</em></a>
</h3>
<div id="FZOPyv">
<iframe src="https://www.youtube.com/embed/KKbjnLpxv70?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="zP5jCd">
The unusual documentary <a href="https://www.vox.com/2017/10/6/16434046/faces-places-review-agnes-varda-jr" target="_blank"><em>Faces Places</em></a> (in French, <em>Visages Villages</em>) turns on the friendship between the accomplished street artist JR and legendary film director Agnès Varda, whose work was central to the development of the French New Wave movement. The pair (whose difference in age is 55 years) met after years of admiring each others work and decided to create a documentary portrait of France — by making a number of actual portraits. The film chronicles a leg of the "Inside Outside Project," a roving art initiative in which JR makes enormous portraits of people he meets and pastes them onto buildings and walls. In the film, Varda joins him, and as they talk to people around the country, they grow in their understanding of themselves and of each other. The development of their friendship, which is both affectionate and mutually sharpening, forms <em>Faces Places</em> emotional center.
</p>
@ -32,7 +36,9 @@
<h3 id="R0KXNO">
19) <a href="https://www.vox.com/summer-movies/2017/8/8/16107088/ingrid-goes-west-review-aubrey-plaza-elizabeth-olsen" target="_blank"><em>Ingrid Goes West</em></a>
</h3>
<div id="94aRXv">
<iframe src="https://www.youtube.com/embed/xP4vD1tWbPU?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="d2ZAUw">
<a href="https://www.vox.com/summer-movies/2017/8/8/16107088/ingrid-goes-west-review-aubrey-plaza-elizabeth-olsen" target="_blank"><em>Ingrid Goes West</em></a> is a twisted and <a href="https://www.vox.com/culture/2017/8/9/16107140/matt-spicer-interview-ingrid-goes-west-dark-comedy-aubrey-plaza-sundance?utm_campaign=vox&amp;utm_content=chorus&amp;utm_medium=social&amp;utm_source=twitter" target="_blank">dark comedy</a> — part addiction narrative, part stalker story — and yet its set in a world thats almost pathologically cheery: the glossy, sunny, nourishing, superfood- and superlative-loving universe of Instagram celebrity. But despite <em>Ingrid Goes West</em>s spot-on take on that world, the best thing about the film is that it refuses to traffic in lazy buzzwords and easy skewering, particularly at the expense of young women. Instead, the movie conveys that behind every Instagram image and meltdown is a real person, with real insecurities, real feelings, and real problems. And it recognizes that living a life performed in public can be its own kind of self-deluding prison.
</p>
@ -42,7 +48,9 @@
<h3 id="qfZ4Iv">
18) <a href="https://www.vox.com/summer-movies/2017/7/14/15955888/review-lady-macbeth-florence-pugh" target="_blank"><em>Lady Macbeth</em></a>
</h3>
<div id="0ZWzkX">
<iframe src="https://www.youtube.com/embed/2Z0N8ULhuUA?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="Ii1QS5">
<a href="https://www.vox.com/summer-movies/2017/7/14/15955888/review-lady-macbeth-florence-pugh" target="_blank"><em>Lady Macbeth</em></a> is no placid costume drama. Adapted from an 1865 Russian novella by Nikolai Leskov, the movie follows Katherine (the astounding Florence Pugh), a woman in the Lady Macbeth line characterized by a potent cocktail of very few scruples and a lot of determination. She's a chilling avatar for the ways that class and privilege — both obvious and hidden — insulate some people from the consequences of their actions while damning others. <em>Lady Macbeth</em> is also a dazzling directorial debut from William Oldroyd, a thrilling combination of sex, murder, intrigue, and power plays. Its visually stunning, each frame composed so carefully and deliberately that the wildness and danger roiling just below the surface feels even more frightening. Each scene ratchets up the tension to an explosive, chilling end.
</p>
@ -52,7 +60,9 @@
<h3 id="JhEBod">
17) <em>BPM (Beats Per Minute)</em>
</h3>
<div id="t3derk">
<iframe src="https://www.youtube.com/embed/2fhO2A4SL24?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="DEyp0A">
<em>BPM (Beats Per Minute)</em> is a remarkably tender and stirring story of the Paris chapter of ACT UP, an AIDS activism group, and the young people who found themselves caught in the crosshairs of the AIDS crisis in the early 1990s. The film follows both the group's actions and the individual members shifting relationships to one another — enemies becoming friends, friends becoming lovers, lovers becoming caretakers — as well as their struggles with the disease wracking their community. As an account of the period, its riveting; as an exploration of life and love set at the urgent intersection of the political and the personal, its devastating.
</p>
@ -62,7 +72,9 @@
<h3 id="jocryI">
16) <a href="https://www.vox.com/summer-movies/2017/6/21/15837678/big-sick-review-kumail-nanjiani-emily-gordon-zoe-kazan-islam" target="_blank"><em>The Big Sick</em></a>
</h3>
<div id="ZRFycn">
<iframe src="https://www.youtube.com/embed/PJmpSMRQhhs?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="DqZc5Q">
Few 2017 movies could top the charm and tenderness of <a href="https://www.vox.com/summer-movies/2017/6/21/15837678/big-sick-review-kumail-nanjiani-emily-gordon-zoe-kazan-islam" target="_blank"><em>The Big Sick</em></a>, which hits all the right romantic comedy notes with one unusual distinction: It feels like real life. Thats probably because <em>The Big Sick</em> is written by <a href="https://www.vox.com/2017/11/22/16687092/the-big-sick-kumail-nanjiani-emily-gordon-real-story" target="_blank">real-life married couple</a> Emily V. Gordon and <em>Silicon Valley</em>'s Kumail Nanjiani, and based on their real-life romance. <em>The Big Sick</em> — which stars Nanjiani as a version of himself, alongside Zoe Kazan as Emily — is funny and sweet while not backing away from matters that romantic comedies dont usually touch on, like serious illness, struggles in long-term marriages, and religion. As it tells the couples story, which takes a serious turn when Emily falls ill with a mysterious infection and her parents (played by Holly Hunter and Ray Romano) come to town, it becomes a funny and wise story about real love.
</p>
@ -72,7 +84,9 @@
<h3 id="dFyVjw">
15) <a href="https://www.vox.com/culture/2017/9/10/16277234/mother-review-aronofsky-lawrence-bardem-tiff" target="_blank"><em>Mother!</em></a>
</h3>
<div id="kUMpyj">
<iframe src="https://www.youtube.com/embed/XpICoc65uh0?rel=0&amp;amp;start=17" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="LA1s4n">
Theres so much pulsing beneath <a href="https://www.vox.com/culture/2017/9/10/16277234/mother-review-aronofsky-lawrence-bardem-tiff" target="_blank">the surface of <em>Mother!</em></a> that its hard to grab on to just one theme as what it “means.” Its full-on apocalyptic fiction, and like all stories of apocalypse, its intended to draw back the veil on reality and show us whats really beneath. And this movie gets wild: If its gleeful cracking apart of traditional theologies doesnt get you (theres a lot of Catholic folk imagery here, complete with an Ash Wednesday-like mud smearing on the foreheads of the faithful), its bonkers scenes of chaos probably will. <em>Mother!</em> is a movie designed to provoke fury, ecstasy, madness, catharsis, and more than a little awe. Watching it, and then participating in the flurry of arguments and discussions unpacking it, was among my best moviegoing experiences of 2017.
</p>
@ -82,7 +96,9 @@
<h3 id="PL5PTS">
14) <a href="https://www.vox.com/culture/2017/7/7/15925272/ghost-story-review-rooney-mara-casey-affleck" target="_blank"><em>A Ghost Story</em></a>
</h3>
<div id="76I1cH">
<iframe src="https://www.youtube.com/embed/0Vb0F_CN83E?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="JWA6Pb">
Director <a href="https://www.vox.com/summer-movies/2017/7/13/15960236/david-lowery-ghost-story-interview" target="_blank">David Lowery</a> filmed <a href="https://www.vox.com/culture/2017/7/7/15925272/ghost-story-review-rooney-mara-casey-affleck" target="_blank"><em>A Ghost Story</em></a> in secret, then premiered it at the Sundance Film Festival to critical acclaim. The movie starts out being about a grieving widow (Rooney Mara) trying to live through the pain of losing her beloved husband, but it soon shifts focus to the ghost of her husband (Casey Affleck, covered in a sheet), evolving into a compelling rumination on the nature of time, memory, history, and the universe. Bathed in warm humor and wistful longing, it's a film that stays with you long after its over, a lingering reminder of the inextricable link between love and place.
</p>
@ -92,7 +108,9 @@
<h3 id="rRIM9r">
13) <a href="https://www.vox.com/2017/10/24/16523642/square-review-ruben-ostlund-claes-bang-elisabeth-moss" target="_blank"><em>The Square</em></a>
</h3>
<div id="z1g0Cs">
<iframe src="https://www.youtube.com/embed/EUzRjRv0Ib0?rel=0" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="NavzzU">
Winner of the Palme dOr at the 2017 Cannes Film Festival, <a href="https://www.vox.com/2017/10/24/16523642/square-review-ruben-ostlund-claes-bang-elisabeth-moss" target="_blank"><em>The Square</em></a> is a hilariously needling comedy about the contemporary art world, as well as the kind of idealistic liberalism that is tough to maintain in the face of real problems. The outstanding Claes Bang stars as Christian, a curator whose cluelessness leads him into some outlandishly rough spots, with Elisabeth Moss in a too-short but brilliant part as an American journalist who wont let him get away with his shenanigans. Its a heady film with a lot of ideas ricocheting around — and a <em>lot</em> of uncomfortable satire — but if you (like me) are the sort of viewer who loves that stuff, its sly jabs at the veneer of civilization that keeps the social contract intact are intoxicating.
</p>
@ -102,7 +120,9 @@
<h3 id="Px2hT6">
12) <a href="https://www.vox.com/culture/2017/7/17/15984026/dunkirk-review-nolan-rylance-hardy-styles-spoilers" target="_blank"><em>Dunkirk</em></a>
</h3>
<div id="TDSYe7">
<iframe src="https://www.youtube.com/embed/F-eMt3SrfFU?rel=0&amp;amp;start=24" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="MLatLf">
<a href="https://www.vox.com/culture/2017/7/17/15984026/dunkirk-review-nolan-rylance-hardy-styles-spoilers" target="_blank"><em>Dunkirk</em></a>, a true cinematic achievement from acclaimed director Christopher Nolan, backs off conventional notions of narrative and chronology as much as possible, while leaning headfirst into everything else that makes a movie a visceral work of art aimed at the senses: the images, the sounds, the scale, the swelling vibrations of it all. You cant smell the sea spray, but your brain may trick you into thinking you can. Nolans camera pushes the edges of the screen as far as it can as <em>Dunkirk</em> engulfs the audience in something that feels like a lot more than a war movie. Its a symphony for the brave and broken, and it resolves in a major key — but one with an undercurrent of sorrow, and of sober warning. Courage in the face of danger is not just for characters in movies.
</p>
@ -112,7 +132,9 @@
<h3 id="CPlXz5">
11) <em>Rat Film</em>
</h3>
<div id="s6q4gj">
<iframe src="https://www.youtube.com/embed/f-kpMAKc0l4?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="GFFO6D">
<em>Rat Film</em> is about rats, yes — and rat poison experts and rat hunters and people who keep rats as pets. But its also about the history of eugenics, dubious science, <a href="https://en.wikipedia.org/wiki/Redlining" target="_blank">“redlining,”</a> and segregated housing in Baltimore. All these pieces come together to form one big essay, where the meaning of each vignette only becomes clearer in light of the whole. Its a fast-paced, no-holds-barred exploration of a damning history, and it accrues meaning as the images, sounds, and text pile up.
</p>
@ -122,7 +144,9 @@
<h3 id="Qgio0l">
10) <a href="https://www.vox.com/culture/2017/4/13/15243556/quiet-passion-review-emily-dickinson-passover-easter" target="_blank"><em>A Quiet Passion</em></a>
</h3>
<div id="Ya6IEK">
<iframe src="https://www.youtube.com/embed/T3SyPbUTEeU?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="EO0XbC">
<a href="https://www.vox.com/culture/2017/4/13/15243556/quiet-passion-review-emily-dickinson-passover-easter" target="_blank"><em>A Quiet Passion</em></a> is technically a biographical film about Emily Dickinson, but it transcends its genre to become something more like poetry. Its a perplexing and challenging film, crafted without the traditional guardrails that guide most biographical movies — dates, times, major accomplishments, and so on. Time slips away in the film almost imperceptibly, and the narrative arc doesnt yield easily to the viewer. Cynthia Nixon plays Emily Dickinson, whose poetry and life is a perfect match for the signature style of director Terence Davies: rich in detail, deeply enigmatic, and weighed down with a kind of sparkling, joy-tinged sorrow. <em>A Quiet Passion</em> is a portrait, both visual and narrative, of the kind of saint most modern people can understand: one who is certain of her uncertainty, and yearning to walk the path on which her passion and longing meet.
</p>
@ -132,7 +156,9 @@
<h3 id="7dz2o3">
9) <em>Columbus</em>
</h3>
<div id="ZfQfEI">
<iframe src="https://www.youtube.com/embed/r3dcnV6Z9Zs?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="pM0BdD">
<em>Columbus</em> is a stunner of a debut from video essayist turned director Kogonada. Haley Lu Richardson stars as Casey, a young woman living in Columbus, Indiana, who cares for her mother, works at a library, and harbors a passion for architecture. (Columbus is a mecca for modernist architecture scholars and enthusiasts.) When a visiting architecture scholar falls into a coma in Columbus, his estranged son Jin (John Cho) arrives to wait for him and strikes up a friendship with Casey, who starts to show him her favorite buildings. The two begin to unlock something in each other thats hard to define but life-changing for both. <em>Columbus</em> is beautiful and subtle, letting us feel how the places we build and the people we let near us move and mold us.
</p>
@ -142,7 +168,9 @@
<h3 id="wkyPUl">
8) <a href="https://www.vox.com/culture/2017/5/31/15706424/florida-project-review-cannes-sean-baker" target="_blank"><em>The Florida Project</em></a>
</h3>
<div id="RLHf4Z">
<iframe src="https://www.youtube.com/embed/WwQ-NH1rRT4?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="J6kOkz">
Sean Bakers <a href="https://www.vox.com/culture/2017/5/31/15706424/florida-project-review-cannes-sean-baker" target="_blank"><em>The Florida Project</em></a> unfolds at first like a series of sketches about the characters who live in a purple-painted, $35-a-night motel called the Magic Castle down the street from Disney World. The film is held together by the hysterical antics of a kid named Moonee and her pack of young friends, as well as long-suffering hotel manager Bobby (a splendid, warm Willem Dafoe), who tries to put up with it all while keeping some kind of order. But as <em>The Florida Project</em> goes on, a narrative starts to form, one that chronicles with heartbreaking attention the sort of dilemmas that face poor parents and their children in America, and the broken systems that try to cope with impossible situations.
</p>
@ -152,7 +180,9 @@
<h3 id="rLGNAf">
7) <a href="https://www.vox.com/2017/11/21/16552862/call-me-by-your-name-review-timothee-chalamet-armie-hammer" target="_blank"><em>Call Me</em> <em>b</em><em>y Your Name</em></a>
</h3>
<div id="xGksjG">
<iframe src="https://www.youtube.com/embed/Z9AYPxH5NTM?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="KyeOGQ">
Luca Guadagninos gorgeous film <a href="https://www.vox.com/2017/11/21/16552862/call-me-by-your-name-review-timothee-chalamet-armie-hammer" target="_blank"><em>Call Me</em> <em>b</em><em>y Your Name</em></a> adapts André Acimans <a href="https://go.redirectingat.com/?id=66960X1516588&amp;xs=1&amp;url=https%3A%2F%2Fwww.amazon.com%2FCall-Me-Your-Name-Novel%2Fdp%2F031242678X" rel="nofollow noopener" target="_blank">2007 novel</a> about a precocious 17-year-old named Elio (Timothée Chalamet), who falls in lust and love with his fathers 24-year-old graduate student Oliver (Armie Hammer). Its remarkable for how it turns literature into pure cinema, all emotion and image and heady sensation. Set in 1983 in Northern Italy, <em>Call Me</em> <em>b</em><em>y Your Name</em> is less about coming out than coming of age, but it also captures a particular sort of love thats equal parts passion and torment, a kind of irrational heart fire that opens a gate into something longer-lasting. The film is a lush, heady experience for the body, but its also an arousal for the soul.
</p>
@ -162,7 +192,9 @@
<h3 id="h6Biwc">
6) <em>Personal Shopper</em>
</h3>
<div id="NSQg2p">
<iframe src="https://www.youtube.com/embed/xC8AjoqpBAY?rel=0&amp;amp;start=15" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="pofJH9">
In her second collaboration with French director <a href="http://www.imdb.com/name/nm0000801/?ref_=fn_al_nm_1" target="_blank">Olivier Assayas</a>, Kristen Stewart plays a personal shopper to a wealthy socialite, with a sideline as an amateur ghost hunter whos searching for her dead twin brother. <em>Personal Shopper</em> is deeper than it seems at first blush, a meditation on grief and an exploration of “between” places — on the fringes of wealth, and in the space between life and death. Some souls are linked in a way that cant be shaken, and whether or not theres an afterlife doesnt change the fact that we see and sense them everywhere. (<em>Personal Shopper</em> also has one of the most tense extended scenes involving text messaging ever seen onscreen.)
</p>
@ -172,7 +204,9 @@
<h3 id="0RkMKy">
5) <em>Princess Cyd</em>
</h3>
<div id="7Tj1H6">
<iframe src="https://www.youtube.com/embed/sr64EJfnJwE?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="2tSIHW">
Stephen Cone is a master of small, carefully realized filmmaking; his earlier films such as <em>The Wise Kids</em> and <em>Henry Gambles Birthday Party</em> combine an unusual level of empathy for his characters with an unusual combination of interests: love, desire, sexual awakenings, and religion. <em>Princess Cyd</em> is his most accomplished film yet, about a young woman named Cyd (<a href="http://www.imdb.com/name/nm6570557/?ref_=tt_cl_t2" target="_blank">Jessie Pinnick</a>) who finds herself attracted to Katie (<a href="http://www.imdb.com/name/nm5154548/?ref_=tt_cl_t3" target="_blank">Malic White</a>), a barista, while visiting her Aunt Miranda (<a href="http://www.imdb.com/name/nm2050642/?ref_=tt_cl_t1" target="_blank">Rebecca Spence</a>, playing a character modeled on the author Marilynne Robinson) in Chicago. As she works through her own sexual awakening with Katie, Cyd unwinds some of the ways Mirandas life has gotten too safe. They provoke each other while forming a bond and being prodded toward a bigger understanding of the world. It is a graceful and honest film, and it feels like a modest miracle.
</p>
@ -182,7 +216,9 @@
<h3 id="ADtiAV">
4) <a href="https://www.vox.com/culture/2017/2/24/14698632/get-out-review-jordan-peele" target="_blank"><em>Get Out</em></a>
</h3>
<div id="swjmhh">
<iframe src="https://www.youtube.com/embed/sRfnevzM9kQ?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="h1ighb">
Racism is sinister, frightening, and deadly. But <a href="https://www.vox.com/culture/2017/2/24/14698632/get-out-review-jordan-peele" target="_blank"><em>Get Out</em></a> (a stunning directorial debut from <em>Key &amp; Peele</em>'s Jordan Peele) isnt about the blatantly, obviously scary kind of racism — burning crosses and lynchings and snarling hate. Instead, its interested in showing how the parts of racism that try to be aggressively unscary are just as horrifying, and its interested in making us feel that horror in a visceral, bodily way. In the tradition of the best classic social thrillers, <em>Get Out</em> takes a topic that is often approached cerebrally — casual racism — and turns it into something you feel in your tummy. And it does it with a wicked sense of humor.
</p>
@ -192,7 +228,9 @@
<h3 id="TQbjNr">
3) <em>The Work</em>
</h3>
<div id="GYqgVe">
<iframe src="https://www.youtube.com/embed/h8OVXG2GhpQ?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="3Uotb3">
<em>The Work</em> is an outstanding, astonishing accomplishment and a viewing experience that will leave you shaken (but in a good way). At Folsom Prison in California, incarcerated men regularly participate in group therapy, and each year other men from the “outside” apply to participate in an intense four-day period of group therapy alongside Folsoms inmates. <em>The Work</em> spends almost all of its time inside the room where that therapy happens, observing the strong, visceral, and sometimes violent emotions the men feel as they expose the hurt and raw nerves that have shaped how they encounter the world. Watching is not always easy, but by letting us peek in, the film invites viewers to become part of the experience — as if we, too, are being asked to let go.
</p>
@ -202,7 +240,9 @@
<h3 id="kUrRP6">
2) <em>Ex Libris</em>
</h3>
<div id="Lb1IzW">
<iframe src="https://www.youtube.com/embed/YzKrlOFZBD8?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="rGpjUU">
Frederick Wiseman is one of the towering giants of nonfiction film, a keen observer of American institutions — ranging from prisons to dance companies to welfare offices — for the past half-century. <em>Ex Libris</em> is his mesmerizing look at the New York Public Library and the many functions it fills, which go far beyond housing books. Wiseman works in the observational mode, which means his films contain no captions, dates, or talking-head interviews: We just see what his camera captured, which in this case includes community meetings, benefit dinners, after-school programs, readings with authors and scholars (including Richard Dawkins and Ta-Nehisi Coates), and NYPL patrons going about their business in the librarys branches all over the city. The result is almost hypnotic and, perhaps surprisingly, deeply moving. It makes a case for having faith in the public institutions where ordinary people work — away from the limelight, without trying to score political points — in order to make our communities truly better.
</p>
@ -212,7 +252,9 @@
<h3 id="QJNuyl">
1) <a href="https://www.vox.com/2017/11/2/16552860/lady-bird-review-saoirse-ronan-greta-gerwig" target="_blank"><em>Lady Bird</em></a>
</h3>
<div id="mgxqrA">
<iframe src="https://www.youtube.com/embed/cNi_HC839Wo?rel=0&amp;" allowfullscreen="allowfullscreen" scrolling="no" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe>
</div>
<p id="z8uM1l">
<em>Lady Bird</em> topped my list almost instantly, and only rose in my estimation on repeated viewings. For many who saw it (including me), it felt like a movie made not just for but <em>about</em> me. <em>Lady Bird</em> is a masterful, exquisite coming-of-age comedy starring the great Saoirse Ronan as Christine — or “Lady Bird,” as shes re-christened herself — and its as funny, smart, and filled with yearning as its heroine. Writer-director Greta Gerwig made the film as an act of love, not just toward her hometown of Sacramento but also toward girlhood, and toward the feeling of always being on the outside of wherever real life is happening. <em>Lady Bird</em> is the rare movie that manages to be affectionate, entertaining, hilarious, witty, and confident. And one line from it struck me as the guiding principle of many of the years best films: “Dont you think they are the same thing? Love, and attention?”
</p>

View file

@ -8,7 +8,9 @@
<p>
<strong><em>Vape Wave</em> (documentaire, 1h28, Planète+)</strong>
</p>
<p>
<iframe width="480" src="https://www.youtube.com/embed/lGL7RgHn5f0" frameborder="0" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1" height="360" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>
Pendant quelques jours, le doute a plané : lEtat comptait-il vraiment légiférer contre la cigarette dans les films français, que ce soit via une interdiction pure et simple ou via un système de «punition» (coupe des aides CNC, par exemple) pour les longs-métrages qui sentent le mégot ? Si <a href="https://www.liberation.fr/direct/element/agnes-buzyn-assure-quelle-na-jamais-envisage-linterdiction-de-la-cigarette-au-cinema_73855/" target="_blank">le rétropédalage de la ministre Buzyn</a> nen est pas vraiment un (elle navait jamais clairement menacé le septième art), la polémique a le mérite de pointer la (sur)représentation clopesque sur écran. Et si, comme cest le cas dans la vie quotidienne, on voyait progressivement les cigarettes électroniques remplacer les tiges nicotinées authentiques ? Que ceux qui mettraient en doute le potentiel cinématographique des vapoteuses se ruent sur <a href="http://www.vapewave.net/" target="_blank"><em>Vape Wave</em></a>, documentaire militant signé Jan Kounen, ex-fumeur reconverti à la vape dont les images magnifient les volutes de vapeur recrachée.
</p>
@ -21,7 +23,9 @@
<p>
<strong><em>Dans la tête dAlan Moore</em> (websérie documentaire, 8x5min, Arte Creative)</strong>
</p>
<p>
<iframe width="480" src="https://www.youtube.com/embed/s_rw5fPHz2g" frameborder="0" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1" height="360" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>
Le week-end dernier, <em>Libération</em> publiait <a href="http://next.liberation.fr/livres/2017/11/17/alan-moore-dernier-barde-avant-la-fin-du-monde_1610854" target="_blank">un portrait de der consacré à lauteur britannique Alan Moore</a>, connu pour ses BD cultes (<em>V pour Vendetta, Watchmen, From Hell</em>), à loccasion de la sortie de son deuxième roman, le pavé <em>Jérusalem</em>. En attendant limminente sortie dune version longue de son entretien avec <em>Libé</em>, on pourra se replonger dans les épisodes dune websérie documentaire dArte Creative en 8 épisodes consacré au maître. Brexit, magie, Anonymous font partie des sujets discutés avec le maître au fil de ce programme sobrement intitulé <a href="https://www.arte.tv/fr/videos/RC-014342/dans-la-tete-d-alan-moore/" target="_blank"><em>Dans la tête dAlan Moore</em></a>. <strong>(A.H.)</strong>
</p>
@ -31,7 +35,9 @@
<p>
<strong><em>The Death and Life of Marsha P. Johnson</em> (docu, 1h45, Netflix)</strong>
</p>
<p>
<iframe width="480" src="https://www.youtube.com/embed/pADsuuPd79E" frameborder="0" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1" height="360" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>
Marsha, la <em>«Rosa Parks du mouvement LGBTQ»</em>. Marsha <em>«la prostituée, lactrice et la sainte, modèle dAndy Warhol»</em> ou encore Marsha lélaborée, la radicale, <em>«avec ses plumes et ce maquillage quelle ne mettait jamais bien»</em>. «Queen Marsha» a été retrouvée morte dans lHudson en juillet 1992, alors quon la voyait encore parader dans les rues de Greenwich Village quelques jours auparavant. Un choc glaçant. Là où son corps a été repêché puis ingratement déposé, les sans-abri ont constitué le lendemain un mémorial de bouteilles et de plantes qui délimitent les contours de labsente.
</p>
@ -44,7 +50,9 @@
<p>
<strong><em>Alphonse President</em> (série, 10x26, OCS Max)</strong>
</p>
<p>
<iframe width="100%" frameborder="0" src="https://www.dailymotion.com/embed/video/x67iqc9" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1"><empty></empty></iframe>
</p>
<p>
Un temps baptisée <em>French Touch</em>, la série <em>Alphonse Président</em> est le dernier né des programmes originaux made in OCS. On savait les budgets de la chaîne bien moins généreux que ceux de Canal+ (voire que ceux de France 3 Limousin), et cette série le prouve à nouveau régulièrement, notamment lors dune scène de conférence de presse alternant plans larges dune authentique conf' à lElysée période François Hollande et plans serrés dacteurs filmés dans un château des Pays de la Loire où a eu lieu le tournage. Le principal atout (et quel atout) de cette série écrite et réalisée par Nicolas Castro (<em>Des lendemains qui chantent</em>, 2014) réside dans son interprète principal, Michel Vuillermoz.
</p>
@ -57,7 +65,9 @@
<p>
<strong><em>Jim &amp; Andy</em> (documentaire, 1h33, Netflix) </strong>
</p>
<p>
<iframe width="480" src="https://www.youtube.com/embed/kB15UFO5ebA" frameborder="0" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1" height="360" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>
A la sortie de <em>Man on the Moon</em> (2000), le magnifique film de Milos Forman consacré à Andy Kaufman  comique et génie de la performance absurde mort en 1984 , le cinéaste et les acteurs insistaient dans chaque interview sur lin­croyable comportement de Jim Carrey pendant le tournage : il aurait été comme possédé par Kaufman, se prenant pour lui 24 heures sur 24. Certains affirmaient même ne jamais avoir eu limpression que lacteur était présent, tant son modèle avait littéralement pris sa place. Nous en avons aujourdhui la preuve en images car tout cela avait été filmé par Bob Zmuda et Lynne Margulies, lancien complice et la veuve de Kaufman.
</p>
@ -70,7 +80,9 @@
<p>
<strong><em>Braguino</em> (documentaire, 50min, Arte)</strong>
</p>
<p>
<iframe width="480" src="https://www.youtube.com/embed/OIS-P-0-cRk" frameborder="0" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1" height="360" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>
La querelle peut se trouver derrière toutes les portes, y compris celle de lexil. On a beau croire avoir tourné le dos à tout, à cette inclination humaine à nourrir sa propre haine, lallergie peut regermer fissa sur une peau qui frissonne à lapproche de ce voisin que lon ne comprend pas. Issu dune lignée de vieux-croyants orthodoxes russes, Sacha Braguine a pris sa famille sous le bras, loin de toute autre présence humaine en taïga sibérienne. Un autre groupe, les Kiline, a décidé den faire de même et de sinstaller de lautre côté de la rivière. Qui est arrivé en premier ? Qui menace lautre ? Lhistoire de limpossible communauté peut commencer.
</p>
@ -83,7 +95,9 @@
<p>
<strong><em>6 Days</em> (film, 1h34, Netflix)</strong>
</p>
<p>
<iframe width="480" src="https://www.youtube.com/embed/7HthiTi_IcI" frameborder="0" allowfullscreen="allowfullscreen" data-aspect-ratio="0.5625" data-responsive="1" height="360" aspect-ratio="auto"><empty></empty></iframe>
</p>
<p>
Fin avril 1980, lambassade dIran à Londres a été le théâtre dune prise dotages largement médiatisée : une trentaine de personnes ont ainsi été retenues pendant six jours par des soldats iraniens dissidents exigeant la libération de 91 prisonniers. Avec Margaret Thatcher au 10 Downing Street à lépoque, pas question pour lAngleterre davoir lair mou du genou sur la réponse à apporter à cette crise scrutée par les caméras du monde entier. Le SAS (Special Air Service) est sur le coup : lopération Nimrod se met en place pour prendre dassaut lambassade.
</p>

View file

@ -11,28 +11,28 @@
<p data-type="text" data-reactid="419">Virtual reality has officially reached the consoles. And its pretty good! <a href="http://finance.yahoo.com/news/review-playstation-vr-is-comfortable-and-affordable-but-lacks-must-have-games-165053851.html" target="_blank">Sonys PlayStation VR</a> is extremely comfortable and reasonably priced, and while its lacking killer apps, its loaded with lots of interesting ones.</p>
<p data-type="text" data-reactid="420">But which ones should you buy? Ive played just about every launch game, and while some are worth your time, others you might want to skip. To help you decide whats what, Ive put together this list of the eight PSVR games worth considering.</p>
<h3 data-type="text" data-reactid="421"><a href="https://www.playstation.com/en-us/games/rez-infinite-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Rez Infinite” ($30)</a></h3>
<div data-reactid="422"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/YlDxEOwj5j8" data-reactid="423" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="424">Beloved cult hit “Rez” gets the VR treatment to help launch the PSVR, and the results are terrific. It includes a fully remastered take on the original “Rez” you zoom through a Matrix-like computer system, shooting down enemies to the steady beat of thumping electronica but the VR setting makes it incredibly immersive. It gets better the more you play it, too; unlock the amazing Area X mode and youll find yourself flying, shooting and bobbing your head to some of the trippiest visuals yet seen in VR.</p>
<h3 data-type="text" data-reactid="425"><a href="https://www.playstation.com/en-us/games/thumper-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Thumper” ($20)</a></h3>
<div data-reactid="426"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/gtPGX8i1Eaw" data-reactid="427" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="428">What would happen if Tron, the board game Simon, a Clown beetle, Cthulhu and a noise band met in VR? Chaos, for sure, and also “Thumper.” Called a “violent rhythm game” by its creators, “Thumper” is, well, a violent rhythm game thats also a gorgeous, unsettling and totally captivating assault on the senses. With simple controls and a straightforward premise click the X button and the analog stick in time with the music as you barrel down a neon highway — its one of the rare games that works equally well both in and out of VR. But since you have PSVR, play it there. Its marvelous.</p>
<h3 data-type="text" data-reactid="429"><a href="https://www.playstation.com/en-us/games/until-dawn-rush-of-blood-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Until Dawn: Rush of Blood” ($20)</a></h3>
<div data-reactid="430"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/EL3svUfC8Ds" data-reactid="431" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="432">Cheeky horror game “Until Dawn” was a breakout hit for the PS4 last year, channeling the classic “dumb teens in the woods” horror trope into an effective interactive drama. Well, forget all that if you fire up “Rush of Blood,” because this one sticks you front and center on a rollercoaster ride from Hell. Literally. You ride through a dimly-lit carnival of terror, dual-wielding pistols as you take down targets, hideous pig monsters and, naturally, maniac clowns. Be warned: If the bad guys dont get you, the jump scares will.</p>
<h3 data-type="text" data-reactid="433"><a href="https://www.playstation.com/en-us/games/headmaster-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Headmaster” ($20)</a></h3>
<div data-reactid="434"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/a7CSMKw1E7g" data-reactid="435" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="436">Soccer meets “Portal” in the weird (and weirdly fun) “Headmaster,” a game about heading soccer balls into nets, targets and a variety of other things while stuck in some diabolical training facility. While at first it seems a little basic, increasingly challenging shots and a consistently entertaining narrative keep it from running off the pitch. Funny, ridiculous and as easy as literally moving your head back and forth, its a pleasant PSVR surprise.</p>
<h3 data-type="text" data-reactid="437"><a href="https://www.playstation.com/en-us/games/rigs-mechanized-combat-league-ps4/" rel="nofollow noopener noreferrer" target="_blank">“RIGS: Mechanized Combat League” ($50)</a></h3>
<div data-reactid="438"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/Rnqlf9EQ2zA" data-reactid="439" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="440">Giant mechs + sports? Thats the gist of this robotic blast-a-thon, which pits two teams of three against one another in gorgeous, explosive and downright fun VR combat. At its best, “RIGS” marries the thrill of fast-paced competitive shooters with the insanity of piloting a giant mech in VR. It can, however, be one of the barfier PSVR games. So pack your Dramamine, youre going to have to ease yourself into this one.</p>
<h3 data-type="text" data-reactid="441"><a href="https://www.playstation.com/en-us/games/batman-arkham-vr-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Batman Arkham VR” ($20)</a></h3>
<div data-reactid="442"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/eS4g0py16N8" data-reactid="443" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="444">“Im Batman,” you will say. And youll actually be right this time, because you are Batman in this detective yarn, and you know this because you actually grab the famous cowl and mask, stick it on your head, and stare into the mirrored reflection of Rocksteady Games impressive Dark Knight character model. It lacks the action of its fellow “Arkham” games and runs disappointingly short, but its a high-quality experience that really shows off how powerfully immersive VR can be.</p>
<h3 data-type="text" data-reactid="445"><a href="https://www.playstation.com/en-us/games/job-simulator-the-2050-archives-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Job Simulator” ($30)</a></h3>
<div data-reactid="446"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/3-iMlQIGH8Y" data-reactid="447" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="448">There are a number of good VR ports in the PSVR launch lineup, but the HTC Vive launch game “Job Simulator” might be the best. Your task? Lots of tasks, actually, from cooking food to fixing cars to working in an office, all for robots, because did I mention you were in the future? Infinitely charming and surprisingly challenging, its a great showpiece for VR.</p>
<h3 data-type="text" data-reactid="449"><a href="https://www.playstation.com/en-us/games/eve-valkyrie-ps4/" rel="nofollow noopener noreferrer" target="_blank">“Eve Valkyrie” ($60)</a></h3>
<div data-reactid="450"><iframe data-type="videoIframe" src="https://www.youtube.com/embed/0KFHw12CTbo" data-reactid="451" width="480" height="360" aspect-ratio="auto"><empty></empty></iframe></div>
<p data-type="text" data-reactid="452">Already a hit on the Oculus Rift, this space dogfighting game was one of the first to really show off how VR can turn a traditional game experience into something special. Its pricey and not quite as hi-res as the Rift version, but “Eve Valkyrie” does an admirable job filling the void left since “Battlestar Galactica” ended. Too bad there arent any Cylons in it (or are there?)</p>
<p data-type="text" data-reactid="453"><em><strong>More games news:</strong></em></p>
<ul data-type="list" data-reactid="454">

View file

@ -0,0 +1,808 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/assets/css/dist-style.css" rel="stylesheet" />
<link href="/assets/css/fonts.css" rel="stylesheet" />
<script src="https://unpkg.com/typeit@8.8.4/dist/index.umd.js"></script>
<!-- Begin Jekyll SEO tag v2.8.0 -->
<title>PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only. | a-chacon</title>
<meta name="generator" content="Jekyll v4.3.4" />
<meta property="og:title" content="PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only." />
<meta name="author" content="Andrés" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="Haciendo funcionar el generador de autenticación de Rails 8 (Beta) en una aplicación creada en modo API-Only." />
<meta property="og:description" content="Haciendo funcionar el generador de autenticación de Rails 8 (Beta) en una aplicación creada en modo API-Only." />
<link rel="canonical" href="https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html" />
<meta property="og:url" content="https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html" />
<meta property="og:site_name" content="a-chacon" />
<meta property="og:image" content="https://a-chacon.com/assets/images/rails8-poc-api-auth.webp" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2024-10-16T00:00:00+00:00" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://a-chacon.com/assets/images/rails8-poc-api-auth.webp" />
<meta property="twitter:title" content="PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only." />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Andrés"},"dateModified":"2024-10-16T00:00:00+00:00","datePublished":"2024-10-16T00:00:00+00:00","description":"Haciendo funcionar el generador de autenticación de Rails 8 (Beta) en una aplicación creada en modo API-Only.","headline":"PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.","image":"https://a-chacon.com/assets/images/rails8-poc-api-auth.webp","mainEntityOfPage":{"@type":"WebPage","@id":"https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"},"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://a-chacon.com/assets/images/keyboard-key-a.png"},"name":"Andrés"},"url":"https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"}</script>
<!-- End Jekyll SEO tag -->
<link type="application/atom+xml" rel="alternate" href="https://a-chacon.com/feed.xml" title="a-chacon" />
<script data-host="https://app.microanalytics.io" data-dnt="false" src="https://app.microanalytics.io/js/script.js"
id="ZwSg9rf6GA" async defer></script>
<style>
.mini-banner {
background-color: #6eea8e;
/* Light red background color */
color: #1c2a18;
/* Dark red text color */
padding: 10px 20px;
/* Padding around the text */
text-align: center;
/* Center the text */
position: fixed;
/* Fix the banner to the top of the page */
bottom: 0;
/* Position at the very top */
left: 0;
/* Full width */
right: 0;
/* Full width */
z-index: 1000;
/* Ensure it's above other elements */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* Slight shadow for depth */
}
.mini-banner p {
margin: 0;
/* Remove default paragraph margin */
font-size: 16px;
/* Font size */
font-weight: bold;
/* Bold text */
}
body {
margin-top: 60px;
/* Adjust based on the banner height */
}
</style>
<div class="mini-banner">
<p>Building an API with Rails? Discover
<a href="https://github.com/a-chacon/oas_rails" style="color: #826cf6; text-decoration: underline;">
OasRails</a>, a Rails engine for generate automatic interactive documentation.
</p>
</div>
</head>
<body class="bg-white text-gray-900 dark:bg-gray-700 dark:text-slate-100">
<script>
document.addEventListener("DOMContentLoaded", function () {
new TypeIt("h1", {}).go();
});
</script>
<nav class="absolute top-0 left-0 right-0 flex w-full flex-wrap items-center justify-between py-4">
<div class="flex w-full flex-wrap items-center justify-between px-5">
<a class="flex" href="/" aria-label="Andres Chacon - Developer">
<img src="/assets/images/keyboard-key-a.png" alt="webpage logo" class="block h-8 w-auto">
<p class="pl-4 hidden md:block text-lg font-bold">a-chacon <span
class="text-mulish text-sm font-normal">~/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html</span>
</p>
</a>
<div class="flex divide-x">
<div><a class="px-4 hover:text-sky-400 "
href="/blog.html">Blog</a></div>
<div><a class="px-4 hover:text-sky-400 "
href="/projects">Proyectos</a></div>
<div class="language-switcher relative">
<button class="current-language px-4 hover:text-sky-400">
ES
</button>
<div class="dropdown-content absolute hidden mt-2 py-2 bg-white rounded shadow-lg z-20">
<a class="p-4 hover:text-sky-400" href="/en/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html">EN</a>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const currentLangButton = document.querySelector('.current-language');
const dropdownContent = document.querySelector('.dropdown-content');
if (currentLangButton && dropdownContent) {
currentLangButton.addEventListener('click', function () {
dropdownContent.classList.toggle('hidden');
});
document.addEventListener('click', function (event) {
if (!currentLangButton.contains(event.target) && !dropdownContent.contains(event.target)) {
dropdownContent.classList.add('hidden');
}
});
}
});
</script>
</nav>
<link rel="stylesheet" href="/assets/css/monokai.css">
<div class="container mx-auto">
<div id="post" class="prose max-w-[75ch] prose-neutral mx-auto pb-10 pt-20 lg:pt-[90px] lg:pb-20">
<div class="pb-4">
<a href="https://a-chacon.com/category/on-rails"
class="no-underline font-mulish text-sm hover:text-sky-400 text-gray-500">
ON RAILS
</a>
</div>
<h1 class="text-left text-2xl font-bold md:text-5xl text-black">PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.</h1>
<div class="flex flex-col md:flex-row text-sm font-mulish justify-between items-center">
<div class="flex">
<p>Andrés</p>
<p class="px-2"></p>
<p>16 October 2024</p>
<p class="px-2"></p>
<p>
6 mins </p>
</div>
<div>
<div class="sharing-buttons flex flex-wrap gap-1 justify-center ">
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://facebook.com/sharer/sharer.php?u=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"
aria-label="Share on Facebook">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Facebook</title>
<path
d="M379 22v75h-44c-36 0-42 17-42 41v54h84l-12 85h-72v217h-88V277h-72v-85h72v-62c0-72 45-112 109-112 31 0 58 3 65 4z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://twitter.com/intent/tweet?url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&text=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only."
aria-label="Share on Twitter">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Twitter</title>
<path
d="m459 152 1 13c0 139-106 299-299 299-59 0-115-17-161-47a217 217 0 0 0 156-44c-47-1-85-31-98-72l19 1c10 0 19-1 28-3-48-10-84-52-84-103v-2c14 8 30 13 47 14A105 105 0 0 1 36 67c51 64 129 106 216 110-2-8-2-16-2-24a105 105 0 0 1 181-72c24-4 47-13 67-25-8 24-25 45-46 58 21-3 41-8 60-17-14 21-32 40-53 55z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://www.linkedin.com/shareArticle?mini=true&url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&title=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.&summary=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.&source=https://a-chacon.com"
aria-label="Share on Linkedin">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Linkedin</title>
<path
d="M136 183v283H42V183h94zm6-88c1 27-20 49-53 49-32 0-52-22-52-49 0-28 21-49 53-49s52 21 52 49zm333 208v163h-94V314c0-38-13-64-47-64-26 0-42 18-49 35-2 6-3 14-3 23v158h-94V183h94v41c12-20 34-48 85-48 62 0 108 41 108 127z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://pinterest.com/pin/create/button/?url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&media=https://a-chacon.com&description=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only."
aria-label="Share on Pinterest" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Pinterest</title>
<path
d="M268 6C165 6 64 75 64 186c0 70 40 110 64 110 9 0 15-28 15-35 0-10-24-30-24-68 0-81 62-138 141-138 68 0 118 39 118 110 0 53-21 153-90 153-25 0-46-18-46-44 0-38 26-74 26-113 0-67-94-55-94 25 0 17 2 36 10 51-14 60-42 148-42 209 0 19 3 38 4 57 4 3 2 3 7 1 51-69 49-82 72-173 12 24 44 36 69 36 106 0 154-103 154-196C448 71 362 6 268 6z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://reddit.com/submit/?url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&resubmit=true&title=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only."
aria-label="Share on Reddit" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Reddit</title>
<path
d="M440 204c-15 0-28 6-38 15-35-24-83-40-137-42l28-125 88 20c0 22 18 39 39 39 22 0 40-18 40-39s-17-40-40-40c-15 0-28 9-35 22l-97-22c-5-1-10 3-11 7l-31 138c-53 2-100 18-136 43a53 53 0 0 0-38-16c-56 0-74 74-23 100l-3 24c0 84 95 152 210 152 117 0 211-68 211-152 0-8-1-17-3-25 50-25 32-99-24-99zM129 309a40 40 0 1 1 80 0 40 40 0 0 1-80 0zm215 93c-37 37-139 37-176 0-4-3-4-9 0-13s10-4 13 0c28 28 120 29 149 0 4-4 10-4 14 0s4 10 0 13zm-1-54c-22 0-39-17-39-39a39 39 0 1 1 39 39z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://wa.me/?text=PoC:%20Usando%20el%20Generador%20de%20Autenticaci%C3%B3n%20de%20Rails%208%20(Beta)%20En%20Modo%20API-Only.%20https://a-chacon.com"
aria-label="Share on Whatsapp" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Whatsapp</title>
<path
d="M413 97A222 222 0 0 0 64 365L31 480l118-31a224 224 0 0 0 330-195c0-59-25-115-67-157zM256 439c-33 0-66-9-94-26l-7-4-70 18 19-68-4-7a185 185 0 0 1 287-229c34 36 56 82 55 131 1 102-84 185-186 185zm101-138c-5-3-33-17-38-18-5-2-9-3-12 2l-18 22c-3 4-6 4-12 2-32-17-54-30-75-66-6-10 5-10 16-31 2-4 1-7-1-10l-17-41c-4-10-9-9-12-9h-11c-4 0-9 1-15 7-5 5-19 19-19 46s20 54 23 57c2 4 39 60 94 84 36 15 49 17 67 14 11-2 33-14 37-27s5-24 4-26c-2-2-5-4-11-6z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://telegram.me/share/url?text=PoC:%20Usando%20el%20Generador%20de%20Autenticaci%C3%B3n%20de%20Rails%208%20(Beta)%20En%20Modo%20API-Only.&url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"
aria-label="Share on Telegram" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Telegram</title>
<path
d="M256 8a248 248 0 1 0 0 496 248 248 0 0 0 0-496zm115 169c-4 39-20 134-28 178-4 19-10 25-17 25-14 2-25-9-39-18l-56-37c-24-17-8-25 6-40 3-4 67-61 68-67l-1-4-5-1q-4 1-105 70-15 10-27 9c-9 0-26-5-38-9-16-5-28-7-27-16q1-7 18-14l145-62c69-29 83-34 92-34 2 0 7 1 10 3l4 7a43 43 0 0 1 0 10z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700" target="_blank" rel="noopener" href="/cdn-cgi/l/email-protection#68571b1d0a020d0b1c5538072b524d5a583d1b09060c074d5a580d044d5a582f0d060d1a090c071a4d5a580c0d4d5a58291d1c0d061c010b090b014d2b5b4d2a5b064d5a580c0d4d5a583a0901041b4d5a58504d5a58402a0d1c09414d5a582d064d5a5825070c074d5a582938214527060411464e0a070c1155001c1c181b52474709450b00090b0706460b07054707064d5a581a0901041b475a585a5c47595847595e4718070b451d1b01060f451a0901041b455045091d1c00451b111b1c0d0545010645091801450706041146001c0504" aria-label="Share by Email" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Email</title>
<path
d="M464 64a48 48 0 0 1 29 86L275 314c-11 8-27 8-38 0L19 150a48 48 0 0 1 29-86h416zM218 339c22 17 54 17 76 0l218-163v208c0 35-29 64-64 64H64c-35 0-64-29-64-64V176l218 163z">
</path>
</svg>
</a>
</div>
</div>
</div>
<img class="w-full" src="/assets/images/rails8-poc-api-auth.webp" alt="PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.">
<div class="my-6">
<p>Como ya saben, una de las funcionalidades nuevas de Rails 8 es el <strong>nuevo generador básico de autenticación</strong> que viene a demostrar que no es tan complejo desarrollar todo lo que respecta a autenticación en una aplicación con Rails y que muchas veces no es necesario depender de terceros (gemas). La discusión comenzó <a href="https://github.com/rails/rails/issues/50446">aquí</a>.</p>
<p>Dicho esto, veamos que pasa usando el generador en una aplicación API-Only:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rails <span class="nt">-v</span>
Rails 8.0.0.beta1
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rails new app <span class="nt">--api</span> &amp; <span class="nb">cd </span>app
</code></pre></div></div>
<p>Y ejecutamos el nuevo comando:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rails g authentication
create app/models/session.rb
create app/models/user.rb
create app/models/current.rb
create app/controllers/sessions_controller.rb
create app/controllers/concerns/authentication.rb
create app/controllers/passwords_controller.rb
create app/mailers/passwords_mailer.rb
create app/views/passwords_mailer/reset.html.erb
create app/views/passwords_mailer/reset.text.erb
create <span class="nb">test</span>/mailers/previews/passwords_mailer_preview.rb
gsub app/controllers/application_controller.rb
route resources :passwords, param: :token
route resource :session
gsub Gemfile
bundle <span class="nb">install</span> <span class="nt">--quiet</span>
generate migration CreateUsers email_address:string!:uniq password_digest:string! <span class="nt">--force</span>
rails generate migration CreateUsers email_address:string!:uniq password_digest:string! <span class="nt">--force</span>
invoke active_record
create db/migrate/20241016002139_create_users.rb
generate migration CreateSessions user:references ip_address:string user_agent:string <span class="nt">--force</span>
rails generate migration CreateSessions user:references ip_address:string user_agent:string <span class="nt">--force</span>
invoke active_record
create db/migrate/20241016002140_create_sessions.rb
</code></pre></div></div>
<p>Ok, ahora por ejemplo, si revisamos <code class="language-plaintext highlighter-rouge">SessionsController</code> veremos que el método de <code class="language-plaintext highlighter-rouge">Login</code> se ve de la siguiente forma:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">def</span> <span class="nf">create</span>
<span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate_by</span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email_address</span><span class="p">,</span> <span class="ss">:password</span><span class="p">))</span>
<span class="n">start_new_session_for</span> <span class="n">user</span>
<span class="n">redirect_to</span> <span class="n">after_authentication_url</span>
<span class="k">else</span>
<span class="n">redirect_to</span> <span class="n">new_session_url</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Try another email address or password."</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>O sea, redirecciona a rutas y/o vistas que en nuestra API no existen ni hacen sentido, y además si inspeccionamos el metodo <code class="language-plaintext highlighter-rouge">start_new_session_for</code> nos daremos cuenta de que el sistema está basado 100% en <strong>autenticación mediante cookies</strong>. Entonces, ¿qué hacemos?</p>
<p>Mi propuesta es la siguiente: el generador crea las bases para la autenticación y creo que funciona bastante bien, por lo que con unas pequeñas modificaciones podemos dejar funcionando una <strong>autenticación Bearer</strong> (Token Authentication) rápidamente en nuestra API con Rails 8 más los archivos ya generados.</p>
<p>El primer paso será agregar <strong>persistencia para nuestro token</strong>, para esto modificaremos la migración que crea las sessiones y agregaremos un nuevo campo llamado <code class="language-plaintext highlighter-rouge">token</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">create_table</span> <span class="ss">:sessions</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">foreign_key: </span><span class="kp">true</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:ip_address</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:user_agent</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:token</span> <span class="c1"># HERE</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Ahora simplemente ejecuta <code class="language-plaintext highlighter-rouge">rails db:migrate</code> y create un usuario de prueba por consola, yo lo haré con esta línea <code class="language-plaintext highlighter-rouge">User.create(email_address: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a6d3d5c3d4e6d2c3d5d288c5c9cb">[email&#160;protected]</a>", password: "123456789")</code> (Lo utilizaremos más tarde). Luego debemos crear un nuevo token para cada sesión nueva de un usuario, para esto lo más simple es usar un callback en el modelo <code class="language-plaintext highlighter-rouge">Session</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/sessions.rb</span>
<span class="k">class</span> <span class="nc">Session</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="n">before_create</span> <span class="ss">:generate_token</span> <span class="c1"># Here call</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">generate_token</span> <span class="c1"># Here implement, generate the token as you wish.</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">token</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">([</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">,</span> <span class="nb">rand</span> <span class="p">].</span><span class="nf">join</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Ahora volviendo al metodo <code class="language-plaintext highlighter-rouge">start_new_session_for</code> en el concern <code class="language-plaintext highlighter-rouge">Authentication</code>, no es necesario que creemos una cookie, asi que debemos remover esa linea y dejar el metodo de la siguiente forma:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/concerns/authentication.rb</span>
<span class="k">def</span> <span class="nf">start_new_session_for</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">user</span><span class="p">.</span><span class="nf">sessions</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">user_agent: </span><span class="n">request</span><span class="p">.</span><span class="nf">user_agent</span><span class="p">,</span> <span class="ss">ip_address: </span><span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">session</span><span class="o">|</span>
<span class="no">Current</span><span class="p">.</span><span class="nf">session</span> <span class="o">=</span> <span class="n">session</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Y modificaremos el <code class="language-plaintext highlighter-rouge">create</code> de <code class="language-plaintext highlighter-rouge">SessionsController</code> para que las respuestas sean en formato json y no redirecciones:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/sessions_controller.rb</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate_by</span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email_address</span><span class="p">,</span> <span class="ss">:password</span><span class="p">))</span>
<span class="n">start_new_session_for</span> <span class="n">user</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="no">Current</span><span class="p">.</span><span class="nf">session</span><span class="p">.</span><span class="nf">token</span> <span class="p">}</span> <span class="p">}</span>
<span class="k">else</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{},</span> <span class="ss">status: :unauthorized</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p><strong>Para hacer que todo esto funcione debemos hacer dos cosas:</strong></p>
<ol>
<li>
<p>Incluir el modulo <code class="language-plaintext highlighter-rouge">Authentication</code> en <code class="language-plaintext highlighter-rouge">ApplicationController</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/application_controller.rb</span>
<span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span>
<span class="kp">include</span> <span class="no">Authentication</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
<li>
<p>Eliminar la linea numero 6 de este mismo concern:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/concerns/authentication.rb</span>
<span class="n">included</span> <span class="k">do</span>
<span class="n">before_action</span> <span class="ss">:require_authentication</span>
<span class="n">helper_method</span> <span class="ss">:authenticated?</span> <span class="c1"># This, we don't use helpers in APIs</span>
<span class="k">end</span>
</code></pre></div> </div>
</li>
</ol>
<p>Hasta este punto ya deberíamos tener el <strong>login funcionando</strong>. Para probar esto voy a agregar <a href="https://github.com/a-chacon/oas_rails">OasRails</a>, que a propósito <strong>ya está funcionando con Rails 8</strong> y voy a enviar un par de peticiones a ver como se comporta, no explicaré como implementar OasRails, para eso puedes ver el repositorio o leer más en <a href="/on rails/2024/07/25/documenting-rails-apis">este post</a>.</p>
<p>Inicio de sesión exitoso:</p>
<p><img src="/assets/images/rails8_success_login.png" alt="" /></p>
<p>Inicio de sesión fallido:</p>
<p><img src="/assets/images/rails8_fail_login.png" alt="" /></p>
<hr />
<p>Ya podemos generar tokens, ahora modificaremos el código para <strong>autenticarnos con ese mismo token</strong>. Para eso, cambiaremos la lógica de buscar la sesión actual del usuario con base en la cookie a buscarla basándonos en la cabecera <code class="language-plaintext highlighter-rouge">Authorization</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># app/controllers/concerns/authentication.rb</span>
<span class="k">def</span> <span class="nf">resume_session</span>
<span class="no">Current</span><span class="p">.</span><span class="nf">session</span> <span class="o">=</span> <span class="n">find_session_by_token</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">find_session_by_cookie</span>
<span class="no">Session</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">token: </span><span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="ss">:authorization</span><span class="p">]</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Para probar esto creo que tendremos que hacer rápidamente un modelo que dependa de <code class="language-plaintext highlighter-rouge">User</code> y que requiera autenticación para utilizar. Intentemos con <code class="language-plaintext highlighter-rouge">rails g scaffold project title:string description:text user:references</code> y le agregamos al principio del controlador la línea de código <code class="language-plaintext highlighter-rouge">before_action :require_authentication</code>.</p>
<p>Aquí les dejo una pequeña prueba del index de Projects autenticado con el token que obtuve en las pruebas anteriores:</p>
<p><img src="/assets/images/rails8_projects.png" alt="" /></p>
<hr />
<p>Con esto ya tienes gran parte de la lógica de autenticación funcionando en la aplicación API-Only. Te queda continuar con las modificaciones en el resto de los endpoints para que las respuestas sean en formato json y no supuestas vistas que no existen en la aplicación.</p>
<p>Probablemente de aquí a que se lance la versión final de Rails 8 aparezca un <strong>PR solucionando esto y el generador funcione correctamente en modo API-Only</strong>. Hasta entonces, con estas pequeñas modificaciones ya puedes seguir construyendo tu API.</p>
</div>
<div class="mx-auto py-8">
<p class="text-center font-extrabold italic">
¿Te gustó? ¡Compártelo!
</p>
<div class="sharing-buttons flex flex-wrap gap-1 justify-center ">
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://facebook.com/sharer/sharer.php?u=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"
aria-label="Share on Facebook">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Facebook</title>
<path
d="M379 22v75h-44c-36 0-42 17-42 41v54h84l-12 85h-72v217h-88V277h-72v-85h72v-62c0-72 45-112 109-112 31 0 58 3 65 4z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://twitter.com/intent/tweet?url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&text=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only."
aria-label="Share on Twitter">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Twitter</title>
<path
d="m459 152 1 13c0 139-106 299-299 299-59 0-115-17-161-47a217 217 0 0 0 156-44c-47-1-85-31-98-72l19 1c10 0 19-1 28-3-48-10-84-52-84-103v-2c14 8 30 13 47 14A105 105 0 0 1 36 67c51 64 129 106 216 110-2-8-2-16-2-24a105 105 0 0 1 181-72c24-4 47-13 67-25-8 24-25 45-46 58 21-3 41-8 60-17-14 21-32 40-53 55z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://www.linkedin.com/shareArticle?mini=true&url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&title=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.&summary=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.&source=https://a-chacon.com"
aria-label="Share on Linkedin">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Linkedin</title>
<path
d="M136 183v283H42V183h94zm6-88c1 27-20 49-53 49-32 0-52-22-52-49 0-28 21-49 53-49s52 21 52 49zm333 208v163h-94V314c0-38-13-64-47-64-26 0-42 18-49 35-2 6-3 14-3 23v158h-94V183h94v41c12-20 34-48 85-48 62 0 108 41 108 127z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://pinterest.com/pin/create/button/?url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&media=https://a-chacon.com&description=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only."
aria-label="Share on Pinterest" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Pinterest</title>
<path
d="M268 6C165 6 64 75 64 186c0 70 40 110 64 110 9 0 15-28 15-35 0-10-24-30-24-68 0-81 62-138 141-138 68 0 118 39 118 110 0 53-21 153-90 153-25 0-46-18-46-44 0-38 26-74 26-113 0-67-94-55-94 25 0 17 2 36 10 51-14 60-42 148-42 209 0 19 3 38 4 57 4 3 2 3 7 1 51-69 49-82 72-173 12 24 44 36 69 36 106 0 154-103 154-196C448 71 362 6 268 6z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://reddit.com/submit/?url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html&resubmit=true&title=PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only."
aria-label="Share on Reddit" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Reddit</title>
<path
d="M440 204c-15 0-28 6-38 15-35-24-83-40-137-42l28-125 88 20c0 22 18 39 39 39 22 0 40-18 40-39s-17-40-40-40c-15 0-28 9-35 22l-97-22c-5-1-10 3-11 7l-31 138c-53 2-100 18-136 43a53 53 0 0 0-38-16c-56 0-74 74-23 100l-3 24c0 84 95 152 210 152 117 0 211-68 211-152 0-8-1-17-3-25 50-25 32-99-24-99zM129 309a40 40 0 1 1 80 0 40 40 0 0 1-80 0zm215 93c-37 37-139 37-176 0-4-3-4-9 0-13s10-4 13 0c28 28 120 29 149 0 4-4 10-4 14 0s4 10 0 13zm-1-54c-22 0-39-17-39-39a39 39 0 1 1 39 39z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://wa.me/?text=PoC:%20Usando%20el%20Generador%20de%20Autenticaci%C3%B3n%20de%20Rails%208%20(Beta)%20En%20Modo%20API-Only.%20https://a-chacon.com"
aria-label="Share on Whatsapp" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Whatsapp</title>
<path
d="M413 97A222 222 0 0 0 64 365L31 480l118-31a224 224 0 0 0 330-195c0-59-25-115-67-157zM256 439c-33 0-66-9-94-26l-7-4-70 18 19-68-4-7a185 185 0 0 1 287-229c34 36 56 82 55 131 1 102-84 185-186 185zm101-138c-5-3-33-17-38-18-5-2-9-3-12 2l-18 22c-3 4-6 4-12 2-32-17-54-30-75-66-6-10 5-10 16-31 2-4 1-7-1-10l-17-41c-4-10-9-9-12-9h-11c-4 0-9 1-15 7-5 5-19 19-19 46s20 54 23 57c2 4 39 60 94 84 36 15 49 17 67 14 11-2 33-14 37-27s5-24 4-26c-2-2-5-4-11-6z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700"
target="_blank" rel="noopener"
href="https://telegram.me/share/url?text=PoC:%20Usando%20el%20Generador%20de%20Autenticaci%C3%B3n%20de%20Rails%208%20(Beta)%20En%20Modo%20API-Only.&url=https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"
aria-label="Share on Telegram" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Telegram</title>
<path
d="M256 8a248 248 0 1 0 0 496 248 248 0 0 0 0-496zm115 169c-4 39-20 134-28 178-4 19-10 25-17 25-14 2-25-9-39-18l-56-37c-24-17-8-25 6-40 3-4 67-61 68-67l-1-4-5-1q-4 1-105 70-15 10-27 9c-9 0-26-5-38-9-16-5-28-7-27-16q1-7 18-14l145-62c69-29 83-34 92-34 2 0 7 1 10 3l4 7a43 43 0 0 1 0 10z">
</path>
</svg>
</a>
<a class="border-2 duration-200 ease inline-flex items-center mb-1 mr-1 transition p-2 rounded-full text-white border-slate-600 bg-slate-600 hover:bg-slate-700 hover:border-slate-700" target="_blank" rel="noopener" href="/cdn-cgi/l/email-protection#79460a0c1b131c1a0d4429163a435c4b492c0a18171d165c4b491c155c4b493e1c171c0b181d160b5c4b491d1c5c4b49380c0d1c170d101a181a105c3a4a5c3b4a175c4b491d1c5c4b492b1810150a5c4b49415c4b49513b1c0d18505c4b493c175c4b4934161d165c4b493829305436171500575f1b161d0044110d0d090a43565618541a11181a1617571a16145616175c4b490b1810150a564b494b4d56484956484f5609161a540c0a10171e540b1810150a544154180c0d11540a000a0d1c1454101754180910541617150057110d1415" aria-label="Share by Email" draggable="false">
<svg aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"
class="w-5 h-5">
<title>Email</title>
<path
d="M464 64a48 48 0 0 1 29 86L275 314c-11 8-27 8-38 0L19 150a48 48 0 0 1 29-86h416zM218 339c22 17 54 17 76 0l218-163v208c0 35-29 64-64 64H64c-35 0-64-29-64-64V176l218 163z">
</path>
</svg>
</a>
</div>
</div>
<div class="font-mulish flex justify-center mb-10">
<a class="pr-2" href="/docker/2024/09/16/run-llm-locally.html">&laquo; LLM en local: Corriendo Ollama y Open WebUI con Docker Compose.</a>
</div>
<!-- <div class="mx-auto py-8"> -->
<!-- <script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" -->
<!-- data-slug="achacon" data-color="#FFDD00" data-emoji="" data-font="Lato" data-text="Buy me a coffee" -->
<!-- data-outline-color="#000000" data-font-color="#000000" data-coffee-color="#ffffff"></script> -->
<!-- </div> -->
<!---->
<!-- <div class="my-6"> -->
<!-- ¡Hola a todos! 👋 ¿Disfrutaron leyendo el artículo? ¡Me encantaría conocer sus opiniones! 💬 No duden en dejar un -->
<!-- comentario abajo, ya sea para compartir sus comentarios, preguntas o simplemente saludar. ¡No es necesario -->
<!-- registrarse, solo compartan algo valioso! 😊 -->
<!-- </div> -->
<!-- <div class="my-6"> -->
<!-- Hey there! 👋 Enjoyed reading the post? I'd love to hear your thoughts! 💬 Feel free to drop a comment -->
<!-- below—whether it's feedback, questions, or just saying hi. No need to sign up, just share something valuable! 😊 -->
<!-- </div> -->
<!---->
<!-- <div id="embedContainer" style="height: fit-content;" data-identifier="/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html" -->
<!-- data-site-key="80dce74b8e1af5839a6f"></div> -->
<!-- <script src="https://opina.fly.dev/embed.js"></script> -->
</div>
</div>
<footer class="w-full bottom-0">
<hr class="my-8 border-zinc-200" />
<div class="container mx-auto">
<div class="flex flex-wrap text-left">
<div class="w-full md:w-6/12 md:px-4">
<p class="text-3xl font-semibold">
Mantengamosnos en contacto!
</p>
<p class="font-mulish text-lg dark:text-zinc-300 mt-2 mb-2">
Encuéntrame en cualquiera de estas plataformas:
</p>
<div class="flex justify-center md:justify-start pt-2 gap-6">
<a class="transition hover:scale-110 duration-300" href="/cdn-cgi/l/email-protection#b7d6d9d3c5d2c499d4dff7c7c5d8c3d8d9dad6dedb99d4d8da">
<img
class="h-6 w-6"
src="/assets/images/contact_channels/email.svg"
alt="Email Icon"
/>
</a>
<a
class="transition hover:scale-110 duration-300"
href="https://github.com/a-chacon"
target="_blank"
>
<img
class="h-6 w-6"
src="/assets/images/contact_channels/github-mark.svg"
alt="Github Icon"
/>
</a>
<a
class="transition hover:scale-110 duration-300"
href="https://t.me/a_chacon"
target="_blank"
>
<img
class="h-6 w-6"
src="/assets/images/contact_channels/telegram.svg"
alt="Telegram Icon"
/>
</a>
<a
class="transition hover:scale-110 duration-300"
href="https://stackoverflow.com/users/16847024/andr%c3%a9s"
target="_blank"
>
<img
class="h-6 w-6"
src="/assets/images/contact_channels/stack-overflow.svg"
alt="StackOverflow Icon"
/>
</a>
<a
class="transition hover:scale-110 duration-300"
href="https://discord.com/users/673943171369140252"
target="_blank"
>
<img
class="h-6 w-6"
src="/assets/images/contact_channels/discord.svg"
alt="Discord Icon"
/>
</a>
<a
class="transition hover:scale-110 duration-300"
rel="me"
href="https://lile.cl/@achacon"
target="_blank"
>
<img
class="h-6 w-6"
src="/assets/images/contact_channels/mastodon.svg"
alt="Mastodon Icon"
/>
</a>
</div>
</div>
<div class="w-full md:w-6/12 md:text-right py-4 md:py-0">
<div class="flex flex-wrap items-top mb-6">
<div class="w-full lg:w-4/12 md:px-4 ml-auto">
<span class="text-lg block text-gray-800 mb-2">
Páginas
</span>
<ul>
<li>
<a class="font-mulish text-slate-600 hover:text-sky-400 dark:text-slate-300 dark:hover:text-slate-200"
href="/blog.html">Blog</a>
</li>
<li>
<a class="font-mulish text-slate-600 hover:text-sky-400 dark:text-slate-300 dark:hover:text-slate-200"
href="/">Casa</a>
</li>
<li>
<a class="font-mulish text-slate-600 hover:text-sky-400 dark:text-slate-300 dark:hover:text-slate-200"
href="/projects">Proyectos</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="font-mulish flex flex-wrap items-center md:justify-between justify-center my-8">
<div class="w-full md:w-4/12 px-4 mx-auto text-center">
<a href="/feed.xml" class="p-2">
<svg fill="#000000" viewBox="-3 0 19 19" xmlns="http://www.w3.org/2000/svg" class="w-10 mx-auto">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path
d="M.926 12.818a1.403 1.403 0 1 1 0 1.984 1.402 1.402 0 0 1 0-1.984zm10.531 2.357a1.03 1.03 0 0 1-1.029-1.03 8.775 8.775 0 0 0-.694-3.438A8.826 8.826 0 0 0 1.591 5.31a1.03 1.03 0 1 1 0-2.059 10.817 10.817 0 0 1 4.24.857 10.893 10.893 0 0 1 3.463 2.334 10.867 10.867 0 0 1 3.19 7.703 1.027 1.027 0 0 1-1.027 1.029zm-4.538 0a1.03 1.03 0 0 1-1.029-1.03 4.297 4.297 0 0 0-4.299-4.298 1.03 1.03 0 0 1 0-2.059 6.362 6.362 0 0 1 5.857 3.883 6.298 6.298 0 0 1 .5 2.475 1.03 1.03 0 0 1-1.029 1.029z">
</path>
</g>
</svg>
</a>
<div class="text-sm text-blueGray-500 font-semibold py-1">
© <span id="get-current-year">2024</span>
<a href="https://www.creative-tim.com/product/notus-js" class="text-blueGray-500 hover:text-gray-800"
target="_blank" aria-label="Creative C">a-chacon.</a>
All rights reserved.
<p class="text-xs pt-4">
Built with ❤️ using the
<a class="hover:text-sky-400" href="https://github.com/a-chacon/awesome-jekyll-theme">Awesome Jekyll
Theme</a>.
</p>
</div>
</div>
</div>
</div>
</footer>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>
document.querySelectorAll("a").forEach(function (link) {
link.addEventListener("click", function (event) {
event.preventDefault(); // Prevent the default link action temporarily
link.classList.add("gentle-clicked");
setTimeout(function () {
link.classList.remove("gentle-clicked");
window.location.href = link.href; // Navigate to the link after the animation
}, 300); // Duration of the gentle bounce animation
});
});
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'8dc35b4edefb4223',t:'MTczMDU0MjQ1Ni4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>

View file

@ -47,6 +47,7 @@ pub fn clean_html_fragment(
if let Some(mut root) = document.get_root_element() {
FullTextParser::post_process_page(&mut root)?;
}
FullTextParser::prevent_self_closing_tags(&xpath_ctx)?;
FullTextParser::post_process_document(&document)?;
let content_node = if let Some(root) = document.get_root_element() {
@ -160,4 +161,23 @@ mod tests {
Some("https://cdn.finshots.app/images/2023/03/Design-8-Amul.jpg")
)
}
#[test]
fn pointieststick() {
let html = r#"
<p>I recently went on Brodie Robertson&#8217;s Tech Over Tea channel for a second time. I guess I didn&#8217;t succeed at pissing him off enough on the first go-around, because he invited me back! Let&#8217;s see if I did a better job of it this time by telling him he was using Arch wrong. <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f600.png" alt="😀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>Anyway, Brodie was a fantastic host, and we talked about a number of topics such as KDE&#8217;s position in the world, institutional continuity, fundraising and financial stability, the difficulty of reporting and triaging bug, the challenges of packaging software, and windows that block WiFi signals.</p>
<p>I hope you enjoy it!</p>
<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="1100" height="619" src="https://www.youtube.com/embed/qJZ2V5FmgO8?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=en&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>
<p>And here&#8217;s the link I mention at the end: <a href="https://kde.org/community/donations">https://kde.org/community/donations</a> <img src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> </p>
"#;
let url = Url::parse("https://pointieststick.com").unwrap();
let res = clean_html_fragment(html, &url).unwrap();
assert_eq!(res.thumbnail, None);
assert!(res.html.contains("iframe"));
}
}

View file

@ -141,6 +141,14 @@ pub static DIV_TO_P_ELEMS: Lazy<HashSet<&str>> = Lazy::new(|| {
pub static VALID_EMPTY_TAGS: Lazy<HashSet<&str>> = Lazy::new(|| {
HashSet::from([
"AREA", "BASE", "BR", "COL", "EMBED", "HR", "IMG", "LINK", "META", "SOURCE", "TRACK",
"IFRAME",
])
});
pub static VALID_SELF_CLOSING_TAGS: Lazy<HashSet<&str>> = Lazy::new(|| {
HashSet::from([
"AREA", "BASE", "BR", "COL", "EMBED", "HR", "IMG", "INPUT", "LINK", "META", "PARAM",
"SOURCE", "TRACK", "WBR",
])
});

View file

@ -6,10 +6,10 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum ScraperError {
#[error("")]
#[error("Configerror {0}")]
Config(#[from] ConfigError),
#[error("")]
#[error("ImageDownloadError {0}")]
Image(#[from] ImageDownloadError),
#[error("")]
#[error("FullTextParserError {0}")]
Scrap(#[from] FullTextParserError),
}

View file

@ -69,6 +69,11 @@ impl FullTextParser {
let html = Self::get_body(response).await?;
if html.is_empty() {
log::error!("Empty response body");
return Err(FullTextParserError::Http);
}
// check for fingerprints
let config = if config.is_none() {
if let Some(url) = Fingerprints::detect(&html) {
@ -264,11 +269,17 @@ impl FullTextParser {
}
// parse html
let parser = Parser::default_html();
Self::parse_html_string_patched(html.as_str(), &parser).map_err(|err| {
let document = Self::parse_html_string_patched(html.as_str()).map_err(|err| {
log::error!("Parsing HTML failed for downloaded HTML {:?}", err);
FullTextParserError::Xml
})
})?;
if document.get_root_element().is_none() {
log::error!("document without root");
Err(FullTextParserError::Xml)
} else {
Ok(document)
}
}
/// FIXME: Here are some patched functions of libxml crate.
@ -278,7 +289,7 @@ impl FullTextParser {
/// - <https://github.com/Orange-OpenSource/hurl/issues/1535>
/// These two functions should be removed when the issue is fixed in libxml crate.
fn try_usize_to_i32(value: usize) -> Result<i32, libxml::parser::XmlParseError> {
if cfg!(target_pointer_width = "16") || (value < i32::max_value() as usize) {
if cfg!(target_pointer_width = "16") || (value < i32::MAX as usize) {
// Cannot safely use our value comparison, but the conversion if always safe.
// Or, if the value can be safely represented as a 32-bit signed integer.
Ok(value as i32)
@ -288,10 +299,14 @@ impl FullTextParser {
}
}
fn parse_html_string_patched(
pub(crate) fn parse_html_string_patched(
input: &str,
parser: &Parser,
) -> Result<Document, libxml::parser::XmlParseError> {
unsafe {
// https://gitlab.gnome.org/GNOME/libxml2/-/wikis/Thread-safety
libxml::bindings::xmlInitParser();
}
let parser = Parser::default_html();
let input_bytes: &[u8] = input.as_ref();
let input_ptr = input_bytes.as_ptr() as *const std::os::raw::c_char;
let input_len = Self::try_usize_to_i32(input_bytes.len())?;
@ -351,19 +366,32 @@ impl FullTextParser {
.send()
.await
.map_err(|err| {
log::error!(
"Downloading HTML failed: GET '{}' - '{}'",
url.as_str(),
err
);
log::error!("Downloading HTML failed: GET '{url}' - '{err}'");
FullTextParserError::Http
})?;
Ok(response)
}
async fn get_body(response: Response) -> Result<String, FullTextParserError> {
if response.status().is_success() {
let status = response.status();
if !status.is_success() {
log::error!("status code: {status}");
return Err(FullTextParserError::Http);
}
let headers = response.headers().clone();
if headers
.get(reqwest::header::CONTENT_LENGTH)
.and_then(|hv| hv.to_str().ok())
.and_then(|str| str.parse::<i64>().ok())
.map(|content_length| content_length == 0)
.unwrap_or(false)
{
log::error!("Empty response body");
return Err(FullTextParserError::Http);
}
let bytes = response
.bytes()
.await
@ -372,14 +400,14 @@ impl FullTextParser {
match from_utf8(&bytes) {
Ok(utf8_str) => {
log::debug!("Valid utf-8 string");
return Ok(utf8_str.into());
Ok(utf8_str.into())
}
Err(error) => {
log::debug!("Invalid utf-8 string");
let lossy_string = std::string::String::from_utf8_lossy(&bytes);
if let Some(encoding) = Self::get_encoding_from_html(&lossy_string) {
log::debug!("Encoding extracted from HTML: '{}'", encoding);
log::debug!("Encoding extracted from HTML: '{encoding}'");
if let Some(decoded_html) = Self::decode_html(&bytes, encoding) {
let decoded_html = decoded_html.replacen(
&format!("charset=\"{encoding}\""),
@ -391,7 +419,7 @@ impl FullTextParser {
}
if let Some(encoding) = Self::get_encoding_from_http_header(&headers) {
log::debug!("Encoding extracted from headers: '{}'", encoding);
log::debug!("Encoding extracted from headers: '{encoding}'");
if let Some(decoded_html) = Self::decode_html(&bytes, encoding) {
let decoded_html = decoded_html.replacen(
&format!("charset=\"{encoding}\""),
@ -402,14 +430,11 @@ impl FullTextParser {
}
}
return Err(FullTextParserError::Utf8(error));
Err(FullTextParserError::Utf8(error))
}
}
}
Err(FullTextParserError::Http)
}
pub async fn download(
url: &url::Url,
client: &Client,
@ -419,8 +444,13 @@ impl FullTextParser {
let headers = Util::generate_headers(config, global_config)?;
let response = Self::get_response(url, client, headers).await?;
let body = Self::get_body(response).await?;
if body.is_empty() {
log::error!("Empty response body");
Err(FullTextParserError::Http)
} else {
Ok(body)
}
}
fn get_encoding_from_http_header(headers: &reqwest::header::HeaderMap) -> Option<&str> {
headers
@ -488,7 +518,7 @@ impl FullTextParser {
}
pub fn thumbnail_from_html(html: &str) -> Option<String> {
if let Ok(doc) = Parser::default_html().parse_string(html) {
if let Ok(doc) = Self::parse_html_string_patched(html) {
if let Ok(ctx) = Self::get_xpath_ctx(&doc) {
return Self::check_for_thumbnail(&ctx);
}
@ -511,6 +541,22 @@ impl FullTextParser {
return Some(thumb);
}
if let Ok(thumb) = Util::get_attribute(
context,
"//meta[contains(@property, 'twitter:image')]",
"content",
) {
return Some(thumb);
}
if let Ok(thumb) = Util::get_attribute(
context,
"//meta[contains(@property, 'og:image')]",
"content",
) {
return Some(thumb);
}
if let Ok(thumb) =
Util::get_attribute(context, "//link[contains(@rel, 'image_src')]", "href")
{
@ -533,6 +579,7 @@ impl FullTextParser {
let score = score + Util::score_by_sibling(&img_node);
let score = score + Util::score_by_dimensions(&img_node);
let score = score + Util::score_by_position(len, index);
let score = score + Util::score_by_alt(&img_node);
scores.insert(src, score);
}
@ -689,8 +736,9 @@ impl FullTextParser {
let success = video_wrapper
.set_property("class", "videoWrapper")
.ok()
.and_then(|()| node.set_property("width", "100%").ok())
.and_then(|()| node.set_property("height", "400").ok())
.and_then(|()| node.set_property("width", "480").ok())
.and_then(|()| node.set_property("height", "360").ok())
.and_then(|()| node.set_property("aspect-ratio", "auto").ok())
.ok_or_else(|| {
node.unlink();
video_wrapper.add_child(&mut node)
@ -965,6 +1013,7 @@ impl FullTextParser {
if let Some(root) = document.get_root_element() {
Util::replace_brs(&root, document);
Util::replace_emoji_images(&root, document);
}
Self::fix_urls(context, url, document);
@ -1175,15 +1224,15 @@ impl FullTextParser {
}
}
fn prevent_self_closing_tags(context: &Context) -> Result<(), FullTextParserError> {
pub(crate) fn prevent_self_closing_tags(context: &Context) -> Result<(), FullTextParserError> {
// search document for empty tags and add a empty text node as child
// this prevents libxml from self closing non void elements such as iframe
let xpath = "//*[not(node())]";
let node_vec = Util::evaluate_xpath(context, xpath, false)?;
for mut node in node_vec {
let name = node.get_name().to_lowercase();
if name == "meta" || name == "img" || name == "br" {
let name = node.get_name().to_uppercase();
if constants::VALID_SELF_CLOSING_TAGS.contains(name.as_str()) {
continue;
}

View file

@ -39,6 +39,10 @@ async fn run_test(name: &str) {
metadata::extract(&xpath_ctx, None, None, &mut article);
super::Readability::extract_body(document, &mut root, article.title.as_deref()).unwrap();
let article_ctx = crate::FullTextParser::get_xpath_ctx(&article_document).unwrap();
crate::FullTextParser::prevent_self_closing_tags(&article_ctx).unwrap();
crate::FullTextParser::post_process_document(&article_document).unwrap();
let html = Util::serialize_node(&article_document, &root);

View file

@ -1,5 +1,5 @@
use super::{config::ConfigEntry, FullTextParser};
use libxml::{parser::Parser, tree::SaveOptions, xpath::Context};
use libxml::{tree::SaveOptions, xpath::Context};
use reqwest::{Client, Url};
async fn run_test(name: &str, url: &str, title: Option<&str>, author: Option<&str>) {
@ -194,7 +194,7 @@ herausgebracht. (<a href="https://www.golem.de/specials/fortschritt/" rel="noope
referrerpolicy="no-referrer">Fortschritt</a>, <a href="https://www.golem.de/specials/wissenschaft/"
rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">Wissenschaft</a>)
"#;
let doc = Parser::default_html().parse_string(html).unwrap();
let doc = FullTextParser::parse_html_string_patched(html).unwrap();
let ctx = Context::new(&doc).unwrap();
let thumb = FullTextParser::check_for_thumbnail(&ctx).unwrap();
@ -269,7 +269,7 @@ Foto: IMAGO/Vaclav Salek / IMAGO/CTK Photo
</section></article>
"#;
let doc = Parser::default_html().parse_string(html).unwrap();
let doc = FullTextParser::parse_html_string_patched(html).unwrap();
let ctx = Context::new(&doc).unwrap();
let thumb = FullTextParser::check_for_thumbnail(&ctx).unwrap();
@ -278,3 +278,17 @@ Foto: IMAGO/Vaclav Salek / IMAGO/CTK Photo
"https://cdn.prod.www.spiegel.de/images/a4573666-f15e-4290-8c73-a0c6cd4ad3b2_w948_r1.778_fpx29.99_fpy44.98.jpg"
)
}
#[test]
fn extract_thumbnail_a_chacon() {
let html = std::fs::read_to_string("./resources/tests/thumbnails/a-chacon.html")
.expect("Failed to read source HTML");
let doc = FullTextParser::parse_html_string_patched(&html).unwrap();
let ctx = Context::new(&doc).unwrap();
let thumb = FullTextParser::check_for_thumbnail(&ctx).unwrap();
assert_eq!(
thumb,
"https://a-chacon.com/assets/images/rails8-poc-api-auth.webp"
)
}

View file

@ -2,7 +2,6 @@
pub struct ImageData {
pub url: String,
pub data: Vec<u8>,
pub content_length: usize,
pub content_type: String,
}

View file

@ -2,12 +2,11 @@ pub use self::error::ImageDownloadError;
use self::image_data::ImageDataBase64;
use self::pair::Pair;
use self::request::ImageRequest;
use crate::constants;
use crate::util::Util;
use crate::{constants, FullTextParser};
use base64::Engine;
use futures::StreamExt;
use image::ImageFormat;
use libxml::parser::Parser;
use libxml::tree::{Node, SaveOptions};
use libxml::xpath::Context;
pub use progress::Progress;
@ -162,9 +161,7 @@ impl ImageDownloader {
html: &str,
downloaded_images: Vec<Pair<ImageDataBase64>>,
) -> Result<String, ImageDownloadError> {
let parser = Parser::default_html();
let doc = parser
.parse_string(html)
let doc = FullTextParser::parse_html_string_patched(html)
.map_err(|_| ImageDownloadError::HtmlParse)?;
let xpath_ctx = Context::new(&doc).map_err(|()| {
@ -207,9 +204,7 @@ impl ImageDownloader {
}
fn harvest_image_urls_from_html(html: &str) -> Result<Vec<Pair<String>>, ImageDownloadError> {
let parser = Parser::default_html();
let doc = parser
.parse_string(html)
let doc = FullTextParser::parse_html_string_patched(html)
.map_err(|_| ImageDownloadError::HtmlParse)?;
let xpath_ctx = Context::new(&doc).map_err(|()| {
@ -224,7 +219,9 @@ impl ImageDownloader {
let mut image_urls = Vec::new();
for node in node_vec {
image_urls.push(Self::harvest_image_urls_from_node(node)?);
if let Ok(url) = Self::harvest_image_urls_from_node(node) {
image_urls.push(url);
}
}
Ok(image_urls)

View file

@ -48,7 +48,6 @@ impl ImageRequest {
Ok(ImageData {
url: self.url,
data: result,
content_length: self.content_length,
content_type: self.content_type,
})
}

View file

@ -682,6 +682,33 @@ impl Util {
}
}
pub fn replace_emoji_images(root: &Node, document: &Document) {
let img_nodes = Util::get_elements_by_tag_name(root, "img");
for img_node in img_nodes {
if let Some(img_alt) = img_node.get_attribute("alt") {
if Self::is_emoji(&img_alt) {
if let Some(mut parent) = img_node.get_parent() {
let emoji_text_node = Node::new_text(&img_alt, document).unwrap();
_ = parent.replace_child_node(emoji_text_node, img_node);
}
}
}
}
}
pub fn is_emoji(text: &str) -> bool {
let mut alt_chars = text.chars();
let first_char = alt_chars.next();
let second_char = alt_chars.next();
if let (Some(char), None) = (first_char, second_char) {
unic_emoji_char::is_emoji(char)
} else {
false
}
}
// Clean an element of all tags of type "tag" if they look fishy.
// "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc.
pub fn clean_conditionally(root: &mut Node, tag: &str) {
@ -1223,6 +1250,18 @@ impl Util {
((len as f32 / 2.0) - index as f32) as i32
}
pub fn score_by_alt(node: &Node) -> i32 {
if let Some(alt) = node.get_attribute("alt") {
if Self::is_emoji(&alt) {
-100
} else {
0
}
} else {
0
}
}
pub fn get_content_length(response: &Response) -> Result<usize, ImageDownloadError> {
let status_code = response.status();
@ -1258,15 +1297,13 @@ impl Util {
#[cfg(test)]
mod tests {
use libxml::parser::Parser;
use super::Util;
use crate::FullTextParser;
fn replace_brs(source: &str, expected: &str) {
libxml::tree::node::set_node_rc_guard(10);
let parser = Parser::default_html();
let document = parser.parse_string(source).unwrap();
let document = FullTextParser::parse_html_string_patched(source).unwrap();
let root = document.get_root_element().unwrap();
let body = root.get_first_child().unwrap();
let div = body.get_first_child().unwrap();
@ -1303,4 +1340,35 @@ mod tests {
"#;
replace_brs(source, source.trim())
}
fn replace_emojis(source: &str, expected: &str) {
libxml::tree::node::set_node_rc_guard(10);
let document = FullTextParser::parse_html_string_patched(source).unwrap();
let root = document.get_root_element().unwrap();
let body = root.get_first_child().unwrap();
let p = body.get_first_child().unwrap();
Util::replace_emoji_images(&root, &document);
let result = document.node_to_string(&p);
assert_eq!(expected, result);
}
#[test]
fn replace_emojis_1() {
replace_emojis(
"<p>Lets see if I did a better job of it this time by telling him he was using Arch wrong. <img src=\"https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f600.png\" alt=\"😀\"/></p>",
"<p>Lets see if I did a better job of it this time by telling him he was using Arch wrong. 😀</p>",
)
}
#[test]
fn replace_emojis_2() {
replace_emojis(
"<p><img src=\"https://abc.com/img.jpeg\"/><img src=\"https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/72x72/1f600.png\" alt=\"😀\"/> Abc</p>",
"<p><img src=\"https://abc.com/img.jpeg\"/>😀 Abc</p>",
)
}
}

View file

@ -1,11 +1,12 @@
[package]
name = "article_scraper_cli"
version = "2.1.0"
authors = ["Jan Lukas Gernert <jangernert@gmail.com>"]
edition = "2018"
license = "GPL-3.0-or-later"
description = "Cli to use the article_scraper lib"
repository = "https://gitlab.com/news-flash/article_scraper"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
article_scraper = { path = "../article_scraper/" }